mmrelay 1.2.0__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.

@@ -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
- """Handle incoming meshtastic message and relay to Matrix.
90
+ """
91
+ Relay a Meshtastic packet to a configured Matrix room.
91
92
 
92
- Args:
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
- Returns:
99
- bool: Always returns False to allow other plugins to process the same packet
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
- Processes the packet by normalizing and preparing it, connects to the Matrix client,
102
- checks if the meshtastic channel is mapped to a Matrix room based on config,
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 False
138
+ return None
140
139
 
141
140
  def matches(self, event):
142
- """Check if Matrix event is a relayed radio packet.
143
-
144
- Args:
145
- event: Matrix room event object
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 event contains embedded meshtastic packet JSON
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
- """Handle incoming Matrix room message and relay to meshtastic mesh.
163
+ """
164
+ Relay an embedded Meshtastic packet from a Matrix room message to the Meshtastic mesh.
164
165
 
165
- Args:
166
- room: Matrix Room object where message was received
167
- event: Matrix room event containing the message
168
- full_message (str): Raw message body text
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
- bool: True if packet relaying succeeded, False otherwise
177
+ None
172
178
 
173
- Checks if the Matrix event matches the expected embedded packet format,
174
- retrieves the packet JSON, decodes it, reconstructs a MeshPacket,
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 False
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 not channel:
193
+ if channel is None:
189
194
  self.logger.debug(f"Skipping message from unmapped channel {channel}")
190
- return False
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 False
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.error(f"Error processing embedded packet: {e}")
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 True
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
- 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.
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 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.".
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
- - 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.
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.error(f"Error fetching weather data: {e}")
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.error(f"Error fetching weather data: {e}")
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.error(f"Malformed weather data: {e}")
213
+ self.logger.exception("Malformed weather data")
215
214
  return "Error parsing weather data."
216
215
  else:
217
216
  # Re-raise unexpected exceptions
@@ -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