mmrelay 1.0.10__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mmrelay might be problematic. Click here for more details.
- mmrelay/__init__.py +1 -1
- mmrelay/db_utils.py +61 -9
- mmrelay/log_utils.py +56 -0
- mmrelay/main.py +11 -12
- mmrelay/matrix_utils.py +385 -73
- mmrelay/meshtastic_utils.py +158 -30
- mmrelay/tools/sample-docker-compose.yaml +32 -0
- mmrelay/tools/sample.env +10 -0
- mmrelay/tools/sample_config.yaml +6 -3
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/METADATA +49 -25
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/RECORD +15 -13
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/WHEEL +0 -0
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/top_level.txt +0 -0
mmrelay/__init__.py
CHANGED
mmrelay/db_utils.py
CHANGED
|
@@ -8,17 +8,57 @@ from mmrelay.log_utils import get_logger
|
|
|
8
8
|
# Global config variable that will be set from main.py
|
|
9
9
|
config = None
|
|
10
10
|
|
|
11
|
+
# Cache for database path to avoid repeated logging and path resolution
|
|
12
|
+
_cached_db_path = None
|
|
13
|
+
_db_path_logged = False
|
|
14
|
+
_cached_config_hash = None
|
|
15
|
+
|
|
11
16
|
logger = get_logger(name="db_utils")
|
|
12
17
|
|
|
13
18
|
|
|
19
|
+
def clear_db_path_cache():
|
|
20
|
+
"""Clear the cached database path to force re-resolution on next call.
|
|
21
|
+
|
|
22
|
+
This is useful for testing or if the application supports runtime
|
|
23
|
+
configuration changes.
|
|
24
|
+
"""
|
|
25
|
+
global _cached_db_path, _db_path_logged, _cached_config_hash
|
|
26
|
+
_cached_db_path = None
|
|
27
|
+
_db_path_logged = False
|
|
28
|
+
_cached_config_hash = None
|
|
29
|
+
|
|
30
|
+
|
|
14
31
|
# Get the database path
|
|
15
32
|
def get_db_path():
|
|
16
33
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
Resolve and return the file path to the SQLite database, using configuration overrides if provided.
|
|
35
|
+
|
|
36
|
+
By default, returns the path to `meshtastic.sqlite` in the standard data directory (`~/.mmrelay/data`).
|
|
37
|
+
If a custom path is specified in the configuration under `database.path` (preferred) or `db.path` (legacy),
|
|
38
|
+
that path is used instead. The resolved path is cached for subsequent calls, and the directory is created
|
|
39
|
+
if it does not exist. Cache is automatically invalidated if the relevant configuration changes.
|
|
20
40
|
"""
|
|
21
|
-
global config
|
|
41
|
+
global config, _cached_db_path, _db_path_logged, _cached_config_hash
|
|
42
|
+
|
|
43
|
+
# Create a hash of the relevant config sections to detect changes
|
|
44
|
+
current_config_hash = None
|
|
45
|
+
if config is not None:
|
|
46
|
+
# Hash only the database-related config sections
|
|
47
|
+
db_config = {
|
|
48
|
+
"database": config.get("database", {}),
|
|
49
|
+
"db": config.get("db", {}), # Legacy format
|
|
50
|
+
}
|
|
51
|
+
current_config_hash = hash(str(sorted(db_config.items())))
|
|
52
|
+
|
|
53
|
+
# Check if cache is valid (path exists and config hasn't changed)
|
|
54
|
+
if _cached_db_path is not None and current_config_hash == _cached_config_hash:
|
|
55
|
+
return _cached_db_path
|
|
56
|
+
|
|
57
|
+
# Config changed or first call - clear cache and re-resolve
|
|
58
|
+
if current_config_hash != _cached_config_hash:
|
|
59
|
+
_cached_db_path = None
|
|
60
|
+
_db_path_logged = False
|
|
61
|
+
_cached_config_hash = current_config_hash
|
|
22
62
|
|
|
23
63
|
# Check if config is available
|
|
24
64
|
if config is not None:
|
|
@@ -30,7 +70,12 @@ def get_db_path():
|
|
|
30
70
|
db_dir = os.path.dirname(custom_path)
|
|
31
71
|
if db_dir:
|
|
32
72
|
os.makedirs(db_dir, exist_ok=True)
|
|
33
|
-
|
|
73
|
+
|
|
74
|
+
# Cache the path and log only once
|
|
75
|
+
_cached_db_path = custom_path
|
|
76
|
+
if not _db_path_logged:
|
|
77
|
+
logger.info(f"Using database path from config: {custom_path}")
|
|
78
|
+
_db_path_logged = True
|
|
34
79
|
return custom_path
|
|
35
80
|
|
|
36
81
|
# Check legacy format (db section)
|
|
@@ -41,13 +86,20 @@ def get_db_path():
|
|
|
41
86
|
db_dir = os.path.dirname(custom_path)
|
|
42
87
|
if db_dir:
|
|
43
88
|
os.makedirs(db_dir, exist_ok=True)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
|
|
90
|
+
# Cache the path and log only once
|
|
91
|
+
_cached_db_path = custom_path
|
|
92
|
+
if not _db_path_logged:
|
|
93
|
+
logger.warning(
|
|
94
|
+
"Using 'db.path' configuration (legacy). 'database.path' is now the preferred format and 'db.path' will be deprecated in a future version."
|
|
95
|
+
)
|
|
96
|
+
_db_path_logged = True
|
|
47
97
|
return custom_path
|
|
48
98
|
|
|
49
99
|
# Use the standard data directory
|
|
50
|
-
|
|
100
|
+
default_path = os.path.join(get_data_dir(), "meshtastic.sqlite")
|
|
101
|
+
_cached_db_path = default_path
|
|
102
|
+
return default_path
|
|
51
103
|
|
|
52
104
|
|
|
53
105
|
# Initialize SQLite database
|
mmrelay/log_utils.py
CHANGED
|
@@ -30,6 +30,17 @@ log_file_path = None
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def get_logger(name):
|
|
33
|
+
"""
|
|
34
|
+
Create and configure a logger with console and optional file output, supporting colorized output and log rotation.
|
|
35
|
+
|
|
36
|
+
The logger's level, color usage, and file logging behavior are determined by global configuration and command line arguments. Console output uses rich formatting if enabled. File logging supports log rotation and stores logs in a configurable or default location. The log file path is stored globally if the logger name is "M<>M Relay".
|
|
37
|
+
|
|
38
|
+
Parameters:
|
|
39
|
+
name (str): The name of the logger to create.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
logging.Logger: The configured logger instance.
|
|
43
|
+
"""
|
|
33
44
|
logger = logging.getLogger(name=name)
|
|
34
45
|
|
|
35
46
|
# Default to INFO level if config is not available
|
|
@@ -134,3 +145,48 @@ def get_logger(name):
|
|
|
134
145
|
logger.addHandler(file_handler)
|
|
135
146
|
|
|
136
147
|
return logger
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def setup_upstream_logging_capture():
|
|
151
|
+
"""
|
|
152
|
+
Redirects warning and error log messages from upstream libraries and the root logger into the application's formatted logging system.
|
|
153
|
+
|
|
154
|
+
This ensures that log output from external dependencies (such as "meshtastic", "bleak", and "asyncio") appears with consistent formatting alongside the application's own logs. Only messages at WARNING level or higher are captured, and messages originating from the application's own loggers are excluded to prevent recursion.
|
|
155
|
+
"""
|
|
156
|
+
# Get our main logger
|
|
157
|
+
main_logger = get_logger("Upstream")
|
|
158
|
+
|
|
159
|
+
# Create a custom handler that redirects root logger messages
|
|
160
|
+
class UpstreamLogHandler(logging.Handler):
|
|
161
|
+
def emit(self, record):
|
|
162
|
+
# Skip if this is already from our logger to avoid recursion
|
|
163
|
+
"""
|
|
164
|
+
Redirects log records from external sources to the main logger, mapping their severity and prefixing with the original logger name.
|
|
165
|
+
|
|
166
|
+
Skips records originating from the application's own loggers to prevent recursion.
|
|
167
|
+
"""
|
|
168
|
+
if record.name.startswith("mmrelay") or record.name == "Upstream":
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Map the log level and emit through our logger
|
|
172
|
+
if record.levelno >= logging.ERROR:
|
|
173
|
+
main_logger.error(f"[{record.name}] {record.getMessage()}")
|
|
174
|
+
elif record.levelno >= logging.WARNING:
|
|
175
|
+
main_logger.warning(f"[{record.name}] {record.getMessage()}")
|
|
176
|
+
elif record.levelno >= logging.INFO:
|
|
177
|
+
main_logger.info(f"[{record.name}] {record.getMessage()}")
|
|
178
|
+
else:
|
|
179
|
+
main_logger.debug(f"[{record.name}] {record.getMessage()}")
|
|
180
|
+
|
|
181
|
+
# Add our handler to the root logger
|
|
182
|
+
root_logger = logging.getLogger()
|
|
183
|
+
upstream_handler = UpstreamLogHandler()
|
|
184
|
+
upstream_handler.setLevel(logging.WARNING) # Only capture warnings and errors
|
|
185
|
+
root_logger.addHandler(upstream_handler)
|
|
186
|
+
|
|
187
|
+
# Also set up specific loggers for known upstream libraries
|
|
188
|
+
for logger_name in ["meshtastic", "bleak", "asyncio"]:
|
|
189
|
+
upstream_logger = logging.getLogger(logger_name)
|
|
190
|
+
upstream_logger.addHandler(upstream_handler)
|
|
191
|
+
upstream_logger.setLevel(logging.WARNING)
|
|
192
|
+
upstream_logger.propagate = False # Prevent duplicate messages via root logger
|
mmrelay/main.py
CHANGED
|
@@ -20,7 +20,7 @@ from mmrelay.db_utils import (
|
|
|
20
20
|
update_shortnames,
|
|
21
21
|
wipe_message_map,
|
|
22
22
|
)
|
|
23
|
-
from mmrelay.log_utils import get_logger
|
|
23
|
+
from mmrelay.log_utils import get_logger, setup_upstream_logging_capture
|
|
24
24
|
from mmrelay.matrix_utils import connect_matrix, join_matrix_room
|
|
25
25
|
from mmrelay.matrix_utils import logger as matrix_logger
|
|
26
26
|
from mmrelay.matrix_utils import on_room_member, on_room_message
|
|
@@ -50,13 +50,12 @@ def print_banner():
|
|
|
50
50
|
|
|
51
51
|
async def main(config):
|
|
52
52
|
"""
|
|
53
|
-
|
|
54
|
-
Includes logic for wiping the message_map if configured in database.msg_map.wipe_on_restart
|
|
55
|
-
or db.msg_map.wipe_on_restart (legacy format).
|
|
56
|
-
Also updates longnames and shortnames periodically as before.
|
|
53
|
+
Sets up and runs the asynchronous relay between Meshtastic and Matrix, managing connections, event handling, and graceful shutdown.
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
This function initializes the database, configures logging, loads plugins, connects to both Meshtastic and Matrix, joins specified Matrix rooms, and registers event callbacks for message and membership events. It periodically updates node names from the Meshtastic network and manages the Matrix sync loop, handling reconnections and shutdown signals. If configured, it wipes the message map on startup and shutdown.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
config: The loaded configuration dictionary containing Matrix, Meshtastic, and database settings.
|
|
60
59
|
"""
|
|
61
60
|
# Extract Matrix configuration
|
|
62
61
|
from typing import List
|
|
@@ -69,6 +68,9 @@ async def main(config):
|
|
|
69
68
|
# Initialize the SQLite database
|
|
70
69
|
initialize_database()
|
|
71
70
|
|
|
71
|
+
# Set up upstream logging capture to format library messages consistently
|
|
72
|
+
setup_upstream_logging_capture()
|
|
73
|
+
|
|
72
74
|
# Check database config for wipe_on_restart (preferred format)
|
|
73
75
|
database_config = config.get("database", {})
|
|
74
76
|
msg_map_config = database_config.get("msg_map", {})
|
|
@@ -131,11 +133,8 @@ async def main(config):
|
|
|
131
133
|
# On Windows, we can't use add_signal_handler, so we'll handle KeyboardInterrupt
|
|
132
134
|
pass
|
|
133
135
|
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
# so its while loop runs in parallel with the matrix sync loop
|
|
137
|
-
# Use "_" to avoid trunk's "assigned but unused variable" warning
|
|
138
|
-
# -------------------------------------------------------------------
|
|
136
|
+
# Start connection health monitoring using getMetadata() heartbeat
|
|
137
|
+
# This provides proactive connection detection for all interface types
|
|
139
138
|
_ = asyncio.create_task(meshtastic_utils.check_connection())
|
|
140
139
|
|
|
141
140
|
# Start the Matrix client sync loop
|