mmrelay 1.1.3__py3-none-any.whl → 1.2.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/cli.py +1097 -110
- mmrelay/cli_utils.py +696 -0
- mmrelay/config.py +632 -44
- mmrelay/constants/__init__.py +54 -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 +42 -0
- mmrelay/constants/queue.py +17 -0
- mmrelay/db_utils.py +281 -132
- mmrelay/e2ee_utils.py +392 -0
- mmrelay/log_utils.py +77 -19
- mmrelay/main.py +101 -30
- mmrelay/matrix_utils.py +1083 -118
- mmrelay/meshtastic_utils.py +374 -118
- mmrelay/message_queue.py +17 -17
- mmrelay/plugin_loader.py +126 -91
- mmrelay/plugins/base_plugin.py +74 -15
- mmrelay/plugins/drop_plugin.py +13 -5
- mmrelay/plugins/mesh_relay_plugin.py +7 -10
- mmrelay/plugins/weather_plugin.py +118 -12
- mmrelay/setup_utils.py +67 -30
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +80 -0
- mmrelay/tools/sample-docker-compose.yaml +34 -8
- mmrelay/tools/sample_config.yaml +29 -4
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/METADATA +21 -50
- mmrelay-1.2.0.dist-info/RECORD +45 -0
- mmrelay/config_checker.py +0 -133
- mmrelay-1.1.3.dist-info/RECORD +0 -35
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
|
|
3
4
|
import requests
|
|
4
5
|
from meshtastic.mesh_interface import BROADCAST_NUM
|
|
5
6
|
|
|
7
|
+
from mmrelay.constants.formats import TEXT_MESSAGE_APP
|
|
6
8
|
from mmrelay.plugins.base_plugin import BasePlugin
|
|
7
9
|
|
|
8
10
|
|
|
@@ -17,41 +19,114 @@ class Plugin(BasePlugin):
|
|
|
17
19
|
return "Show weather forecast for a radio node using GPS location"
|
|
18
20
|
|
|
19
21
|
def generate_forecast(self, latitude, longitude):
|
|
22
|
+
"""
|
|
23
|
+
Generate a concise one-line weather forecast for the given GPS coordinates.
|
|
24
|
+
|
|
25
|
+
Builds and queries the Open-Meteo API for current conditions and hour-aligned forecasts ~+2h and ~+5h, formats temperatures according to the plugin configuration (`self.config["units"]`, default "metric"), and returns a single-line summary including current conditions and the two forecast points.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
latitude (float): Latitude in decimal degrees.
|
|
29
|
+
longitude (float): Longitude in decimal degrees.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str: A single-line forecast such as
|
|
33
|
+
"Now: ☀️ Clear sky - 12.3°C | +2h: 🌧️ Light rain - 13.1°C 20% | +5h: ⛅️ Partly cloudy - 10.8°C 5%".
|
|
34
|
+
On recoverable failures returns a short error message: "Weather data temporarily unavailable.",
|
|
35
|
+
"Error fetching weather data.", or "Error parsing weather data.".
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
- Temperature units are determined by `self.config.get("units", "metric")` ("metric" -> °C, "imperial" -> °F).
|
|
39
|
+
- The function attempts to anchor forecasts to hourly timestamps when available; if timestamps cannot be matched it falls back to hour-of-day indexing (may be less accurate).
|
|
40
|
+
- Network/HTTP errors and request-related exceptions are handled and result in the "Error fetching weather data." message.
|
|
41
|
+
- Malformed or incomplete API responses result in "Error parsing weather data." Unexpected exceptions are re-raised.
|
|
42
|
+
"""
|
|
20
43
|
units = self.config.get("units", "metric") # Default to metric
|
|
21
44
|
temperature_unit = "°C" if units == "metric" else "°F"
|
|
22
45
|
|
|
23
46
|
url = (
|
|
24
47
|
f"https://api.open-meteo.com/v1/forecast?"
|
|
25
48
|
f"latitude={latitude}&longitude={longitude}&"
|
|
26
|
-
f"hourly=temperature_2m,precipitation_probability,weathercode,
|
|
27
|
-
f"forecast_days=
|
|
49
|
+
f"hourly=temperature_2m,precipitation_probability,weathercode,is_day&"
|
|
50
|
+
f"forecast_days=2&timezone=auto¤t_weather=true"
|
|
28
51
|
)
|
|
29
52
|
|
|
30
53
|
try:
|
|
31
54
|
response = requests.get(url, timeout=10)
|
|
55
|
+
response.raise_for_status()
|
|
32
56
|
data = response.json()
|
|
33
57
|
|
|
34
58
|
# Extract relevant weather data
|
|
35
59
|
current_temp = data["current_weather"]["temperature"]
|
|
36
60
|
current_weather_code = data["current_weather"]["weathercode"]
|
|
37
61
|
is_day = data["current_weather"]["is_day"]
|
|
62
|
+
current_time_str = data["current_weather"]["time"]
|
|
63
|
+
|
|
64
|
+
# Parse current time to get the hour with defensive handling
|
|
65
|
+
current_hour = 0
|
|
66
|
+
current_time = None
|
|
67
|
+
try:
|
|
68
|
+
current_time = datetime.fromisoformat(
|
|
69
|
+
current_time_str.replace("Z", "+00:00")
|
|
70
|
+
)
|
|
71
|
+
current_hour = current_time.hour
|
|
72
|
+
except ValueError as ex:
|
|
73
|
+
self.logger.warning(
|
|
74
|
+
f"Unexpected current_weather.time '{current_time_str}': {ex}. Defaulting to hour=0."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Calculate indices for +2h and +5h forecasts
|
|
78
|
+
# Try to anchor to hourly timestamps for robustness, fall back to hour-of-day
|
|
79
|
+
base_index = current_hour
|
|
80
|
+
hourly_times = data["hourly"].get("time", [])
|
|
81
|
+
if hourly_times and current_time:
|
|
82
|
+
try:
|
|
83
|
+
# Normalize current time to the hour and find it in hourly timestamps
|
|
84
|
+
base_key = current_time.replace(
|
|
85
|
+
minute=0, second=0, microsecond=0
|
|
86
|
+
).strftime("%Y-%m-%dT%H:00")
|
|
87
|
+
base_index = hourly_times.index(base_key)
|
|
88
|
+
except (ValueError, AttributeError):
|
|
89
|
+
# Fall back to hour-of-day if hourly timestamps are unavailable/mismatched
|
|
90
|
+
self.logger.warning(
|
|
91
|
+
"Could not find current time in hourly timestamps. "
|
|
92
|
+
"Falling back to hour-of-day indexing, which may be inaccurate."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
forecast_2h_index = base_index + 2
|
|
96
|
+
forecast_5h_index = base_index + 5
|
|
38
97
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
98
|
+
# Guard against empty hourly series before clamping
|
|
99
|
+
temps = data["hourly"].get("temperature_2m") or []
|
|
100
|
+
if not temps:
|
|
101
|
+
self.logger.warning("No hourly temperature data returned.")
|
|
102
|
+
return "Weather data temporarily unavailable."
|
|
103
|
+
max_index = len(temps) - 1
|
|
104
|
+
forecast_2h_index = min(forecast_2h_index, max_index)
|
|
105
|
+
forecast_5h_index = min(forecast_5h_index, max_index)
|
|
43
106
|
|
|
44
107
|
forecast_2h_temp = data["hourly"]["temperature_2m"][forecast_2h_index]
|
|
45
108
|
forecast_2h_precipitation = data["hourly"]["precipitation_probability"][
|
|
46
109
|
forecast_2h_index
|
|
47
110
|
]
|
|
48
111
|
forecast_2h_weather_code = data["hourly"]["weathercode"][forecast_2h_index]
|
|
112
|
+
# Get hour-specific day/night flag for +2h forecast
|
|
113
|
+
forecast_2h_is_day = (
|
|
114
|
+
data["hourly"]["is_day"][forecast_2h_index]
|
|
115
|
+
if data["hourly"].get("is_day")
|
|
116
|
+
else is_day
|
|
117
|
+
)
|
|
49
118
|
|
|
50
119
|
forecast_5h_temp = data["hourly"]["temperature_2m"][forecast_5h_index]
|
|
51
120
|
forecast_5h_precipitation = data["hourly"]["precipitation_probability"][
|
|
52
121
|
forecast_5h_index
|
|
53
122
|
]
|
|
54
123
|
forecast_5h_weather_code = data["hourly"]["weathercode"][forecast_5h_index]
|
|
124
|
+
# Get hour-specific day/night flag for +5h forecast
|
|
125
|
+
forecast_5h_is_day = (
|
|
126
|
+
data["hourly"]["is_day"][forecast_5h_index]
|
|
127
|
+
if data["hourly"].get("is_day")
|
|
128
|
+
else is_day
|
|
129
|
+
)
|
|
55
130
|
|
|
56
131
|
if units == "imperial":
|
|
57
132
|
# Convert temperatures from Celsius to Fahrenheit
|
|
@@ -105,27 +180,58 @@ class Plugin(BasePlugin):
|
|
|
105
180
|
f"{current_temp}{temperature_unit} | "
|
|
106
181
|
)
|
|
107
182
|
forecast += (
|
|
108
|
-
f"+2h: {weather_code_to_text(forecast_2h_weather_code,
|
|
183
|
+
f"+2h: {weather_code_to_text(forecast_2h_weather_code, forecast_2h_is_day)} - "
|
|
109
184
|
f"{forecast_2h_temp}{temperature_unit} {forecast_2h_precipitation}% | "
|
|
110
185
|
)
|
|
111
186
|
forecast += (
|
|
112
|
-
f"+5h: {weather_code_to_text(forecast_5h_weather_code,
|
|
187
|
+
f"+5h: {weather_code_to_text(forecast_5h_weather_code, forecast_5h_is_day)} - "
|
|
113
188
|
f"{forecast_5h_temp}{temperature_unit} {forecast_5h_precipitation}%"
|
|
114
189
|
)
|
|
115
190
|
|
|
116
191
|
return forecast
|
|
117
192
|
|
|
118
|
-
except
|
|
119
|
-
|
|
120
|
-
|
|
193
|
+
except Exception as e:
|
|
194
|
+
# Handle HTTP/network errors from requests
|
|
195
|
+
# Handle requests-related exceptions using safe attribute checking
|
|
196
|
+
try:
|
|
197
|
+
# Check if this is a requests exception by checking the module
|
|
198
|
+
if hasattr(requests, "RequestException") and isinstance(
|
|
199
|
+
e, requests.RequestException
|
|
200
|
+
):
|
|
201
|
+
self.logger.error(f"Error fetching weather data: {e}")
|
|
202
|
+
return "Error fetching weather data."
|
|
203
|
+
except (AttributeError, TypeError):
|
|
204
|
+
# Fallback to string-based detection if isinstance fails
|
|
205
|
+
exception_module = getattr(type(e), "__module__", "")
|
|
206
|
+
if "requests" in exception_module:
|
|
207
|
+
self.logger.error(f"Error fetching weather data: {e}")
|
|
208
|
+
return "Error fetching weather data."
|
|
209
|
+
|
|
210
|
+
# Handle data parsing errors
|
|
211
|
+
if isinstance(
|
|
212
|
+
e, (KeyError, IndexError, TypeError, ValueError, AttributeError)
|
|
213
|
+
):
|
|
214
|
+
self.logger.error(f"Malformed weather data: {e}")
|
|
215
|
+
return "Error parsing weather data."
|
|
216
|
+
else:
|
|
217
|
+
# Re-raise unexpected exceptions
|
|
218
|
+
raise
|
|
121
219
|
|
|
122
220
|
async def handle_meshtastic_message(
|
|
123
221
|
self, packet, formatted_message, longname, meshnet_name
|
|
124
222
|
):
|
|
223
|
+
"""
|
|
224
|
+
Processes incoming Meshtastic text messages and responds with a weather forecast if the plugin command is detected.
|
|
225
|
+
|
|
226
|
+
Checks if the message is a valid text message on the expected port, verifies channel and command enablement, retrieves the sender's GPS location, generates a weather forecast, and sends the response either as a direct message or broadcast depending on the message type.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
bool: True if the message was handled and a response was sent; False otherwise.
|
|
230
|
+
"""
|
|
125
231
|
if (
|
|
126
232
|
"decoded" in packet
|
|
127
233
|
and "portnum" in packet["decoded"]
|
|
128
|
-
and packet["decoded"]["portnum"] ==
|
|
234
|
+
and packet["decoded"]["portnum"] == TEXT_MESSAGE_APP
|
|
129
235
|
and "text" in packet["decoded"]
|
|
130
236
|
):
|
|
131
237
|
message = packet["decoded"]["text"].strip()
|
mmrelay/setup_utils.py
CHANGED
|
@@ -14,6 +14,7 @@ import subprocess
|
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
from mmrelay.constants.database import PROGRESS_COMPLETE, PROGRESS_TOTAL_STEPS
|
|
17
18
|
from mmrelay.tools import get_service_template_path
|
|
18
19
|
|
|
19
20
|
|
|
@@ -54,7 +55,11 @@ def print_service_commands():
|
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
def wait_for_service_start():
|
|
57
|
-
"""
|
|
58
|
+
"""
|
|
59
|
+
Displays a progress spinner while waiting up to 10 seconds for the mmrelay service to become active.
|
|
60
|
+
|
|
61
|
+
The function checks the service status after 5 seconds and completes early if the service is detected as active.
|
|
62
|
+
"""
|
|
58
63
|
import time
|
|
59
64
|
|
|
60
65
|
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
@@ -67,7 +72,7 @@ def wait_for_service_start():
|
|
|
67
72
|
transient=True,
|
|
68
73
|
) as progress:
|
|
69
74
|
# Add a task that will run for approximately 10 seconds
|
|
70
|
-
task = progress.add_task("Starting", total=
|
|
75
|
+
task = progress.add_task("Starting", total=PROGRESS_TOTAL_STEPS)
|
|
71
76
|
|
|
72
77
|
# Update progress over 10 seconds
|
|
73
78
|
for i in range(10):
|
|
@@ -76,7 +81,7 @@ def wait_for_service_start():
|
|
|
76
81
|
|
|
77
82
|
# Check if service is active after 5 seconds to potentially finish early
|
|
78
83
|
if i >= 5 and is_service_active():
|
|
79
|
-
progress.update(task, completed=
|
|
84
|
+
progress.update(task, completed=PROGRESS_COMPLETE)
|
|
80
85
|
break
|
|
81
86
|
|
|
82
87
|
|
|
@@ -372,10 +377,11 @@ def check_loginctl_available():
|
|
|
372
377
|
|
|
373
378
|
|
|
374
379
|
def check_lingering_enabled():
|
|
375
|
-
"""
|
|
380
|
+
"""
|
|
381
|
+
Determine whether user lingering is enabled for the current user.
|
|
376
382
|
|
|
377
383
|
Returns:
|
|
378
|
-
bool: True if lingering is enabled, False otherwise.
|
|
384
|
+
bool: True if user lingering is enabled, False otherwise.
|
|
379
385
|
"""
|
|
380
386
|
try:
|
|
381
387
|
username = os.environ.get("USER", os.environ.get("USERNAME"))
|
|
@@ -386,7 +392,8 @@ def check_lingering_enabled():
|
|
|
386
392
|
text=True,
|
|
387
393
|
)
|
|
388
394
|
return result.returncode == 0 and "Linger=yes" in result.stdout
|
|
389
|
-
except Exception:
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print(f"Error checking lingering status: {e}")
|
|
390
397
|
return False
|
|
391
398
|
|
|
392
399
|
|
|
@@ -417,7 +424,14 @@ def enable_lingering():
|
|
|
417
424
|
|
|
418
425
|
|
|
419
426
|
def install_service():
|
|
420
|
-
"""
|
|
427
|
+
"""
|
|
428
|
+
Install or update the MMRelay systemd user service, guiding the user through creation, updating, enabling, and starting the service as needed.
|
|
429
|
+
|
|
430
|
+
Prompts the user for confirmation before updating an existing service file, enabling user lingering, enabling the service to start at boot, and starting or restarting the service. Handles user interruptions gracefully and prints a summary of the service status and management commands upon completion.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
bool: True if the installation or update process completes successfully, False otherwise.
|
|
434
|
+
"""
|
|
421
435
|
# Check if service already exists
|
|
422
436
|
existing_service = read_service_file()
|
|
423
437
|
service_path = get_user_service_path()
|
|
@@ -431,11 +445,14 @@ def install_service():
|
|
|
431
445
|
|
|
432
446
|
if update_needed:
|
|
433
447
|
print(f"The service file needs to be updated: {reason}")
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
.lower()
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
try:
|
|
449
|
+
user_input = input("Do you want to update the service file? (y/n): ")
|
|
450
|
+
if not user_input.lower().startswith("y"):
|
|
451
|
+
print("Service update cancelled.")
|
|
452
|
+
print_service_commands()
|
|
453
|
+
return True
|
|
454
|
+
except (EOFError, KeyboardInterrupt):
|
|
455
|
+
print("\nInput cancelled. Proceeding with default behavior.")
|
|
439
456
|
print("Service update cancelled.")
|
|
440
457
|
print_service_commands()
|
|
441
458
|
return True
|
|
@@ -450,9 +467,11 @@ def install_service():
|
|
|
450
467
|
if not create_service_file():
|
|
451
468
|
return False
|
|
452
469
|
|
|
453
|
-
# Reload daemon
|
|
470
|
+
# Reload daemon (continue even if this fails)
|
|
454
471
|
if not reload_daemon():
|
|
455
|
-
|
|
472
|
+
print(
|
|
473
|
+
"Warning: Failed to reload systemd daemon. You may need to run 'systemctl --user daemon-reload' manually."
|
|
474
|
+
)
|
|
456
475
|
|
|
457
476
|
if existing_service:
|
|
458
477
|
print("Service file updated successfully")
|
|
@@ -473,13 +492,16 @@ def install_service():
|
|
|
473
492
|
print(
|
|
474
493
|
"Lingering allows user services to run even when you're not logged in."
|
|
475
494
|
)
|
|
476
|
-
|
|
477
|
-
input(
|
|
495
|
+
try:
|
|
496
|
+
user_input = input(
|
|
478
497
|
"Do you want to enable lingering for your user? (requires sudo) (y/n): "
|
|
479
498
|
)
|
|
480
|
-
.lower()
|
|
481
|
-
|
|
482
|
-
|
|
499
|
+
should_enable_lingering = user_input.lower().startswith("y")
|
|
500
|
+
except (EOFError, KeyboardInterrupt):
|
|
501
|
+
print("\nInput cancelled. Skipping lingering setup.")
|
|
502
|
+
should_enable_lingering = False
|
|
503
|
+
|
|
504
|
+
if should_enable_lingering:
|
|
483
505
|
enable_lingering()
|
|
484
506
|
|
|
485
507
|
# Check if the service is already enabled
|
|
@@ -488,11 +510,16 @@ def install_service():
|
|
|
488
510
|
print("The service is already enabled to start at boot.")
|
|
489
511
|
else:
|
|
490
512
|
print("The service is not currently enabled to start at boot.")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
513
|
+
try:
|
|
514
|
+
user_input = input(
|
|
515
|
+
"Do you want to enable the service to start at boot? (y/n): "
|
|
516
|
+
)
|
|
517
|
+
enable_service = user_input.lower().startswith("y")
|
|
518
|
+
except (EOFError, KeyboardInterrupt):
|
|
519
|
+
print("\nInput cancelled. Skipping service enable.")
|
|
520
|
+
enable_service = False
|
|
521
|
+
|
|
522
|
+
if enable_service:
|
|
496
523
|
try:
|
|
497
524
|
subprocess.run(
|
|
498
525
|
["/usr/bin/systemctl", "--user", "enable", "mmrelay.service"],
|
|
@@ -509,7 +536,14 @@ def install_service():
|
|
|
509
536
|
service_active = is_service_active()
|
|
510
537
|
if service_active:
|
|
511
538
|
print("The service is already running.")
|
|
512
|
-
|
|
539
|
+
try:
|
|
540
|
+
user_input = input("Do you want to restart the service? (y/n): ")
|
|
541
|
+
restart_service = user_input.lower().startswith("y")
|
|
542
|
+
except (EOFError, KeyboardInterrupt):
|
|
543
|
+
print("\nInput cancelled. Skipping service restart.")
|
|
544
|
+
restart_service = False
|
|
545
|
+
|
|
546
|
+
if restart_service:
|
|
513
547
|
try:
|
|
514
548
|
subprocess.run(
|
|
515
549
|
["/usr/bin/systemctl", "--user", "restart", "mmrelay.service"],
|
|
@@ -526,11 +560,14 @@ def install_service():
|
|
|
526
560
|
print(f"Error: {e}")
|
|
527
561
|
else:
|
|
528
562
|
print("The service is not currently running.")
|
|
529
|
-
|
|
530
|
-
input("Do you want to start the service now? (y/n): ")
|
|
531
|
-
.lower()
|
|
532
|
-
|
|
533
|
-
|
|
563
|
+
try:
|
|
564
|
+
user_input = input("Do you want to start the service now? (y/n): ")
|
|
565
|
+
start_now = user_input.lower().startswith("y")
|
|
566
|
+
except (EOFError, KeyboardInterrupt):
|
|
567
|
+
print("\nInput cancelled. Skipping service start.")
|
|
568
|
+
start_now = False
|
|
569
|
+
|
|
570
|
+
if start_now:
|
|
534
571
|
if start_service():
|
|
535
572
|
# Wait for the service to start
|
|
536
573
|
wait_for_service_start()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
services:
|
|
2
|
+
mmrelay:
|
|
3
|
+
image: ghcr.io/jeremiah-k/mmrelay:latest
|
|
4
|
+
container_name: meshtastic-matrix-relay
|
|
5
|
+
restart: unless-stopped
|
|
6
|
+
user: "${UID:-1000}:${GID:-1000}"
|
|
7
|
+
|
|
8
|
+
environment:
|
|
9
|
+
- TZ=UTC
|
|
10
|
+
- PYTHONUNBUFFERED=1
|
|
11
|
+
- MPLCONFIGDIR=/tmp/matplotlib
|
|
12
|
+
|
|
13
|
+
# Matrix Authentication: Use 'mmrelay auth login' on host system to create credentials.json
|
|
14
|
+
# The credentials file will be automatically loaded from the volume mount below
|
|
15
|
+
|
|
16
|
+
# Meshtastic Connection Settings - Uncomment and configure as needed
|
|
17
|
+
# TCP Connection (most common)
|
|
18
|
+
# - MMRELAY_MESHTASTIC_CONNECTION_TYPE=tcp
|
|
19
|
+
# - MMRELAY_MESHTASTIC_HOST=192.168.1.100
|
|
20
|
+
# - MMRELAY_MESHTASTIC_PORT=4403 # Default port
|
|
21
|
+
|
|
22
|
+
# Serial Connection (uncomment for serial)
|
|
23
|
+
# - MMRELAY_MESHTASTIC_CONNECTION_TYPE=serial
|
|
24
|
+
# - MMRELAY_MESHTASTIC_SERIAL_PORT=/dev/ttyUSB0
|
|
25
|
+
|
|
26
|
+
# BLE Connection (uncomment for Bluetooth)
|
|
27
|
+
# - MMRELAY_MESHTASTIC_CONNECTION_TYPE=ble
|
|
28
|
+
# - MMRELAY_MESHTASTIC_BLE_ADDRESS=AA:BB:CC:DD:EE:FF
|
|
29
|
+
|
|
30
|
+
# Meshtastic Operational Settings
|
|
31
|
+
# - MMRELAY_MESHTASTIC_BROADCAST_ENABLED=true
|
|
32
|
+
# - MMRELAY_MESHTASTIC_MESHNET_NAME=My Mesh Network
|
|
33
|
+
# - MMRELAY_MESHTASTIC_MESSAGE_DELAY=2.2 # Minimum 2.0 seconds
|
|
34
|
+
|
|
35
|
+
# System Configuration
|
|
36
|
+
# - MMRELAY_LOGGING_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
37
|
+
# - MMRELAY_LOG_FILE=/app/data/logs/mmrelay.log # Enables file logging
|
|
38
|
+
# - MMRELAY_DATABASE_PATH=/app/data/meshtastic.sqlite
|
|
39
|
+
|
|
40
|
+
# Note: Environment variables for Meshtastic, logging, and database settings take precedence over config.yaml
|
|
41
|
+
|
|
42
|
+
volumes:
|
|
43
|
+
# Map entire ~/.mmrelay directory to /app/data to maintain proper structure
|
|
44
|
+
# This includes config.yaml, plugins/, data/, logs/, credentials.json, and any other subdirectories
|
|
45
|
+
# The --data-dir parameter in CMD points to /app/data
|
|
46
|
+
# Create config.yaml first - see docs/DOCKER.md for setup instructions
|
|
47
|
+
- ${MMRELAY_HOME}/.mmrelay:/app/data
|
|
48
|
+
|
|
49
|
+
# For TCP connections (most common) - Meshtastic typically uses port 4403
|
|
50
|
+
ports:
|
|
51
|
+
- 4403:4403
|
|
52
|
+
|
|
53
|
+
# For serial connections, uncomment the device you need:
|
|
54
|
+
# devices:
|
|
55
|
+
# - /dev/ttyUSB0:/dev/ttyUSB0
|
|
56
|
+
# - /dev/ttyACM0:/dev/ttyACM0
|
|
57
|
+
|
|
58
|
+
# For BLE connections, uncomment these:
|
|
59
|
+
# privileged: true
|
|
60
|
+
# network_mode: host
|
|
61
|
+
# Additional volumes for BLE (add to existing volumes section above):
|
|
62
|
+
# - /var/run/dbus:/var/run/dbus:ro
|
|
63
|
+
# - /sys/bus/usb:/sys/bus/usb:ro
|
|
64
|
+
# - /sys/class/bluetooth:/sys/class/bluetooth:ro
|
|
65
|
+
# - /sys/devices:/sys/devices:ro
|
|
66
|
+
|
|
67
|
+
# Optional: Watchtower for automatic updates
|
|
68
|
+
# Uncomment this service to enable daily checks for new images
|
|
69
|
+
# watchtower:
|
|
70
|
+
# image: containrrr/watchtower:latest
|
|
71
|
+
# container_name: watchtower-mmrelay
|
|
72
|
+
# restart: unless-stopped
|
|
73
|
+
# volumes:
|
|
74
|
+
# - /var/run/docker.sock:/var/run/docker.sock
|
|
75
|
+
# environment:
|
|
76
|
+
# - WATCHTOWER_CLEANUP=true
|
|
77
|
+
# - WATCHTOWER_INCLUDE_STOPPED=true
|
|
78
|
+
# - WATCHTOWER_SCHEDULE=0 0 2 * * * # Daily at 2 AM
|
|
79
|
+
# - WATCHTOWER_TIMEOUT=30s
|
|
80
|
+
# command: meshtastic-matrix-relay
|
|
@@ -10,15 +10,41 @@ services:
|
|
|
10
10
|
- PYTHONUNBUFFERED=1
|
|
11
11
|
- MPLCONFIGDIR=/tmp/matplotlib
|
|
12
12
|
|
|
13
|
+
# Matrix Authentication: Use 'mmrelay auth login' on host system to create credentials.json
|
|
14
|
+
# The credentials file will be automatically loaded from the volume mount below
|
|
15
|
+
|
|
16
|
+
# Meshtastic Connection Settings - Uncomment and configure as needed
|
|
17
|
+
# TCP Connection (most common)
|
|
18
|
+
# - MMRELAY_MESHTASTIC_CONNECTION_TYPE=tcp
|
|
19
|
+
# - MMRELAY_MESHTASTIC_HOST=192.168.1.100
|
|
20
|
+
# - MMRELAY_MESHTASTIC_PORT=4403 # Default port
|
|
21
|
+
|
|
22
|
+
# Serial Connection (uncomment for serial)
|
|
23
|
+
# - MMRELAY_MESHTASTIC_CONNECTION_TYPE=serial
|
|
24
|
+
# - MMRELAY_MESHTASTIC_SERIAL_PORT=/dev/ttyUSB0
|
|
25
|
+
|
|
26
|
+
# BLE Connection (uncomment for Bluetooth)
|
|
27
|
+
# - MMRELAY_MESHTASTIC_CONNECTION_TYPE=ble
|
|
28
|
+
# - MMRELAY_MESHTASTIC_BLE_ADDRESS=AA:BB:CC:DD:EE:FF
|
|
29
|
+
|
|
30
|
+
# Meshtastic Operational Settings
|
|
31
|
+
# - MMRELAY_MESHTASTIC_BROADCAST_ENABLED=true
|
|
32
|
+
# - MMRELAY_MESHTASTIC_MESHNET_NAME=My Mesh Network
|
|
33
|
+
# - MMRELAY_MESHTASTIC_MESSAGE_DELAY=2.2 # Minimum 2.0 seconds
|
|
34
|
+
|
|
35
|
+
# System Configuration
|
|
36
|
+
# - MMRELAY_LOGGING_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
37
|
+
# - MMRELAY_LOG_FILE=/app/data/logs/mmrelay.log # Enables file logging
|
|
38
|
+
# - MMRELAY_DATABASE_PATH=/app/data/meshtastic.sqlite
|
|
39
|
+
|
|
40
|
+
# Note: Environment variables for Meshtastic, logging, and database settings take precedence over config.yaml
|
|
41
|
+
|
|
13
42
|
volumes:
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# These directories will be created automatically
|
|
20
|
-
- ${MMRELAY_HOME}/.mmrelay/data:/app/data
|
|
21
|
-
- ${MMRELAY_HOME}/.mmrelay/logs:/app/logs
|
|
43
|
+
# Map entire ~/.mmrelay directory to /app/data to maintain proper structure
|
|
44
|
+
# This includes config.yaml, plugins/, data/, logs/, credentials.json, and any other subdirectories
|
|
45
|
+
# The --data-dir parameter in CMD points to /app/data
|
|
46
|
+
# Create config.yaml first with: make config
|
|
47
|
+
- ${MMRELAY_HOME}/.mmrelay:/app/data
|
|
22
48
|
|
|
23
49
|
# For TCP connections (most common) - Meshtastic typically uses port 4403
|
|
24
50
|
ports:
|
mmrelay/tools/sample_config.yaml
CHANGED
|
@@ -3,6 +3,27 @@ matrix:
|
|
|
3
3
|
access_token: reaalllllyloooooongsecretttttcodeeeeeeforrrrbot # See: https://t2bot.io/docs/access_tokens/
|
|
4
4
|
bot_user_id: "@botuser:example.matrix.org"
|
|
5
5
|
|
|
6
|
+
# Alternative: Automatic credentials creation (Docker-friendly)
|
|
7
|
+
# If you provide password instead of access_token, MMRelay will automatically
|
|
8
|
+
# create credentials.json on startup. Useful for Docker deployments.
|
|
9
|
+
#password: your_matrix_password_here # Uncomment and set your Matrix password
|
|
10
|
+
|
|
11
|
+
# End-to-End Encryption (E2EE) configuration
|
|
12
|
+
# NOTE: E2EE requires credentials.json instead of access_token for new sessions
|
|
13
|
+
#
|
|
14
|
+
# SETUP INSTRUCTIONS:
|
|
15
|
+
# 1. Install E2EE dependencies: pipx install 'mmrelay[e2e]'
|
|
16
|
+
# 2. Enable E2EE in config: uncomment and set enabled: true below
|
|
17
|
+
# 3. Create credentials: mmrelay auth login
|
|
18
|
+
# 4. The auth login command will create credentials.json with your Matrix login
|
|
19
|
+
# 5. Restart mmrelay - it will use credentials.json and enable E2EE automatically
|
|
20
|
+
#
|
|
21
|
+
#e2ee:
|
|
22
|
+
# # Optional: When credentials.json is present, MMRelay auto-enables E2EE.
|
|
23
|
+
# # Configure this section only if you want to override defaults (e.g., store_path).
|
|
24
|
+
# enabled: false # Explicit toggle if you need to force-enable/disable (usually not needed)
|
|
25
|
+
# store_path: ~/.mmrelay/store # Optional path for encryption keys storage
|
|
26
|
+
|
|
6
27
|
# Message prefix customization (Meshtastic → Matrix direction)
|
|
7
28
|
#prefix_enabled: true # Enable prefixes on messages from mesh (e.g., "[Alice/MyMesh]: message")
|
|
8
29
|
#prefix_format: "[{long}/{mesh}]: " # Default format. Variables: {long1-20}, {long}, {short}, {mesh1-20}, {mesh}
|
|
@@ -31,7 +52,7 @@ meshtastic:
|
|
|
31
52
|
# # Legacy: heartbeat_interval at meshtastic level still supported but deprecated
|
|
32
53
|
|
|
33
54
|
# Additional configuration options (commented out with defaults)
|
|
34
|
-
|
|
55
|
+
broadcast_enabled: true # Must be set to true to enable Matrix to Meshtastic messages
|
|
35
56
|
#detection_sensor: true # Must be set to true to forward messages of Meshtastic's detection sensor module
|
|
36
57
|
#message_delay: 2.2 # Delay in seconds between messages sent to mesh (minimum: 2.0 due to firmware)
|
|
37
58
|
|
|
@@ -48,10 +69,14 @@ logging:
|
|
|
48
69
|
#color_enabled: true # Set to false to disable colored console output
|
|
49
70
|
|
|
50
71
|
# Component-specific debug logging (useful for troubleshooting)
|
|
72
|
+
# When disabled (false or omitted), external library logs are completely suppressed
|
|
73
|
+
# When enabled, you can use: true (DEBUG level) or specify level: "debug", "info", "warning", "error"
|
|
51
74
|
#debug:
|
|
52
|
-
# matrix_nio: false #
|
|
53
|
-
#
|
|
54
|
-
#
|
|
75
|
+
# matrix_nio: false # Disable matrix-nio logging (default: completely suppressed)
|
|
76
|
+
# #matrix_nio: true # Enable matrix-nio debug logging
|
|
77
|
+
# #matrix_nio: "warning" # Enable matrix-nio warning+ logging
|
|
78
|
+
# bleak: false # Disable BLE (bleak) logging (default: completely suppressed)
|
|
79
|
+
# meshtastic: false # Disable meshtastic library logging (default: completely suppressed)
|
|
55
80
|
|
|
56
81
|
#database:
|
|
57
82
|
# path: ~/.mmrelay/data/meshtastic.sqlite # Default location
|