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 CHANGED
@@ -14,4 +14,4 @@ else:
14
14
  __version__ = version("mmrelay")
15
15
  except PackageNotFoundError:
16
16
  # If all else fails, use hardcoded version
17
- __version__ = "1.0.10"
17
+ __version__ = "1.1.0"
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
- Returns the path to the SQLite database file.
18
- By default, uses the standard data directory (~/.mmrelay/data).
19
- Can be overridden by setting 'path' under 'database' in config.yaml.
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
- logger.info(f"Using database path from config: {custom_path}")
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
- logger.warning(
45
- "Using 'db.path' configuration (legacy). 'database.path' is now the preferred format and 'db.path' will be deprecated in a future version."
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
- return os.path.join(get_data_dir(), "meshtastic.sqlite")
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
- Main asynchronous function to set up and run the relay.
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
- Args:
59
- config: The loaded configuration
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
- # IMPORTANT: We create a task to run the meshtastic_utils.check_connection()
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