mmrelay 1.2.1__py3-none-any.whl → 1.2.2__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/__main__.py +29 -0
- mmrelay/cli.py +452 -50
- mmrelay/cli_utils.py +59 -9
- mmrelay/config.py +198 -71
- mmrelay/constants/app.py +2 -2
- mmrelay/db_utils.py +73 -26
- mmrelay/e2ee_utils.py +6 -3
- mmrelay/log_utils.py +16 -5
- mmrelay/main.py +41 -38
- mmrelay/matrix_utils.py +1069 -293
- mmrelay/meshtastic_utils.py +350 -206
- mmrelay/message_queue.py +22 -23
- mmrelay/plugin_loader.py +634 -205
- mmrelay/plugins/mesh_relay_plugin.py +43 -38
- mmrelay/plugins/weather_plugin.py +11 -12
- mmrelay/runtime_utils.py +35 -0
- mmrelay/setup_utils.py +324 -129
- mmrelay/tools/mmrelay.service +2 -1
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +11 -72
- mmrelay/tools/sample-docker-compose.yaml +12 -58
- mmrelay/tools/sample_config.yaml +1 -1
- mmrelay/windows_utils.py +349 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/METADATA +7 -7
- mmrelay-1.2.2.dist-info/RECORD +48 -0
- mmrelay-1.2.1.dist-info/RECORD +0 -45
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/WHEEL +0 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/top_level.txt +0 -0
|
@@ -87,20 +87,19 @@ class Plugin(BasePlugin):
|
|
|
87
87
|
async def handle_meshtastic_message(
|
|
88
88
|
self, packet, formatted_message, longname, meshnet_name
|
|
89
89
|
):
|
|
90
|
-
"""
|
|
90
|
+
"""
|
|
91
|
+
Relay a Meshtastic packet to a configured Matrix room.
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
packet: Raw packet data (dict or JSON) to relay
|
|
94
|
-
formatted_message (str): Human-readable message extracted from packet
|
|
95
|
-
longname (str): Long name of the sender node
|
|
96
|
-
meshnet_name (str): Name of the mesh network
|
|
93
|
+
Normalizes and prepares the incoming Meshtastic packet, determines its channel (defaults to 0 if absent), and, if that channel is mapped in plugin configuration, sends the processed packet to the mapped Matrix room. The sent Matrix event includes a JSON-serialized `meshtastic_packet` in the content and sets `mmrelay_suppress` to True to mark it as a bridged packet. If the packet's channel is not mapped, the function returns without sending anything.
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
Parameters:
|
|
96
|
+
packet: Raw Meshtastic packet (dict, JSON string, or other) to be normalized and relayed.
|
|
97
|
+
formatted_message (str): Human-readable message extracted from the packet (not used for routing).
|
|
98
|
+
longname (str): Long name of the sending node (informational).
|
|
99
|
+
meshnet_name (str): Name of the mesh network (informational).
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
and sends the packet to the appropriate Matrix room.
|
|
101
|
+
Returns:
|
|
102
|
+
None
|
|
104
103
|
"""
|
|
105
104
|
from mmrelay.matrix_utils import connect_matrix
|
|
106
105
|
|
|
@@ -123,7 +122,7 @@ class Plugin(BasePlugin):
|
|
|
123
122
|
|
|
124
123
|
if not channel_mapped:
|
|
125
124
|
self.logger.debug(f"Skipping message from unmapped channel {channel}")
|
|
126
|
-
return
|
|
125
|
+
return None
|
|
127
126
|
|
|
128
127
|
await matrix_client.room_send(
|
|
129
128
|
room_id=room["id"],
|
|
@@ -136,19 +135,20 @@ class Plugin(BasePlugin):
|
|
|
136
135
|
},
|
|
137
136
|
)
|
|
138
137
|
|
|
139
|
-
return
|
|
138
|
+
return None
|
|
140
139
|
|
|
141
140
|
def matches(self, event):
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
"""
|
|
142
|
+
Return True if the Matrix event's content body exactly matches the relayed-packet marker.
|
|
143
|
+
|
|
144
|
+
Checks event.source["content"]["body"] (when a string) against the anchored pattern
|
|
145
|
+
"Processed <anything> radio packet" and returns True on match, otherwise False.
|
|
146
|
+
|
|
147
|
+
Parameters:
|
|
148
|
+
event: Matrix event object with a .source mapping expected to contain a "content" dict.
|
|
149
|
+
|
|
147
150
|
Returns:
|
|
148
|
-
bool: True if
|
|
149
|
-
|
|
150
|
-
Identifies Matrix messages that contain embedded meshtastic packet
|
|
151
|
-
data by matching the default relay message format "Processed <portnum> radio packet".
|
|
151
|
+
bool: True if the body matches the relayed-packet pattern, False otherwise.
|
|
152
152
|
"""
|
|
153
153
|
# Check for the presence of necessary keys in the event
|
|
154
154
|
content = event.source.get("content", {})
|
|
@@ -160,23 +160,28 @@ class Plugin(BasePlugin):
|
|
|
160
160
|
return False
|
|
161
161
|
|
|
162
162
|
async def handle_room_message(self, room, event, full_message):
|
|
163
|
-
"""
|
|
163
|
+
"""
|
|
164
|
+
Relay an embedded Meshtastic packet from a Matrix room message to the Meshtastic mesh.
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
If the Matrix event contains an embedded meshtastic packet (detected via self.matches),
|
|
167
|
+
this function looks up the Meshtastic channel mapped to the Matrix room, parses the
|
|
168
|
+
embedded JSON packet from the event content, reconstructs a MeshPacket (decoding the
|
|
169
|
+
base64-encoded payload), and sends it on the radio via the Meshtastic client.
|
|
170
|
+
|
|
171
|
+
Parameters:
|
|
172
|
+
room: Matrix room object where the message was received (used to find room→channel mapping).
|
|
173
|
+
event: Matrix event containing the message; the embedded packet is read from event.source["content"].
|
|
174
|
+
full_message: Unused — matching and extraction are performed against `event`.
|
|
169
175
|
|
|
170
176
|
Returns:
|
|
171
|
-
|
|
177
|
+
None
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
connects to the meshtastic client, and sends the packet via the radio.
|
|
179
|
+
Side effects:
|
|
180
|
+
Sends a packet onto the Meshtastic radio network when a valid embedded packet and room→channel mapping exist.
|
|
176
181
|
"""
|
|
177
182
|
# Use the event for matching instead of full_message
|
|
178
183
|
if not self.matches(event):
|
|
179
|
-
return
|
|
184
|
+
return None
|
|
180
185
|
|
|
181
186
|
channel = None
|
|
182
187
|
if config is not None:
|
|
@@ -185,20 +190,20 @@ class Plugin(BasePlugin):
|
|
|
185
190
|
if room_config["id"] == room.room_id:
|
|
186
191
|
channel = room_config["meshtastic_channel"]
|
|
187
192
|
|
|
188
|
-
if
|
|
193
|
+
if channel is None:
|
|
189
194
|
self.logger.debug(f"Skipping message from unmapped channel {channel}")
|
|
190
|
-
return
|
|
195
|
+
return None
|
|
191
196
|
|
|
192
197
|
packet_json = event.source["content"].get("meshtastic_packet")
|
|
193
198
|
if not packet_json:
|
|
194
199
|
self.logger.debug("Missing embedded packet")
|
|
195
|
-
return
|
|
200
|
+
return None
|
|
196
201
|
|
|
197
202
|
try:
|
|
198
203
|
packet = json.loads(packet_json)
|
|
199
204
|
except (json.JSONDecodeError, TypeError) as e:
|
|
200
|
-
self.logger.
|
|
201
|
-
return
|
|
205
|
+
self.logger.exception(f"Error processing embedded packet: {e}")
|
|
206
|
+
return None
|
|
202
207
|
|
|
203
208
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
204
209
|
|
|
@@ -215,4 +220,4 @@ class Plugin(BasePlugin):
|
|
|
215
220
|
meshtastic_client._sendPacket(
|
|
216
221
|
meshPacket=meshPacket, destinationId=packet["toId"]
|
|
217
222
|
)
|
|
218
|
-
return
|
|
223
|
+
return None
|
|
@@ -22,23 +22,22 @@ class Plugin(BasePlugin):
|
|
|
22
22
|
"""
|
|
23
23
|
Generate a concise one-line weather forecast for the given GPS coordinates.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Queries the Open-Meteo API for current conditions and hour-aligned forecasts at approximately +2h and +5h, formats temperatures according to self.config.get("units", "metric") ("metric" -> °C, "imperial" -> °F), and returns a single-line summary like:
|
|
26
|
+
"Now: ☀️ Clear sky - 12.3°C | +2h: 🌧️ Light rain - 13.1°C 20% | +5h: ⛅️ Partly cloudy - 10.8°C 5%".
|
|
26
27
|
|
|
27
28
|
Parameters:
|
|
28
29
|
latitude (float): Latitude in decimal degrees.
|
|
29
30
|
longitude (float): Longitude in decimal degrees.
|
|
30
31
|
|
|
31
32
|
Returns:
|
|
32
|
-
str: A
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"Error
|
|
33
|
+
str: A one-line forecast string on success. On recoverable failures returns one of:
|
|
34
|
+
- "Weather data temporarily unavailable." (missing hourly data),
|
|
35
|
+
- "Error fetching weather data." (network/HTTP/request errors),
|
|
36
|
+
- "Error parsing weather data." (malformed or unexpected API response).
|
|
36
37
|
|
|
37
38
|
Notes:
|
|
38
|
-
-
|
|
39
|
-
-
|
|
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.
|
|
39
|
+
- The function attempts to anchor forecasts to hourly timestamps when available; if a timestamp match cannot be found it falls back to hour-of-day indexing.
|
|
40
|
+
- Network/request-related errors and parsing errors are handled as described above; unexpected exceptions are re-raised.
|
|
42
41
|
"""
|
|
43
42
|
units = self.config.get("units", "metric") # Default to metric
|
|
44
43
|
temperature_unit = "°C" if units == "metric" else "°F"
|
|
@@ -198,20 +197,20 @@ class Plugin(BasePlugin):
|
|
|
198
197
|
if hasattr(requests, "RequestException") and isinstance(
|
|
199
198
|
e, requests.RequestException
|
|
200
199
|
):
|
|
201
|
-
self.logger.
|
|
200
|
+
self.logger.exception("Error fetching weather data")
|
|
202
201
|
return "Error fetching weather data."
|
|
203
202
|
except (AttributeError, TypeError):
|
|
204
203
|
# Fallback to string-based detection if isinstance fails
|
|
205
204
|
exception_module = getattr(type(e), "__module__", "")
|
|
206
205
|
if "requests" in exception_module:
|
|
207
|
-
self.logger.
|
|
206
|
+
self.logger.exception("Error fetching weather data")
|
|
208
207
|
return "Error fetching weather data."
|
|
209
208
|
|
|
210
209
|
# Handle data parsing errors
|
|
211
210
|
if isinstance(
|
|
212
211
|
e, (KeyError, IndexError, TypeError, ValueError, AttributeError)
|
|
213
212
|
):
|
|
214
|
-
self.logger.
|
|
213
|
+
self.logger.exception("Malformed weather data")
|
|
215
214
|
return "Error parsing weather data."
|
|
216
215
|
else:
|
|
217
216
|
# Re-raise unexpected exceptions
|
mmrelay/runtime_utils.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Runtime environment helpers for MMRelay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from mmrelay.constants.network import SYSTEMD_INIT_SYSTEM
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_running_as_service() -> bool:
|
|
11
|
+
"""
|
|
12
|
+
Return True if the current process appears to be running as a systemd service.
|
|
13
|
+
|
|
14
|
+
Checks whether the INVOCATION_ID environment variable is set (systemd-provided) and, if not,
|
|
15
|
+
inspects /proc/self/status to find the parent PID and then /proc/<ppid>/comm to compare the
|
|
16
|
+
parent process name against the expected systemd init binary name. If any file access,
|
|
17
|
+
permission, or parsing errors occur, the function returns False.
|
|
18
|
+
Returns:
|
|
19
|
+
bool: True when running under a systemd service, otherwise False.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if os.environ.get("INVOCATION_ID"):
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with open("/proc/self/status", encoding="utf-8") as status_file:
|
|
27
|
+
for line in status_file:
|
|
28
|
+
if line.startswith("PPid:"):
|
|
29
|
+
ppid = int(line.split()[1])
|
|
30
|
+
with open(f"/proc/{ppid}/comm", encoding="utf-8") as comm_file:
|
|
31
|
+
return comm_file.read().strip() == SYSTEMD_INIT_SYSTEM
|
|
32
|
+
except (FileNotFoundError, PermissionError, ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
return False
|