mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Merge pull request #278 from peanutyost/main
Added Features to Location module
This commit is contained in:
@@ -200,6 +200,12 @@ lat = 48.50
|
|||||||
lon = -123.0
|
lon = -123.0
|
||||||
fuzzConfigLocation = True
|
fuzzConfigLocation = True
|
||||||
fuzzItAll = False
|
fuzzItAll = False
|
||||||
|
# database file for saved locations
|
||||||
|
locations_db = data/locations.db
|
||||||
|
# if True, only administrators can save public locations
|
||||||
|
public_location_admin_manage = False
|
||||||
|
# if True, only administrators can delete locations
|
||||||
|
delete_public_locations_admins_only = False
|
||||||
|
|
||||||
# Default to metric units rather than imperial
|
# Default to metric units rather than imperial
|
||||||
useMetric = False
|
useMetric = False
|
||||||
|
|||||||
@@ -353,16 +353,15 @@ The system uses SQLite with four tables:
|
|||||||
| `howfar` | Distance traveled since last check |
|
| `howfar` | Distance traveled since last check |
|
||||||
| `howtall` | Calculate height using sun angle |
|
| `howtall` | Calculate height using sun angle |
|
||||||
| `whereami` | Show current location/address |
|
| `whereami` | Show current location/address |
|
||||||
| `map` | Log/view location data to map.csv |
|
| `map` | Save/retrieve locations, get headings, manage location database |
|
||||||
Configure in `[location]` section of `config.ini`.
|
Configure in `[location]` section of `config.ini`.
|
||||||
|
|
||||||
Certainly! Here’s a README help section for your `mapHandler` command, suitable for users of your meshbot:
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📍 Map Command
|
## 📍 Map Command
|
||||||
|
|
||||||
The `map` command allows you to log your current GPS location with a custom description. This is useful for mapping mesh nodes, events, or points of interest.
|
The `map` command provides a comprehensive location management system that allows you to save, retrieve, and manage locations in a SQLite database. You can save private locations (visible only to you) or public locations (visible to all nodes), get headings and distances to saved locations, and manage your location data.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
@@ -370,23 +369,117 @@ The `map` command allows you to log your current GPS location with a custom desc
|
|||||||
```
|
```
|
||||||
map help
|
map help
|
||||||
```
|
```
|
||||||
Displays usage instructions for the map command.
|
Displays usage instructions for all map commands.
|
||||||
|
|
||||||
- **Log a Location**
|
- **Save a Private Location**
|
||||||
```
|
```
|
||||||
map <description>
|
map save <name> [description]
|
||||||
```
|
```
|
||||||
|
Saves your current location as a private location (only visible to your node).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
|
map save BaseCamp
|
||||||
|
map save BaseCamp Main base camp location
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Save a Public Location**
|
||||||
|
```
|
||||||
|
map save public <name> [description]
|
||||||
|
```
|
||||||
|
Saves your current location as a public location (visible to all nodes).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
|
map save public TrailHead
|
||||||
|
map save public TrailHead Starting point for hiking trail
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** If `public_location_admin_manage = True` in config, only administrators can save public locations.
|
||||||
|
|
||||||
|
- **Get Heading to a Location**
|
||||||
|
```
|
||||||
|
map <name>
|
||||||
|
```
|
||||||
|
Retrieves a saved location and provides heading (bearing) and distance from your current position.
|
||||||
|
|
||||||
|
The system prioritizes your private location if both private and public locations exist with the same name.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```
|
||||||
map Found a new mesh node near the park
|
map BaseCamp
|
||||||
|
```
|
||||||
|
Response includes:
|
||||||
|
- Location coordinates
|
||||||
|
- Compass heading (bearing)
|
||||||
|
- Distance
|
||||||
|
- Description (if provided)
|
||||||
|
|
||||||
|
- **Get Heading to a Public Location**
|
||||||
|
```
|
||||||
|
map public <name>
|
||||||
|
```
|
||||||
|
Specifically retrieves a public location, even if you have a private location with the same name.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
map public BaseCamp
|
||||||
|
```
|
||||||
|
|
||||||
|
- **List All Saved Locations**
|
||||||
|
```
|
||||||
|
map list
|
||||||
|
```
|
||||||
|
Lists all locations you can access:
|
||||||
|
- Your private locations (🔒Private)
|
||||||
|
- All public locations (🌐Public)
|
||||||
|
|
||||||
|
Locations are sorted with private locations first, then public locations, both alphabetically by name.
|
||||||
|
|
||||||
|
- **Delete a Location**
|
||||||
|
```
|
||||||
|
map delete <name>
|
||||||
|
```
|
||||||
|
Deletes a location from the database.
|
||||||
|
|
||||||
|
**Permission Rules:**
|
||||||
|
- If `delete_public_locations_admins_only = False` (default):
|
||||||
|
- Users can delete their own private locations
|
||||||
|
- Users can delete public locations they created
|
||||||
|
- Anyone can delete any public location
|
||||||
|
- If `delete_public_locations_admins_only = True`:
|
||||||
|
- Only administrators can delete public locations
|
||||||
|
|
||||||
|
The system prioritizes deleting your private location if both private and public locations exist with the same name.
|
||||||
|
|
||||||
|
- **Legacy CSV Logging**
|
||||||
|
```
|
||||||
|
map log <description>
|
||||||
|
```
|
||||||
|
Logs your current location to the legacy CSV file (`data/map_data.csv`) with a description. This is the original map functionality preserved for backward compatibility.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
map log Found a new mesh node near the park
|
||||||
```
|
```
|
||||||
This will log your current location with the description "Found a new mesh node near the park".
|
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
- The bot records your user ID, latitude, longitude, and your description in a CSV file (`data/map_data.csv`).
|
- **Database Storage:** All locations are stored in a SQLite database (`data/locations.db` by default, configurable via `locations_db` in config.ini).
|
||||||
- If your location data is missing or invalid, you’ll receive an error message.
|
- **Location Types:**
|
||||||
- You can view or process the CSV file later for mapping or analysis.
|
- **Private Locations:** Only visible to the node that created them
|
||||||
|
- **Public Locations:** Visible to all nodes
|
||||||
|
- **Conflict Resolution:** If you try to save a private location with the same name as an existing public location, you'll be prompted that there is a the public record with that name.
|
||||||
|
- **Distance Calculation:** Uses the Haversine formula for accurate distance calculations. Distances less than 0.25 miles are displayed in feet; otherwise in miles (or kilometers if metric is enabled).
|
||||||
|
- **Heading Calculation:** Provides compass bearing (0-360 degrees) from your current location to the target location.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Configure in `[location]` section of `config.ini`:
|
||||||
|
|
||||||
|
- `locations_db` - Path to the SQLite database file (default: `data/locations.db`)
|
||||||
|
- `public_location_admin_manage` - If `True`, only administrators can save public locations (default: `False`)
|
||||||
|
- `delete_public_locations_admins_only` - If `True`, only administrators can delete locations (default: `False`)
|
||||||
|
|
||||||
**Tip:** Use `map help` at any time to see these instructions in the bot.
|
**Tip:** Use `map help` at any time to see these instructions in the bot.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import modules.settings as my_settings
|
|||||||
import math
|
import math
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar", "map",)
|
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar", "map",)
|
||||||
|
|
||||||
@@ -1171,6 +1172,507 @@ def get_openskynetwork(lat=0, lon=0, altitude=0, node_altitude=0, altitude_windo
|
|||||||
logger.debug(f"SYSTEM: Location HighFly: Error processing OpenSky Network data: {e}")
|
logger.debug(f"SYSTEM: Location HighFly: Error processing OpenSky Network data: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_public_location_admin_manage():
|
||||||
|
"""Get the public_location_admin_manage setting directly from config file
|
||||||
|
This ensures the setting is reloaded fresh on first load of the program
|
||||||
|
"""
|
||||||
|
import configparser
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
config.read("config.ini", encoding='utf-8')
|
||||||
|
return config['location'].getboolean('public_location_admin_manage', False)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_delete_public_locations_admins_only():
|
||||||
|
"""Get the delete_public_locations_admins_only setting directly from config file
|
||||||
|
This ensures the setting is reloaded fresh on first load of the program
|
||||||
|
"""
|
||||||
|
import configparser
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
config.read("config.ini", encoding='utf-8')
|
||||||
|
return config['location'].getboolean('delete_public_locations_admins_only', False)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_node_altitude(nodeID, deviceID=1):
|
||||||
|
"""Get altitude for a node from position data or positionMetadata
|
||||||
|
|
||||||
|
Returns altitude in meters, or None if not available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import modules.system as system_module
|
||||||
|
|
||||||
|
# Try to get altitude from node position dict first
|
||||||
|
# Access interface dynamically from system module
|
||||||
|
interface = getattr(system_module, f'interface{deviceID}', None)
|
||||||
|
if interface and hasattr(interface, 'nodes') and interface.nodes:
|
||||||
|
for node in interface.nodes.values():
|
||||||
|
if nodeID == node['num']:
|
||||||
|
pos = node.get('position')
|
||||||
|
if pos and isinstance(pos, dict) and pos.get('altitude') is not None:
|
||||||
|
try:
|
||||||
|
altitude = float(pos['altitude'])
|
||||||
|
if altitude > 0: # Valid altitude
|
||||||
|
return altitude
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fall back to positionMetadata (from POSITION_APP packets)
|
||||||
|
positionMetadata = getattr(system_module, 'positionMetadata', None)
|
||||||
|
if positionMetadata and nodeID in positionMetadata:
|
||||||
|
metadata = positionMetadata[nodeID]
|
||||||
|
if 'altitude' in metadata:
|
||||||
|
altitude = metadata.get('altitude', 0)
|
||||||
|
if altitude and altitude > 0:
|
||||||
|
return float(altitude)
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Location: Error getting altitude for node {nodeID}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def initialize_locations_database():
|
||||||
|
"""Initialize the SQLite database for storing saved locations"""
|
||||||
|
try:
|
||||||
|
# Ensure data directory exists
|
||||||
|
db_dir = os.path.dirname(my_settings.locations_db)
|
||||||
|
if db_dir:
|
||||||
|
os.makedirs(db_dir, exist_ok=True)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(my_settings.locations_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
logger.debug("Location: Initializing locations database...")
|
||||||
|
|
||||||
|
# Check if table exists and get its structure
|
||||||
|
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='locations'")
|
||||||
|
table_exists = c.fetchone() is not None
|
||||||
|
|
||||||
|
if table_exists:
|
||||||
|
# Check if is_public column exists
|
||||||
|
c.execute("PRAGMA table_info(locations)")
|
||||||
|
columns_info = c.fetchall()
|
||||||
|
column_names = [col[1] for col in columns_info]
|
||||||
|
|
||||||
|
# Check for UNIQUE constraint on location_name by examining the table schema
|
||||||
|
c.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='locations'")
|
||||||
|
table_sql = c.fetchone()
|
||||||
|
has_unique_constraint = False
|
||||||
|
if table_sql and table_sql[0]:
|
||||||
|
# Check if UNIQUE constraint exists in the table definition
|
||||||
|
if 'UNIQUE' in table_sql[0].upper() and 'location_name' in table_sql[0]:
|
||||||
|
has_unique_constraint = True
|
||||||
|
|
||||||
|
# If UNIQUE constraint exists, we need to recreate the table
|
||||||
|
if has_unique_constraint:
|
||||||
|
logger.debug("Location: Removing UNIQUE constraint from locations table")
|
||||||
|
# Create temporary table without UNIQUE constraint
|
||||||
|
c.execute('''CREATE TABLE locations_new
|
||||||
|
(location_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
location_name TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
altitude REAL,
|
||||||
|
description TEXT,
|
||||||
|
userID TEXT,
|
||||||
|
is_public INTEGER DEFAULT 0,
|
||||||
|
created_date TEXT,
|
||||||
|
created_time TEXT)''')
|
||||||
|
|
||||||
|
# Copy data from old table to new table
|
||||||
|
c.execute('''INSERT INTO locations_new
|
||||||
|
(location_id, location_name, latitude, longitude, description, userID,
|
||||||
|
is_public, created_date, created_time)
|
||||||
|
SELECT location_id, location_name, latitude, longitude, description, userID,
|
||||||
|
COALESCE(is_public, 0), created_date, created_time
|
||||||
|
FROM locations''')
|
||||||
|
|
||||||
|
# Drop old table
|
||||||
|
c.execute("DROP TABLE locations")
|
||||||
|
|
||||||
|
# Rename new table
|
||||||
|
c.execute("ALTER TABLE locations_new RENAME TO locations")
|
||||||
|
|
||||||
|
logger.debug("Location: Successfully removed UNIQUE constraint")
|
||||||
|
|
||||||
|
# Refresh column list after table recreation
|
||||||
|
c.execute("PRAGMA table_info(locations)")
|
||||||
|
columns_info = c.fetchall()
|
||||||
|
column_names = [col[1] for col in columns_info]
|
||||||
|
|
||||||
|
# Add is_public column if it doesn't exist (migration)
|
||||||
|
if 'is_public' not in column_names:
|
||||||
|
try:
|
||||||
|
c.execute('''ALTER TABLE locations ADD COLUMN is_public INTEGER DEFAULT 0''')
|
||||||
|
logger.debug("Location: Added is_public column to locations table")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
# Column might already exist, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add altitude column if it doesn't exist (migration)
|
||||||
|
if 'altitude' not in column_names:
|
||||||
|
try:
|
||||||
|
c.execute('''ALTER TABLE locations ADD COLUMN altitude REAL''')
|
||||||
|
logger.debug("Location: Added altitude column to locations table")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
# Column might already exist, ignore
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Table doesn't exist, create it without UNIQUE constraint
|
||||||
|
c.execute('''CREATE TABLE locations
|
||||||
|
(location_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
location_name TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
altitude REAL,
|
||||||
|
description TEXT,
|
||||||
|
userID TEXT,
|
||||||
|
is_public INTEGER DEFAULT 0,
|
||||||
|
created_date TEXT,
|
||||||
|
created_time TEXT)''')
|
||||||
|
|
||||||
|
# Create index for faster lookups (non-unique)
|
||||||
|
c.execute('''CREATE INDEX IF NOT EXISTS idx_location_name_user
|
||||||
|
ON locations(location_name, userID, is_public)''')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Location: Failed to initialize locations database: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_location_to_db(location_name, lat, lon, description="", userID="", is_public=False, altitude=None):
|
||||||
|
"""Save a location to the SQLite database
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(success, message, conflict_info)
|
||||||
|
conflict_info is None if no conflict, or dict with conflict details if conflict exists
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not location_name or not location_name.strip():
|
||||||
|
return False, "Location name cannot be empty", None
|
||||||
|
|
||||||
|
# Check if public locations are admin-only and user is not admin
|
||||||
|
if is_public and get_public_location_admin_manage():
|
||||||
|
from modules.system import isNodeAdmin
|
||||||
|
if not isNodeAdmin(userID):
|
||||||
|
return False, "Only admins can save public locations.", None
|
||||||
|
|
||||||
|
conn = sqlite3.connect(my_settings.locations_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
location_name_clean = location_name.strip()
|
||||||
|
|
||||||
|
# Check for conflicts
|
||||||
|
# 1. Check if user already has a location with this name (private or public)
|
||||||
|
c.execute('''SELECT location_id, is_public FROM locations
|
||||||
|
WHERE location_name = ? AND userID = ?''',
|
||||||
|
(location_name_clean, userID))
|
||||||
|
user_existing = c.fetchone()
|
||||||
|
|
||||||
|
if user_existing:
|
||||||
|
conn.close()
|
||||||
|
return False, f"Location '{location_name}' already exists for your node", None
|
||||||
|
|
||||||
|
# 2. Check if there's a public location with this name
|
||||||
|
# Note: We allow public locations to overlap with other users' private locations
|
||||||
|
# Only check for existing public locations to prevent duplicate public locations
|
||||||
|
c.execute('''SELECT location_id, userID, description FROM locations
|
||||||
|
WHERE location_name = ? AND is_public = 1''',
|
||||||
|
(location_name_clean,))
|
||||||
|
public_existing = c.fetchone()
|
||||||
|
|
||||||
|
if public_existing:
|
||||||
|
if not is_public:
|
||||||
|
# User is trying to create private location but public one exists
|
||||||
|
# They can use "map public <name>" to access the public location
|
||||||
|
conn.close()
|
||||||
|
return False, f"Public location '{location_name}' already exists. Use 'map public {location_name}' to access it.", None
|
||||||
|
else:
|
||||||
|
# User is trying to create public location but one already exists
|
||||||
|
# Only one public location per name globally
|
||||||
|
conn.close()
|
||||||
|
return False, f"Public location '{location_name}' already exists", None
|
||||||
|
|
||||||
|
# 3. If saving as public, we don't check for other users' private locations
|
||||||
|
# This allows public locations to overlap with private location names
|
||||||
|
|
||||||
|
# Insert new location
|
||||||
|
now = datetime.now()
|
||||||
|
c.execute('''INSERT INTO locations
|
||||||
|
(location_name, latitude, longitude, altitude, description, userID, is_public, created_date, created_time)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||||
|
(location_name_clean, lat, lon, altitude, description, userID, 1 if is_public else 0,
|
||||||
|
now.strftime("%Y-%m-%d"), now.strftime("%H:%M:%S")))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
visibility = "public" if is_public else "private"
|
||||||
|
logger.debug(f"Location: Saved {visibility} location '{location_name}' to database")
|
||||||
|
return True, f"Location '{location_name}' saved as {visibility}", None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Location: Failed to save location: {e}")
|
||||||
|
return False, f"Error saving location: {e}", None
|
||||||
|
|
||||||
|
def get_location_from_db(location_name, userID=None):
|
||||||
|
"""Retrieve a location from the database by name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- User's private location if exists
|
||||||
|
- Public location if exists
|
||||||
|
- None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(my_settings.locations_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
location_name_clean = location_name.strip()
|
||||||
|
|
||||||
|
# First, try to get user's private location
|
||||||
|
if userID:
|
||||||
|
c.execute('''SELECT location_name, latitude, longitude, altitude, description, userID, is_public, created_date, created_time
|
||||||
|
FROM locations
|
||||||
|
WHERE location_name = ? AND userID = ? AND is_public = 0''',
|
||||||
|
(location_name_clean, userID))
|
||||||
|
result = c.fetchone()
|
||||||
|
if result:
|
||||||
|
conn.close()
|
||||||
|
return {
|
||||||
|
'name': result[0],
|
||||||
|
'lat': result[1],
|
||||||
|
'lon': result[2],
|
||||||
|
'altitude': result[3],
|
||||||
|
'description': result[4],
|
||||||
|
'userID': result[5],
|
||||||
|
'is_public': bool(result[6]),
|
||||||
|
'created_date': result[7],
|
||||||
|
'created_time': result[8]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Then try public location
|
||||||
|
c.execute('''SELECT location_name, latitude, longitude, altitude, description, userID, is_public, created_date, created_time
|
||||||
|
FROM locations
|
||||||
|
WHERE location_name = ? AND is_public = 1''',
|
||||||
|
(location_name_clean,))
|
||||||
|
result = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {
|
||||||
|
'name': result[0],
|
||||||
|
'lat': result[1],
|
||||||
|
'lon': result[2],
|
||||||
|
'altitude': result[3],
|
||||||
|
'description': result[4],
|
||||||
|
'userID': result[5],
|
||||||
|
'is_public': bool(result[6]),
|
||||||
|
'created_date': result[7],
|
||||||
|
'created_time': result[8]
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Location: Failed to retrieve location: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_public_location_from_db(location_name):
|
||||||
|
"""Retrieve only a public location from the database by name (ignores private locations)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Public location if exists
|
||||||
|
- None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(my_settings.locations_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
location_name_clean = location_name.strip()
|
||||||
|
|
||||||
|
# Get only public location
|
||||||
|
c.execute('''SELECT location_name, latitude, longitude, altitude, description, userID, is_public, created_date, created_time
|
||||||
|
FROM locations
|
||||||
|
WHERE location_name = ? AND is_public = 1''',
|
||||||
|
(location_name_clean,))
|
||||||
|
result = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {
|
||||||
|
'name': result[0],
|
||||||
|
'lat': result[1],
|
||||||
|
'lon': result[2],
|
||||||
|
'altitude': result[3],
|
||||||
|
'description': result[4],
|
||||||
|
'userID': result[5],
|
||||||
|
'is_public': bool(result[6]),
|
||||||
|
'created_date': result[7],
|
||||||
|
'created_time': result[8]
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Location: Failed to retrieve public location: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_locations_from_db(userID=None):
|
||||||
|
"""List saved locations
|
||||||
|
|
||||||
|
Shows:
|
||||||
|
- User's private locations
|
||||||
|
- All public locations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(my_settings.locations_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if userID:
|
||||||
|
# Get user's private locations and all public locations
|
||||||
|
c.execute('''SELECT location_name, latitude, longitude, altitude, description, is_public, created_date
|
||||||
|
FROM locations
|
||||||
|
WHERE (userID = ? AND is_public = 0) OR is_public = 1
|
||||||
|
ORDER BY is_public ASC, location_name''', (userID,))
|
||||||
|
else:
|
||||||
|
# Get all public locations only
|
||||||
|
c.execute('''SELECT location_name, latitude, longitude, altitude, description, is_public, created_date
|
||||||
|
FROM locations
|
||||||
|
WHERE is_public = 1
|
||||||
|
ORDER BY location_name''')
|
||||||
|
|
||||||
|
results = c.fetchall() # Get ALL results, no limit
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return "No saved locations found"
|
||||||
|
|
||||||
|
locations_list = f"Saved Locations ({len(results)} total):\n"
|
||||||
|
# Return ALL results, not limited
|
||||||
|
for result in results:
|
||||||
|
is_public = bool(result[5])
|
||||||
|
visibility = "🌐Public" if is_public else "🔒Private"
|
||||||
|
locations_list += f" • {result[0]} ({result[1]:.5f}, {result[2]:.5f})"
|
||||||
|
if result[3] is not None: # altitude
|
||||||
|
locations_list += f" @ {result[3]:.1f}m"
|
||||||
|
locations_list += f" [{visibility}]"
|
||||||
|
if result[4]: # description
|
||||||
|
locations_list += f" - {result[4]}"
|
||||||
|
locations_list += "\n"
|
||||||
|
return locations_list.strip()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Location: Failed to list locations: {e}")
|
||||||
|
return f"Error listing locations: {e}"
|
||||||
|
|
||||||
|
def delete_location_from_db(location_name, userID=""):
|
||||||
|
"""Delete a location from the database
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(success, message)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not location_name or not location_name.strip():
|
||||||
|
return False, "Location name cannot be empty"
|
||||||
|
|
||||||
|
conn = sqlite3.connect(my_settings.locations_db)
|
||||||
|
c = conn.cursor()
|
||||||
|
location_name_clean = location_name.strip()
|
||||||
|
|
||||||
|
# Check if location exists - prioritize user's private location, then public
|
||||||
|
# First try to get user's private location
|
||||||
|
c.execute('''SELECT location_id, userID, is_public FROM locations
|
||||||
|
WHERE location_name = ? AND userID = ? AND is_public = 0''',
|
||||||
|
(location_name_clean, userID))
|
||||||
|
location = c.fetchone()
|
||||||
|
|
||||||
|
# If not found, try public location
|
||||||
|
if not location:
|
||||||
|
c.execute('''SELECT location_id, userID, is_public FROM locations
|
||||||
|
WHERE location_name = ? AND is_public = 1''',
|
||||||
|
(location_name_clean,))
|
||||||
|
location = c.fetchone()
|
||||||
|
|
||||||
|
# If still not found, try any location (for admin delete)
|
||||||
|
if not location:
|
||||||
|
c.execute('''SELECT location_id, userID, is_public FROM locations
|
||||||
|
WHERE location_name = ? LIMIT 1''',
|
||||||
|
(location_name_clean,))
|
||||||
|
location = c.fetchone()
|
||||||
|
|
||||||
|
if not location:
|
||||||
|
conn.close()
|
||||||
|
return False, f"Location '{location_name}' not found"
|
||||||
|
|
||||||
|
location_id, location_userID, is_public = location
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
# Users can only delete their own private locations
|
||||||
|
# Admins can delete any location if delete_public_locations_admins_only is enabled
|
||||||
|
is_admin = False
|
||||||
|
if get_delete_public_locations_admins_only():
|
||||||
|
from modules.system import isNodeAdmin
|
||||||
|
is_admin = isNodeAdmin(userID)
|
||||||
|
|
||||||
|
# Check if user owns this location
|
||||||
|
is_owner = (str(location_userID) == str(userID))
|
||||||
|
|
||||||
|
# Determine if deletion is allowed
|
||||||
|
can_delete = False
|
||||||
|
if is_public:
|
||||||
|
# Public locations: only admins can delete if admin-only is enabled
|
||||||
|
if get_delete_public_locations_admins_only():
|
||||||
|
can_delete = is_admin
|
||||||
|
else:
|
||||||
|
# If not admin-only, then anyone can delete public locations
|
||||||
|
can_delete = True
|
||||||
|
else:
|
||||||
|
# Private locations: owner can always delete
|
||||||
|
can_delete = is_owner
|
||||||
|
|
||||||
|
if not can_delete:
|
||||||
|
conn.close()
|
||||||
|
if is_public and get_delete_public_locations_admins_only():
|
||||||
|
return False, "Only admins can delete public locations."
|
||||||
|
else:
|
||||||
|
return False, f"You can only delete your own locations. This location belongs to another user."
|
||||||
|
|
||||||
|
# Delete the location
|
||||||
|
c.execute('''DELETE FROM locations WHERE location_id = ?''', (location_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
visibility = "public" if is_public else "private"
|
||||||
|
logger.debug(f"Location: Deleted {visibility} location '{location_name}' from database")
|
||||||
|
return True, f"Location '{location_name}' deleted"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Location: Failed to delete location: {e}")
|
||||||
|
return False, f"Error deleting location: {e}"
|
||||||
|
|
||||||
|
def calculate_heading_and_distance(lat1, lon1, lat2, lon2):
|
||||||
|
"""Calculate heading (bearing) and distance between two points"""
|
||||||
|
if lat1 == 0 and lon1 == 0:
|
||||||
|
return None, None, "Current location not available"
|
||||||
|
if lat2 == 0 and lon2 == 0:
|
||||||
|
return None, None, "Target location not available"
|
||||||
|
|
||||||
|
# Calculate distance using Haversine formula
|
||||||
|
r = 6371 # Earth radius in kilometers
|
||||||
|
lat1_rad = math.radians(lat1)
|
||||||
|
lon1_rad = math.radians(lon1)
|
||||||
|
lat2_rad = math.radians(lat2)
|
||||||
|
lon2_rad = math.radians(lon2)
|
||||||
|
|
||||||
|
dlon = lon2_rad - lon1_rad
|
||||||
|
dlat = lat2_rad - lat1_rad
|
||||||
|
|
||||||
|
a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2
|
||||||
|
c = 2 * math.asin(math.sqrt(a))
|
||||||
|
distance_km = c * r
|
||||||
|
|
||||||
|
# Calculate bearing
|
||||||
|
x = math.sin(dlon) * math.cos(lat2_rad)
|
||||||
|
y = math.cos(lat1_rad) * math.sin(lat2_rad) - (math.sin(lat1_rad) * math.cos(lat2_rad) * math.cos(dlon))
|
||||||
|
initial_bearing = math.atan2(x, y)
|
||||||
|
initial_bearing = math.degrees(initial_bearing)
|
||||||
|
compass_bearing = (initial_bearing + 360) % 360
|
||||||
|
|
||||||
|
return compass_bearing, distance_km, None
|
||||||
|
|
||||||
def log_locationData_toMap(userID, location, message):
|
def log_locationData_toMap(userID, location, message):
|
||||||
"""
|
"""
|
||||||
Logs location data to a CSV file for meshing purposes.
|
Logs location data to a CSV file for meshing purposes.
|
||||||
@@ -1219,40 +1721,277 @@ def mapHandler(userID, deviceID, channel_number, message, snr, rssi, hop):
|
|||||||
"""
|
"""
|
||||||
Handles 'map' commands from meshbot.
|
Handles 'map' commands from meshbot.
|
||||||
Usage:
|
Usage:
|
||||||
map <description text> - Log current location with description
|
map save <name> [description] - Save current location with a name
|
||||||
|
map save public <name> [desc] - Save public location (all can see)
|
||||||
|
map <name> - Get heading and distance to a saved location
|
||||||
|
map public <name> - Get heading to public location (ignores private)
|
||||||
|
map delete <name> - Delete a location
|
||||||
|
map list - List all saved locations
|
||||||
|
map log <description> - Log current location with description (CSV, legacy)
|
||||||
"""
|
"""
|
||||||
command = str(command) # Ensure command is always a string
|
command = str(command) # Ensure command is always a string
|
||||||
|
|
||||||
if command.strip().lower() == "?":
|
if command.strip().lower() == "help":
|
||||||
return (
|
return (
|
||||||
"Usage:\n"
|
f"'map save <name> [description]' - Save private\n"
|
||||||
" 🗺️map <description text> - Log your current location with a description\n"
|
f"'map save public <name> [desc]' - Save public\n"
|
||||||
"Example:\n"
|
f"'map <name>' - heading to saved\n"
|
||||||
" 🗺️map Found a new mesh node near the park"
|
f"'map public <name>' - heading to public\n"
|
||||||
|
f"'map delete <name>' \n"
|
||||||
|
f"'map list' - List\n"
|
||||||
|
f"'map log <description>' - Log CSV\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
description = command.strip()
|
# Handle "save" command
|
||||||
# if no description provided, set to default
|
if command.lower().startswith("save "):
|
||||||
if not description:
|
save_cmd = command[5:].strip()
|
||||||
description = "Logged:"
|
is_public = False
|
||||||
# Sanitize description for CSV injection
|
|
||||||
if description and description[0] in ('=', '+', '-', '@'):
|
|
||||||
description = "'" + description
|
|
||||||
|
|
||||||
# if there is SNR and RSSI info, append to description
|
# Check for "public" keyword
|
||||||
if snr is not None and rssi is not None:
|
if save_cmd.lower().startswith("public "):
|
||||||
description += f" SNR:{snr}dB RSSI:{rssi}dBm"
|
is_public = True
|
||||||
|
save_cmd = save_cmd[7:].strip() # Remove "public " prefix
|
||||||
|
|
||||||
# if there is hop info, append to description
|
parts = save_cmd.split(" ", 1)
|
||||||
if hop is not None:
|
if len(parts) < 1 or not parts[0]:
|
||||||
description += f" Meta:{hop}"
|
if is_public:
|
||||||
|
return "🚫Usage: map save public <name> [description]"
|
||||||
|
else:
|
||||||
|
return "🚫Usage: map save <name> [description]"
|
||||||
|
|
||||||
# location should be a tuple: (lat, lon)
|
location_name = parts[0]
|
||||||
if not location or len(location) != 2:
|
description = parts[1] if len(parts) > 1 else ""
|
||||||
return "🚫Location data is missing or invalid."
|
|
||||||
|
|
||||||
success = log_locationData_toMap(userID, location, description)
|
# Add SNR/RSSI info to description if available
|
||||||
if success:
|
if snr is not None and rssi is not None:
|
||||||
return f"📍Location logged "
|
if description:
|
||||||
else:
|
description += f" SNR:{snr}dB RSSI:{rssi}dBm"
|
||||||
return "🚫Failed to log location. Please try again."
|
else:
|
||||||
|
description = f"SNR:{snr}dB RSSI:{rssi}dBm"
|
||||||
|
|
||||||
|
if hop is not None:
|
||||||
|
if description:
|
||||||
|
description += f" Meta:{hop}"
|
||||||
|
else:
|
||||||
|
description = f"Meta:{hop}"
|
||||||
|
|
||||||
|
if not location or len(location) != 2 or lat == 0 or lon == 0:
|
||||||
|
return "🚫Location data is missing or invalid."
|
||||||
|
|
||||||
|
# Get altitude for the node
|
||||||
|
altitude = get_node_altitude(userID, deviceID)
|
||||||
|
|
||||||
|
success, msg, _ = save_location_to_db(location_name, lat, lon, description, str(userID), is_public, altitude)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return f"📍{msg}"
|
||||||
|
else:
|
||||||
|
return f"🚫{msg}"
|
||||||
|
|
||||||
|
# Handle "list" command
|
||||||
|
if command.strip().lower() == "list":
|
||||||
|
return list_locations_from_db(str(userID))
|
||||||
|
|
||||||
|
# Handle "delete" command
|
||||||
|
if command.lower().startswith("delete "):
|
||||||
|
location_name = command[7:].strip() # Remove "delete " prefix
|
||||||
|
if not location_name:
|
||||||
|
return "🚫Usage: map delete <name>"
|
||||||
|
|
||||||
|
success, msg = delete_location_from_db(location_name, str(userID))
|
||||||
|
if success:
|
||||||
|
return f"🗑️{msg}"
|
||||||
|
else:
|
||||||
|
return f"🚫{msg}"
|
||||||
|
|
||||||
|
# Handle "public" command to retrieve public locations (even if user has private with same name)
|
||||||
|
if command.lower().startswith("public "):
|
||||||
|
location_name = command[7:].strip() # Remove "public " prefix
|
||||||
|
if not location_name:
|
||||||
|
return "🚫Usage: map public <name>"
|
||||||
|
|
||||||
|
saved_location = get_public_location_from_db(location_name)
|
||||||
|
|
||||||
|
if saved_location:
|
||||||
|
# Calculate heading and distance from current location
|
||||||
|
if not location or len(location) != 2 or lat == 0 or lon == 0:
|
||||||
|
result = f"📍{saved_location['name']} (Public): {saved_location['lat']:.5f}, {saved_location['lon']:.5f}"
|
||||||
|
if saved_location.get('altitude') is not None:
|
||||||
|
result += f" @ {saved_location['altitude']:.1f}m"
|
||||||
|
result += "\n🚫Current location not available for heading"
|
||||||
|
return result
|
||||||
|
|
||||||
|
bearing, distance_km, error = calculate_heading_and_distance(
|
||||||
|
lat, lon, saved_location['lat'], saved_location['lon']
|
||||||
|
)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return f"📍{saved_location['name']} (Public): {error}"
|
||||||
|
|
||||||
|
# Format distance
|
||||||
|
if my_settings.use_metric:
|
||||||
|
distance_str = f"{distance_km:.2f} km"
|
||||||
|
else:
|
||||||
|
distance_miles = distance_km * 0.621371
|
||||||
|
if distance_miles < 0.25:
|
||||||
|
# Convert to feet for short distances
|
||||||
|
distance_feet = distance_miles * 5280
|
||||||
|
distance_str = f"{distance_feet:.0f} ft"
|
||||||
|
else:
|
||||||
|
distance_str = f"{distance_miles:.2f} miles"
|
||||||
|
|
||||||
|
# Format bearing with cardinal direction
|
||||||
|
bearing_rounded = round(bearing)
|
||||||
|
cardinal = ""
|
||||||
|
if bearing_rounded == 0 or bearing_rounded == 360:
|
||||||
|
cardinal = "N"
|
||||||
|
elif bearing_rounded == 90:
|
||||||
|
cardinal = "E"
|
||||||
|
elif bearing_rounded == 180:
|
||||||
|
cardinal = "S"
|
||||||
|
elif bearing_rounded == 270:
|
||||||
|
cardinal = "W"
|
||||||
|
elif 0 < bearing_rounded < 90:
|
||||||
|
cardinal = "NE"
|
||||||
|
elif 90 < bearing_rounded < 180:
|
||||||
|
cardinal = "SE"
|
||||||
|
elif 180 < bearing_rounded < 270:
|
||||||
|
cardinal = "SW"
|
||||||
|
elif 270 < bearing_rounded < 360:
|
||||||
|
cardinal = "NW"
|
||||||
|
|
||||||
|
result = f"📍{saved_location['name']} (Public)\n"
|
||||||
|
result += f"🧭Heading: {bearing_rounded}° {cardinal}\n"
|
||||||
|
result += f"📏Distance: {distance_str}"
|
||||||
|
|
||||||
|
# Calculate altitude difference if both are available
|
||||||
|
current_altitude = get_node_altitude(userID, deviceID)
|
||||||
|
saved_altitude = saved_location.get('altitude')
|
||||||
|
if current_altitude is not None and saved_altitude is not None:
|
||||||
|
altitude_diff_m = saved_altitude - current_altitude # message altitude - DB altitude
|
||||||
|
altitude_diff_ft = altitude_diff_m * 3.28084 # Convert meters to feet
|
||||||
|
altitude_diff_ft_rounded = round(altitude_diff_ft) # Round to nearest foot
|
||||||
|
if altitude_diff_ft_rounded > 0:
|
||||||
|
result += f"\n⛰️Altitude: +{altitude_diff_ft_rounded}ft" # Message is higher
|
||||||
|
elif altitude_diff_ft_rounded < 0:
|
||||||
|
result += f"\n⛰️Altitude: {altitude_diff_ft_rounded}ft" # Message is lower (negative already has -)
|
||||||
|
else:
|
||||||
|
result += f"\n⛰️Altitude: ±0ft"
|
||||||
|
|
||||||
|
if saved_location['description']:
|
||||||
|
result += f"\n📝{saved_location['description']}"
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return f"🚫Public location '{location_name}' not found."
|
||||||
|
|
||||||
|
# Handle "log" command for CSV logging
|
||||||
|
if command.lower().startswith("log "):
|
||||||
|
description = command[4:].strip() # Remove "log " prefix
|
||||||
|
# if no description provided, set to default
|
||||||
|
if not description:
|
||||||
|
description = "Logged:"
|
||||||
|
# Sanitize description for CSV injection
|
||||||
|
if description and description[0] in ('=', '+', '-', '@'):
|
||||||
|
description = "'" + description
|
||||||
|
|
||||||
|
# if there is SNR and RSSI info, append to description
|
||||||
|
if snr is not None and rssi is not None:
|
||||||
|
description += f" SNR:{snr}dB RSSI:{rssi}dBm"
|
||||||
|
|
||||||
|
# if there is hop info, append to description
|
||||||
|
if hop is not None:
|
||||||
|
description += f" Meta:{hop}"
|
||||||
|
|
||||||
|
# location should be a tuple: (lat, lon)
|
||||||
|
if not location or len(location) != 2:
|
||||||
|
return "🚫Location data is missing or invalid."
|
||||||
|
|
||||||
|
success = log_locationData_toMap(userID, location, description)
|
||||||
|
if success:
|
||||||
|
return f"📍Location logged (CSV)"
|
||||||
|
else:
|
||||||
|
return "🚫Failed to log location. Please try again."
|
||||||
|
|
||||||
|
# Handle location name lookup (get heading)
|
||||||
|
if command.strip():
|
||||||
|
location_name = command.strip()
|
||||||
|
saved_location = get_location_from_db(location_name, str(userID))
|
||||||
|
|
||||||
|
if saved_location:
|
||||||
|
# Calculate heading and distance from current location
|
||||||
|
if not location or len(location) != 2 or lat == 0 or lon == 0:
|
||||||
|
result = f"📍{saved_location['name']}: {saved_location['lat']:.5f}, {saved_location['lon']:.5f}"
|
||||||
|
if saved_location.get('altitude') is not None:
|
||||||
|
result += f" @ {saved_location['altitude']:.1f}m"
|
||||||
|
result += "\n🚫Current location not available for heading"
|
||||||
|
return result
|
||||||
|
|
||||||
|
bearing, distance_km, error = calculate_heading_and_distance(
|
||||||
|
lat, lon, saved_location['lat'], saved_location['lon']
|
||||||
|
)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return f"📍{saved_location['name']}: {error}"
|
||||||
|
|
||||||
|
# Format distance
|
||||||
|
if my_settings.use_metric:
|
||||||
|
distance_str = f"{distance_km:.2f} km"
|
||||||
|
else:
|
||||||
|
distance_miles = distance_km * 0.621371
|
||||||
|
if distance_miles < 0.25:
|
||||||
|
# Convert to feet for short distances
|
||||||
|
distance_feet = distance_miles * 5280
|
||||||
|
distance_str = f"{distance_feet:.0f} ft"
|
||||||
|
else:
|
||||||
|
distance_str = f"{distance_miles:.2f} miles"
|
||||||
|
|
||||||
|
# Format bearing with cardinal direction
|
||||||
|
bearing_rounded = round(bearing)
|
||||||
|
cardinal = ""
|
||||||
|
if bearing_rounded == 0 or bearing_rounded == 360:
|
||||||
|
cardinal = "N"
|
||||||
|
elif bearing_rounded == 90:
|
||||||
|
cardinal = "E"
|
||||||
|
elif bearing_rounded == 180:
|
||||||
|
cardinal = "S"
|
||||||
|
elif bearing_rounded == 270:
|
||||||
|
cardinal = "W"
|
||||||
|
elif 0 < bearing_rounded < 90:
|
||||||
|
cardinal = "NE"
|
||||||
|
elif 90 < bearing_rounded < 180:
|
||||||
|
cardinal = "SE"
|
||||||
|
elif 180 < bearing_rounded < 270:
|
||||||
|
cardinal = "SW"
|
||||||
|
elif 270 < bearing_rounded < 360:
|
||||||
|
cardinal = "NW"
|
||||||
|
|
||||||
|
result = f"📍{saved_location['name']}\n"
|
||||||
|
result += f"🧭Heading: {bearing_rounded}° {cardinal}\n"
|
||||||
|
result += f"📏Distance: {distance_str}"
|
||||||
|
|
||||||
|
# Calculate altitude difference if both are available
|
||||||
|
current_altitude = get_node_altitude(userID, deviceID)
|
||||||
|
saved_altitude = saved_location.get('altitude')
|
||||||
|
if current_altitude is not None and saved_altitude is not None:
|
||||||
|
altitude_diff_m = saved_altitude - current_altitude # message altitude - DB altitude
|
||||||
|
altitude_diff_ft = altitude_diff_m * 3.28084 # Convert meters to feet
|
||||||
|
altitude_diff_ft_rounded = round(altitude_diff_ft) # Round to nearest foot
|
||||||
|
if altitude_diff_ft_rounded > 0:
|
||||||
|
result += f"\n⛰️Altitude: +{altitude_diff_ft_rounded}ft" # Message is higher
|
||||||
|
elif altitude_diff_ft_rounded < 0:
|
||||||
|
result += f"\n⛰️Altitude: {altitude_diff_ft_rounded}ft" # Message is lower (negative already has -)
|
||||||
|
else:
|
||||||
|
result += f"\n⛰️Altitude: ±0ft"
|
||||||
|
|
||||||
|
if saved_location['description']:
|
||||||
|
result += f"\n📝{saved_location['description']}"
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# Location not found
|
||||||
|
return f"🚫Location '{location_name}' not found. Use 'map list' to see available locations."
|
||||||
|
|
||||||
|
# Empty command - show help
|
||||||
|
return "🗺️Use 'map help' for help"
|
||||||
|
|
||||||
|
# Initialize the locations database when module is imported
|
||||||
|
initialize_locations_database()
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ if 'inventory' not in config:
|
|||||||
config['inventory'] = {'enabled': 'False', 'inventory_db': 'data/inventory.db', 'disable_penny': 'False'}
|
config['inventory'] = {'enabled': 'False', 'inventory_db': 'data/inventory.db', 'disable_penny': 'False'}
|
||||||
config.write(open(config_file, 'w'))
|
config.write(open(config_file, 'w'))
|
||||||
|
|
||||||
|
if 'location' not in config:
|
||||||
|
config['location'] = {'locations_db': 'data/locations.db', 'public_location_admin_manage': 'False', 'delete_public_locations_admins_only': 'False'}
|
||||||
|
config.write(open(config_file, 'w'))
|
||||||
|
|
||||||
# interface1 settings
|
# interface1 settings
|
||||||
interface1_type = config['interface'].get('type', 'serial')
|
interface1_type = config['interface'].get('type', 'serial')
|
||||||
port1 = config['interface'].get('port', '')
|
port1 = config['interface'].get('port', '')
|
||||||
@@ -393,6 +397,11 @@ try:
|
|||||||
inventory_db = config['inventory'].get('inventory_db', 'data/inventory.db')
|
inventory_db = config['inventory'].get('inventory_db', 'data/inventory.db')
|
||||||
disable_penny = config['inventory'].getboolean('disable_penny', False)
|
disable_penny = config['inventory'].getboolean('disable_penny', False)
|
||||||
|
|
||||||
|
# location mapping
|
||||||
|
locations_db = config['location'].get('locations_db', 'data/locations.db')
|
||||||
|
public_location_admin_manage = config['location'].getboolean('public_location_admin_manage', False)
|
||||||
|
delete_public_locations_admins_only = config['location'].getboolean('delete_public_locations_admins_only', False)
|
||||||
|
|
||||||
# E-Mail Settings
|
# E-Mail Settings
|
||||||
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
|
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
|
||||||
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
|
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
|
||||||
|
|||||||
Reference in New Issue
Block a user