mmrelay 1.2.6__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.
- mmrelay/__init__.py +5 -0
- mmrelay/__main__.py +29 -0
- mmrelay/cli.py +2013 -0
- mmrelay/cli_utils.py +746 -0
- mmrelay/config.py +956 -0
- mmrelay/constants/__init__.py +65 -0
- mmrelay/constants/app.py +29 -0
- mmrelay/constants/config.py +78 -0
- mmrelay/constants/database.py +22 -0
- mmrelay/constants/formats.py +20 -0
- mmrelay/constants/messages.py +45 -0
- mmrelay/constants/network.py +45 -0
- mmrelay/constants/plugins.py +42 -0
- mmrelay/constants/queue.py +20 -0
- mmrelay/db_runtime.py +269 -0
- mmrelay/db_utils.py +1017 -0
- mmrelay/e2ee_utils.py +400 -0
- mmrelay/log_utils.py +274 -0
- mmrelay/main.py +439 -0
- mmrelay/matrix_utils.py +3091 -0
- mmrelay/meshtastic_utils.py +1245 -0
- mmrelay/message_queue.py +647 -0
- mmrelay/plugin_loader.py +1933 -0
- mmrelay/plugins/__init__.py +3 -0
- mmrelay/plugins/base_plugin.py +638 -0
- mmrelay/plugins/debug_plugin.py +30 -0
- mmrelay/plugins/drop_plugin.py +127 -0
- mmrelay/plugins/health_plugin.py +64 -0
- mmrelay/plugins/help_plugin.py +79 -0
- mmrelay/plugins/map_plugin.py +353 -0
- mmrelay/plugins/mesh_relay_plugin.py +222 -0
- mmrelay/plugins/nodes_plugin.py +92 -0
- mmrelay/plugins/ping_plugin.py +128 -0
- mmrelay/plugins/telemetry_plugin.py +179 -0
- mmrelay/plugins/weather_plugin.py +312 -0
- mmrelay/runtime_utils.py +35 -0
- mmrelay/setup_utils.py +828 -0
- mmrelay/tools/__init__.py +27 -0
- mmrelay/tools/mmrelay.service +19 -0
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +30 -0
- mmrelay/tools/sample-docker-compose.yaml +30 -0
- mmrelay/tools/sample.env +10 -0
- mmrelay/tools/sample_config.yaml +120 -0
- mmrelay/windows_utils.py +346 -0
- mmrelay-1.2.6.dist-info/METADATA +145 -0
- mmrelay-1.2.6.dist-info/RECORD +50 -0
- mmrelay-1.2.6.dist-info/WHEEL +5 -0
- mmrelay-1.2.6.dist-info/entry_points.txt +2 -0
- mmrelay-1.2.6.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.2.6.dist-info/top_level.txt +1 -0
mmrelay/main.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This script connects a Meshtastic mesh network to Matrix chat rooms by relaying messages between them.
|
|
3
|
+
It uses Meshtastic-python and Matrix nio client library to interface with the radio and the Matrix server respectively.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import concurrent.futures
|
|
8
|
+
import functools
|
|
9
|
+
import signal
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from nio import (
|
|
13
|
+
MegolmEvent,
|
|
14
|
+
ReactionEvent,
|
|
15
|
+
RoomMessageEmote,
|
|
16
|
+
RoomMessageNotice,
|
|
17
|
+
RoomMessageText,
|
|
18
|
+
)
|
|
19
|
+
from nio.events.room_events import RoomMemberEvent
|
|
20
|
+
|
|
21
|
+
# Import version from package
|
|
22
|
+
# Import meshtastic_utils as a module to set event_loop
|
|
23
|
+
from mmrelay import __version__, meshtastic_utils
|
|
24
|
+
from mmrelay.cli_utils import msg_suggest_check_config, msg_suggest_generate_config
|
|
25
|
+
from mmrelay.constants.app import APP_DISPLAY_NAME, WINDOWS_PLATFORM
|
|
26
|
+
from mmrelay.db_utils import (
|
|
27
|
+
initialize_database,
|
|
28
|
+
update_longnames,
|
|
29
|
+
update_shortnames,
|
|
30
|
+
wipe_message_map,
|
|
31
|
+
)
|
|
32
|
+
from mmrelay.log_utils import get_logger
|
|
33
|
+
from mmrelay.matrix_utils import (
|
|
34
|
+
connect_matrix,
|
|
35
|
+
join_matrix_room,
|
|
36
|
+
)
|
|
37
|
+
from mmrelay.matrix_utils import logger as matrix_logger
|
|
38
|
+
from mmrelay.matrix_utils import (
|
|
39
|
+
on_decryption_failure,
|
|
40
|
+
on_room_member,
|
|
41
|
+
on_room_message,
|
|
42
|
+
)
|
|
43
|
+
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
44
|
+
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
45
|
+
from mmrelay.message_queue import (
|
|
46
|
+
DEFAULT_MESSAGE_DELAY,
|
|
47
|
+
get_message_queue,
|
|
48
|
+
start_message_queue,
|
|
49
|
+
stop_message_queue,
|
|
50
|
+
)
|
|
51
|
+
from mmrelay.plugin_loader import load_plugins, shutdown_plugins
|
|
52
|
+
|
|
53
|
+
# Initialize logger
|
|
54
|
+
logger = get_logger(name=APP_DISPLAY_NAME)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Flag to track if banner has been printed
|
|
58
|
+
_banner_printed = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_banner():
|
|
62
|
+
"""
|
|
63
|
+
Log the MMRelay startup banner with version information once.
|
|
64
|
+
|
|
65
|
+
This records an informational message "Starting MMRelay version <version>" via the module logger
|
|
66
|
+
the first time it is called and sets a module-level flag to prevent subsequent prints.
|
|
67
|
+
"""
|
|
68
|
+
global _banner_printed
|
|
69
|
+
# Only print the banner once
|
|
70
|
+
if not _banner_printed:
|
|
71
|
+
logger.info(f"Starting MMRelay version {__version__}")
|
|
72
|
+
_banner_printed = True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def main(config):
|
|
76
|
+
"""
|
|
77
|
+
Coordinate the asynchronous relay loop between Meshtastic and Matrix clients.
|
|
78
|
+
|
|
79
|
+
Initializes the database and plugins, starts the message queue, connects to Meshtastic and Matrix, joins configured Matrix rooms, registers event callbacks, monitors connection health, runs the Matrix sync loop with automatic retries, and ensures an orderly shutdown of all components (including optional message map wiping on startup and shutdown).
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
config (dict): Application configuration mapping. Expected keys used by this function include:
|
|
83
|
+
- "matrix_rooms": list of room dicts with at least an "id" entry,
|
|
84
|
+
- "meshtastic": optional dict with "message_delay",
|
|
85
|
+
- "database" (preferred) or legacy "db": optional dict containing "msg_map" with "wipe_on_restart" boolean.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ConnectionError: If connecting to Matrix fails and no Matrix client can be obtained.
|
|
89
|
+
"""
|
|
90
|
+
# Extract Matrix configuration
|
|
91
|
+
from typing import List
|
|
92
|
+
|
|
93
|
+
matrix_rooms: List[dict] = config["matrix_rooms"]
|
|
94
|
+
|
|
95
|
+
loop = asyncio.get_running_loop()
|
|
96
|
+
meshtastic_utils.event_loop = loop
|
|
97
|
+
|
|
98
|
+
# Initialize the SQLite database
|
|
99
|
+
initialize_database()
|
|
100
|
+
|
|
101
|
+
# Check database config for wipe_on_restart (preferred format)
|
|
102
|
+
database_config = config.get("database", {})
|
|
103
|
+
msg_map_config = database_config.get("msg_map", {})
|
|
104
|
+
wipe_on_restart = msg_map_config.get("wipe_on_restart", False)
|
|
105
|
+
|
|
106
|
+
# If not found in database config, check legacy db config
|
|
107
|
+
if not wipe_on_restart:
|
|
108
|
+
db_config = config.get("db", {})
|
|
109
|
+
legacy_msg_map_config = db_config.get("msg_map", {})
|
|
110
|
+
legacy_wipe_on_restart = legacy_msg_map_config.get("wipe_on_restart", False)
|
|
111
|
+
|
|
112
|
+
if legacy_wipe_on_restart:
|
|
113
|
+
wipe_on_restart = legacy_wipe_on_restart
|
|
114
|
+
logger.warning(
|
|
115
|
+
"Using 'db.msg_map' configuration (legacy). 'database.msg_map' is now the preferred format and 'db.msg_map' will be deprecated in a future version."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if wipe_on_restart:
|
|
119
|
+
logger.debug("wipe_on_restart enabled. Wiping message_map now (startup).")
|
|
120
|
+
wipe_message_map()
|
|
121
|
+
|
|
122
|
+
# Load plugins early (run in executor to avoid blocking event loop with time.sleep)
|
|
123
|
+
await loop.run_in_executor(
|
|
124
|
+
None, functools.partial(load_plugins, passed_config=config)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Start message queue with configured message delay
|
|
128
|
+
message_delay = config.get("meshtastic", {}).get(
|
|
129
|
+
"message_delay", DEFAULT_MESSAGE_DELAY
|
|
130
|
+
)
|
|
131
|
+
start_message_queue(message_delay=message_delay)
|
|
132
|
+
|
|
133
|
+
# Connect to Meshtastic
|
|
134
|
+
meshtastic_utils.meshtastic_client = await asyncio.to_thread(
|
|
135
|
+
connect_meshtastic, passed_config=config
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Connect to Matrix
|
|
139
|
+
matrix_client = await connect_matrix(passed_config=config)
|
|
140
|
+
|
|
141
|
+
# Check if Matrix connection was successful
|
|
142
|
+
if matrix_client is None:
|
|
143
|
+
# The error is logged by connect_matrix, so we can just raise here.
|
|
144
|
+
raise ConnectionError(
|
|
145
|
+
"Failed to connect to Matrix. Cannot continue without Matrix client."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Join the rooms specified in the config.yaml
|
|
149
|
+
for room in matrix_rooms:
|
|
150
|
+
await join_matrix_room(matrix_client, room["id"])
|
|
151
|
+
|
|
152
|
+
# Register the message callback for Matrix
|
|
153
|
+
matrix_logger.info("Listening for inbound Matrix messages...")
|
|
154
|
+
matrix_client.add_event_callback(
|
|
155
|
+
on_room_message,
|
|
156
|
+
(RoomMessageText, RoomMessageNotice, RoomMessageEmote, ReactionEvent),
|
|
157
|
+
)
|
|
158
|
+
# Add E2EE callbacks - MegolmEvent only goes to decryption failure handler
|
|
159
|
+
# Successfully decrypted messages will be converted to RoomMessageText etc. by matrix-nio
|
|
160
|
+
matrix_client.add_event_callback(on_decryption_failure, (MegolmEvent,))
|
|
161
|
+
# Add RoomMemberEvent callback to track room-specific display name changes
|
|
162
|
+
matrix_client.add_event_callback(on_room_member, (RoomMemberEvent,))
|
|
163
|
+
|
|
164
|
+
# Set up shutdown event
|
|
165
|
+
shutdown_event = asyncio.Event()
|
|
166
|
+
|
|
167
|
+
async def shutdown():
|
|
168
|
+
"""
|
|
169
|
+
Signal the application to begin shutdown.
|
|
170
|
+
|
|
171
|
+
Set the Meshtastic shutdown flag and set the local shutdown event so any coroutines waiting on that event can start cleanup.
|
|
172
|
+
"""
|
|
173
|
+
matrix_logger.info("Shutdown signal received. Closing down...")
|
|
174
|
+
meshtastic_utils.shutting_down = True # Set the shutting_down flag
|
|
175
|
+
shutdown_event.set()
|
|
176
|
+
|
|
177
|
+
# Handle signals differently based on the platform
|
|
178
|
+
if sys.platform != WINDOWS_PLATFORM:
|
|
179
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
180
|
+
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown()))
|
|
181
|
+
else:
|
|
182
|
+
# On Windows, we can't use add_signal_handler, so we'll handle KeyboardInterrupt
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Start connection health monitoring using getMetadata() heartbeat
|
|
186
|
+
# This provides proactive connection detection for all interface types
|
|
187
|
+
_ = asyncio.create_task(meshtastic_utils.check_connection())
|
|
188
|
+
|
|
189
|
+
# Ensure message queue processor is started now that event loop is running
|
|
190
|
+
get_message_queue().ensure_processor_started()
|
|
191
|
+
|
|
192
|
+
# Start the Matrix client sync loop
|
|
193
|
+
try:
|
|
194
|
+
while not shutdown_event.is_set():
|
|
195
|
+
try:
|
|
196
|
+
if meshtastic_utils.meshtastic_client:
|
|
197
|
+
nodes_snapshot = dict(meshtastic_utils.meshtastic_client.nodes)
|
|
198
|
+
await loop.run_in_executor(
|
|
199
|
+
None,
|
|
200
|
+
update_longnames,
|
|
201
|
+
nodes_snapshot,
|
|
202
|
+
)
|
|
203
|
+
await loop.run_in_executor(
|
|
204
|
+
None,
|
|
205
|
+
update_shortnames,
|
|
206
|
+
nodes_snapshot,
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
meshtastic_logger.warning("Meshtastic client is not connected.")
|
|
210
|
+
|
|
211
|
+
matrix_logger.info("Starting Matrix sync loop...")
|
|
212
|
+
sync_task = asyncio.create_task(
|
|
213
|
+
matrix_client.sync_forever(timeout=30000)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
|
217
|
+
|
|
218
|
+
# Wait for either the matrix sync to fail, or for a shutdown
|
|
219
|
+
done, pending = await asyncio.wait(
|
|
220
|
+
[sync_task, shutdown_task],
|
|
221
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Cancel any pending tasks
|
|
225
|
+
for task in pending:
|
|
226
|
+
task.cancel()
|
|
227
|
+
try:
|
|
228
|
+
await task
|
|
229
|
+
except asyncio.CancelledError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
if shutdown_event.is_set():
|
|
233
|
+
matrix_logger.info("Shutdown event detected. Stopping sync loop...")
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Check if sync_task completed with an exception
|
|
237
|
+
if sync_task in done:
|
|
238
|
+
try:
|
|
239
|
+
# This will raise the exception if the task failed
|
|
240
|
+
sync_task.result()
|
|
241
|
+
# If we get here, sync completed normally (shouldn't happen with sync_forever)
|
|
242
|
+
matrix_logger.warning(
|
|
243
|
+
"Matrix sync_forever completed unexpectedly"
|
|
244
|
+
)
|
|
245
|
+
except Exception: # noqa: BLE001 — sync loop must keep retrying
|
|
246
|
+
# Log the exception and continue to retry
|
|
247
|
+
matrix_logger.exception("Matrix sync failed")
|
|
248
|
+
# The outer try/catch will handle the retry logic
|
|
249
|
+
|
|
250
|
+
except Exception: # noqa: BLE001 — keep loop alive for retries
|
|
251
|
+
if shutdown_event.is_set():
|
|
252
|
+
break
|
|
253
|
+
matrix_logger.exception("Error syncing with Matrix server")
|
|
254
|
+
await asyncio.sleep(5) # Wait briefly before retrying
|
|
255
|
+
except KeyboardInterrupt:
|
|
256
|
+
await shutdown()
|
|
257
|
+
finally:
|
|
258
|
+
# Cleanup
|
|
259
|
+
matrix_logger.info("Stopping plugins...")
|
|
260
|
+
await loop.run_in_executor(None, shutdown_plugins)
|
|
261
|
+
matrix_logger.info("Stopping message queue...")
|
|
262
|
+
await loop.run_in_executor(None, stop_message_queue)
|
|
263
|
+
|
|
264
|
+
matrix_logger.info("Closing Matrix client...")
|
|
265
|
+
await matrix_client.close()
|
|
266
|
+
if meshtastic_utils.meshtastic_client:
|
|
267
|
+
meshtastic_logger.info("Closing Meshtastic client...")
|
|
268
|
+
try:
|
|
269
|
+
# Timeout wrapper to prevent infinite hanging during shutdown
|
|
270
|
+
# The meshtastic library can sometimes hang indefinitely during close()
|
|
271
|
+
# operations, especially with BLE connections. This timeout ensures
|
|
272
|
+
# the application can shut down gracefully within 10 seconds.
|
|
273
|
+
|
|
274
|
+
def _close_meshtastic():
|
|
275
|
+
"""
|
|
276
|
+
Closes the Meshtastic client connection synchronously.
|
|
277
|
+
"""
|
|
278
|
+
meshtastic_utils.meshtastic_client.close()
|
|
279
|
+
|
|
280
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
281
|
+
future = executor.submit(_close_meshtastic)
|
|
282
|
+
future.result(timeout=10.0) # 10-second timeout
|
|
283
|
+
|
|
284
|
+
meshtastic_logger.info("Meshtastic client closed successfully")
|
|
285
|
+
except concurrent.futures.TimeoutError:
|
|
286
|
+
meshtastic_logger.warning(
|
|
287
|
+
"Meshtastic client close timed out - forcing shutdown"
|
|
288
|
+
)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
meshtastic_logger.error(
|
|
291
|
+
f"Unexpected error during Meshtastic client close: {e}",
|
|
292
|
+
exc_info=True,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Attempt to wipe message_map on shutdown if enabled
|
|
296
|
+
if wipe_on_restart:
|
|
297
|
+
logger.debug("wipe_on_restart enabled. Wiping message_map now (shutdown).")
|
|
298
|
+
wipe_message_map()
|
|
299
|
+
|
|
300
|
+
# Cancel the reconnect task if it exists
|
|
301
|
+
if meshtastic_utils.reconnect_task:
|
|
302
|
+
meshtastic_utils.reconnect_task.cancel()
|
|
303
|
+
meshtastic_logger.info("Cancelled Meshtastic reconnect task.")
|
|
304
|
+
|
|
305
|
+
# Cancel any remaining tasks (including the check_conn_task)
|
|
306
|
+
current_task = asyncio.current_task()
|
|
307
|
+
pending_tasks = [
|
|
308
|
+
task
|
|
309
|
+
for task in asyncio.all_tasks(loop)
|
|
310
|
+
if task is not current_task and not task.done()
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
for task in pending_tasks:
|
|
314
|
+
task.cancel()
|
|
315
|
+
|
|
316
|
+
if pending_tasks:
|
|
317
|
+
await asyncio.gather(*pending_tasks, return_exceptions=True)
|
|
318
|
+
|
|
319
|
+
matrix_logger.info("Shutdown complete.")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def run_main(args):
|
|
323
|
+
"""
|
|
324
|
+
Start the application: load configuration, validate required keys, and run the main async runner.
|
|
325
|
+
|
|
326
|
+
Loads and applies configuration (optionally overriding logging level from args), initializes module configuration, verifies required configuration sections (required keys are ["meshtastic", "matrix_rooms"] when credentials.json is present, otherwise ["matrix", "meshtastic", "matrix_rooms"]), and executes the main async entrypoint. Returns process exit codes: 0 for successful completion or user interrupt, 1 for configuration errors or unhandled exceptions.
|
|
327
|
+
|
|
328
|
+
Parameters:
|
|
329
|
+
args: Parsed command-line arguments (may be None). Recognized option used here: `log_level` to override the configured logging level.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
int: Exit code (0 on success or user-initiated interrupt, 1 on failure such as invalid config or runtime error).
|
|
333
|
+
"""
|
|
334
|
+
# Print the banner at startup
|
|
335
|
+
print_banner()
|
|
336
|
+
|
|
337
|
+
# Load configuration
|
|
338
|
+
from mmrelay.config import load_config
|
|
339
|
+
|
|
340
|
+
# Load configuration with args
|
|
341
|
+
config = load_config(args=args)
|
|
342
|
+
|
|
343
|
+
# Handle the --log-level option
|
|
344
|
+
if args and args.log_level:
|
|
345
|
+
# Override the log level from config
|
|
346
|
+
if "logging" not in config:
|
|
347
|
+
config["logging"] = {}
|
|
348
|
+
config["logging"]["level"] = args.log_level
|
|
349
|
+
|
|
350
|
+
# Set the global config variables in each module
|
|
351
|
+
from mmrelay import (
|
|
352
|
+
db_utils,
|
|
353
|
+
log_utils,
|
|
354
|
+
matrix_utils,
|
|
355
|
+
meshtastic_utils,
|
|
356
|
+
plugin_loader,
|
|
357
|
+
)
|
|
358
|
+
from mmrelay.config import set_config
|
|
359
|
+
from mmrelay.plugins import base_plugin
|
|
360
|
+
|
|
361
|
+
# Use the centralized set_config function to set up the configuration for all modules
|
|
362
|
+
set_config(matrix_utils, config)
|
|
363
|
+
set_config(meshtastic_utils, config)
|
|
364
|
+
set_config(plugin_loader, config)
|
|
365
|
+
set_config(log_utils, config)
|
|
366
|
+
set_config(db_utils, config)
|
|
367
|
+
set_config(base_plugin, config)
|
|
368
|
+
|
|
369
|
+
# Configure component debug logging now that config is available
|
|
370
|
+
log_utils.configure_component_debug_logging()
|
|
371
|
+
|
|
372
|
+
# Get config path and log file path for logging
|
|
373
|
+
from mmrelay.config import config_path
|
|
374
|
+
from mmrelay.log_utils import log_file_path
|
|
375
|
+
|
|
376
|
+
# Create a logger with a different name to avoid conflicts with the one in config.py
|
|
377
|
+
config_rich_logger = get_logger("ConfigInfo")
|
|
378
|
+
|
|
379
|
+
# Now log the config file and log file locations with the properly formatted logger
|
|
380
|
+
if config_path:
|
|
381
|
+
config_rich_logger.info(f"Config file location: {config_path}")
|
|
382
|
+
if log_file_path:
|
|
383
|
+
config_rich_logger.info(f"Log file location: {log_file_path}")
|
|
384
|
+
|
|
385
|
+
# Check if config exists and has the required keys
|
|
386
|
+
# Note: matrix section is optional if credentials.json exists
|
|
387
|
+
from mmrelay.config import load_credentials
|
|
388
|
+
|
|
389
|
+
credentials = load_credentials()
|
|
390
|
+
|
|
391
|
+
if credentials:
|
|
392
|
+
# With credentials.json, only meshtastic and matrix_rooms are required
|
|
393
|
+
required_keys = ["meshtastic", "matrix_rooms"]
|
|
394
|
+
else:
|
|
395
|
+
# Without credentials.json, all sections are required
|
|
396
|
+
required_keys = ["matrix", "meshtastic", "matrix_rooms"]
|
|
397
|
+
|
|
398
|
+
# Check each key individually for better debugging
|
|
399
|
+
for key in required_keys:
|
|
400
|
+
if key not in config:
|
|
401
|
+
logger.error(f"Required key '{key}' is missing from config")
|
|
402
|
+
|
|
403
|
+
if not config or not all(key in config for key in required_keys):
|
|
404
|
+
# Exit with error if no config exists
|
|
405
|
+
missing_keys = [key for key in required_keys if key not in config]
|
|
406
|
+
if credentials:
|
|
407
|
+
logger.error(f"Configuration is missing required keys: {missing_keys}")
|
|
408
|
+
logger.error("Matrix authentication will use credentials.json")
|
|
409
|
+
logger.error("Next steps:")
|
|
410
|
+
logger.error(
|
|
411
|
+
f" • Create a valid config.yaml file or {msg_suggest_generate_config()}"
|
|
412
|
+
)
|
|
413
|
+
logger.error(f" • {msg_suggest_check_config()}")
|
|
414
|
+
else:
|
|
415
|
+
logger.error(f"Configuration is missing required keys: {missing_keys}")
|
|
416
|
+
logger.error("Next steps:")
|
|
417
|
+
logger.error(
|
|
418
|
+
f" • Create a valid config.yaml file or {msg_suggest_generate_config()}"
|
|
419
|
+
)
|
|
420
|
+
logger.error(f" • {msg_suggest_check_config()}")
|
|
421
|
+
return 1
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
asyncio.run(main(config))
|
|
425
|
+
return 0
|
|
426
|
+
except KeyboardInterrupt:
|
|
427
|
+
logger.info("Interrupted by user. Exiting.")
|
|
428
|
+
return 0
|
|
429
|
+
except Exception: # noqa: BLE001 — top-level guard to log and exit cleanly
|
|
430
|
+
logger.exception("Error running main functionality")
|
|
431
|
+
return 1
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
if __name__ == "__main__":
|
|
435
|
+
import sys
|
|
436
|
+
|
|
437
|
+
from mmrelay.cli import main
|
|
438
|
+
|
|
439
|
+
sys.exit(main())
|