mmrelay 1.0.8__tar.gz → 1.0.9__tar.gz
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-1.0.8/src/mmrelay.egg-info → mmrelay-1.0.9}/PKG-INFO +2 -2
- {mmrelay-1.0.8 → mmrelay-1.0.9}/requirements.txt +1 -1
- {mmrelay-1.0.8 → mmrelay-1.0.9}/setup.cfg +2 -2
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/main.py +1 -1
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/matrix_utils.py +11 -29
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugin_loader.py +63 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/base_plugin.py +179 -1
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/debug_plugin.py +13 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/help_plugin.py +28 -3
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/map_plugin.py +31 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/mesh_relay_plugin.py +89 -2
- {mmrelay-1.0.8 → mmrelay-1.0.9/src/mmrelay.egg-info}/PKG-INFO +2 -2
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay.egg-info/requires.txt +1 -1
- {mmrelay-1.0.8 → mmrelay-1.0.9}/LICENSE +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/MANIFEST.in +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/README.md +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/pyproject.toml +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/__init__.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/cli.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/config.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/config_checker.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/db_utils.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/log_utils.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/meshtastic_utils.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/__init__.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/drop_plugin.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/health_plugin.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/nodes_plugin.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/ping_plugin.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/telemetry_plugin.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/plugins/weather_plugin.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/setup_utils.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/tools/__init__.py +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/tools/mmrelay.service +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay/tools/sample_config.yaml +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay.egg-info/SOURCES.txt +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay.egg-info/dependency_links.txt +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay.egg-info/entry_points.txt +0 -0
- {mmrelay-1.0.8 → mmrelay-1.0.9}/src/mmrelay.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmrelay
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
5
5
|
Home-page: https://github.com/geoffwhittington/meshtastic-matrix-relay
|
|
6
6
|
Author: Geoff Whittington, Jeremiah K., and contributors
|
|
@@ -25,7 +25,7 @@ Requires-Dist: schedule==1.2.2
|
|
|
25
25
|
Requires-Dist: platformdirs==4.3.8
|
|
26
26
|
Requires-Dist: py-staticmaps>=0.4.0
|
|
27
27
|
Requires-Dist: rich==14.0.0
|
|
28
|
-
Requires-Dist: setuptools==80.
|
|
28
|
+
Requires-Dist: setuptools==80.9.0
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
31
|
# M<>M Relay
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = mmrelay
|
|
3
|
-
version = 1.0.
|
|
3
|
+
version = 1.0.9
|
|
4
4
|
author = Geoff Whittington, Jeremiah K., and contributors
|
|
5
5
|
author_email = jeremiahk@gmx.com
|
|
6
6
|
description = Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
@@ -33,7 +33,7 @@ install_requires =
|
|
|
33
33
|
platformdirs==4.3.8
|
|
34
34
|
py-staticmaps>=0.4.0
|
|
35
35
|
rich==14.0.0
|
|
36
|
-
setuptools==80.
|
|
36
|
+
setuptools==80.9.0
|
|
37
37
|
include_package_data = True
|
|
38
38
|
|
|
39
39
|
[options.packages.find]
|
|
@@ -23,7 +23,7 @@ from mmrelay.db_utils import (
|
|
|
23
23
|
from mmrelay.log_utils import get_logger
|
|
24
24
|
from mmrelay.matrix_utils import connect_matrix, join_matrix_room
|
|
25
25
|
from mmrelay.matrix_utils import logger as matrix_logger
|
|
26
|
-
from mmrelay.matrix_utils import
|
|
26
|
+
from mmrelay.matrix_utils import on_room_member, on_room_message
|
|
27
27
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
28
28
|
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
29
29
|
from mmrelay.plugin_loader import load_plugins
|
|
@@ -524,7 +524,9 @@ async def on_room_message(
|
|
|
524
524
|
full_display_name = room_display_name
|
|
525
525
|
else:
|
|
526
526
|
# Fallback to global display name if room-specific name is not available
|
|
527
|
-
display_name_response = await matrix_client.get_displayname(
|
|
527
|
+
display_name_response = await matrix_client.get_displayname(
|
|
528
|
+
event.sender
|
|
529
|
+
)
|
|
528
530
|
full_display_name = display_name_response.displayname or event.sender
|
|
529
531
|
|
|
530
532
|
# If not from a remote meshnet, proceed as normal to relay back to the originating meshnet
|
|
@@ -771,32 +773,12 @@ async def on_room_member(room: MatrixRoom, event: RoomMemberEvent) -> None:
|
|
|
771
773
|
"""
|
|
772
774
|
Callback to handle room member events, specifically tracking room-specific display name changes.
|
|
773
775
|
This ensures we detect when users update their display names in specific rooms.
|
|
774
|
-
"""
|
|
775
|
-
# Only track updates from active members
|
|
776
|
-
if event.membership != "join":
|
|
777
|
-
return
|
|
778
|
-
|
|
779
|
-
new_displayname = event.content.get("displayname")
|
|
780
|
-
old_displayname = event.prev_content.get("displayname") if event.prev_content else None
|
|
781
|
-
user_id = event.state_key
|
|
782
|
-
room_id = room.room_id
|
|
783
|
-
|
|
784
|
-
# Log display name changes for debugging
|
|
785
|
-
if new_displayname != old_displayname:
|
|
786
|
-
if new_displayname and old_displayname:
|
|
787
|
-
logger.info(
|
|
788
|
-
f"[Matrix] {user_id} updated room display name in {room_id}: "
|
|
789
|
-
f"'{old_displayname}' → '{new_displayname}'"
|
|
790
|
-
)
|
|
791
|
-
elif new_displayname and not old_displayname:
|
|
792
|
-
logger.info(
|
|
793
|
-
f"[Matrix] {user_id} set room display name in {room_id}: '{new_displayname}'"
|
|
794
|
-
)
|
|
795
|
-
elif not new_displayname and old_displayname:
|
|
796
|
-
logger.info(
|
|
797
|
-
f"[Matrix] {user_id} removed room display name in {room_id}: '{old_displayname}' → (global name)"
|
|
798
|
-
)
|
|
799
776
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
777
|
+
Note: This callback doesn't need to do any explicit processing since matrix-nio
|
|
778
|
+
automatically updates the room state and room.user_name() will return the
|
|
779
|
+
updated room-specific display name immediately after this event.
|
|
780
|
+
"""
|
|
781
|
+
# The callback is registered to ensure matrix-nio processes the event,
|
|
782
|
+
# but no explicit action is needed since room.user_name() automatically
|
|
783
|
+
# handles room-specific display names after the room state is updated.
|
|
784
|
+
pass
|
|
@@ -57,6 +57,28 @@ def get_community_plugin_dirs():
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def clone_or_update_repo(repo_url, ref, plugins_dir):
|
|
60
|
+
"""Clone or update a Git repository for community plugins.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
repo_url (str): Git repository URL to clone/update
|
|
64
|
+
ref (dict): Reference specification with keys:
|
|
65
|
+
- type: "tag" or "branch"
|
|
66
|
+
- value: tag name or branch name
|
|
67
|
+
plugins_dir (str): Directory where the repository should be cloned
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
bool: True if successful, False if clone/update failed
|
|
71
|
+
|
|
72
|
+
Handles complex Git operations including:
|
|
73
|
+
- Cloning new repositories with specific tags/branches
|
|
74
|
+
- Updating existing repositories and switching refs
|
|
75
|
+
- Installing requirements.txt dependencies via pip or pipx
|
|
76
|
+
- Fallback to default branches (main/master) when specified ref fails
|
|
77
|
+
- Robust error handling and logging
|
|
78
|
+
|
|
79
|
+
The function automatically installs Python dependencies if a requirements.txt
|
|
80
|
+
file is found in the repository root.
|
|
81
|
+
"""
|
|
60
82
|
# Extract the repository name from the URL
|
|
61
83
|
repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
|
|
62
84
|
repo_path = os.path.join(plugins_dir, repo_name)
|
|
@@ -497,6 +519,27 @@ def clone_or_update_repo(repo_url, ref, plugins_dir):
|
|
|
497
519
|
|
|
498
520
|
|
|
499
521
|
def load_plugins_from_directory(directory, recursive=False):
|
|
522
|
+
"""Load plugin classes from Python files in a directory.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
directory (str): Directory path to search for plugin files
|
|
526
|
+
recursive (bool): Whether to search subdirectories recursively
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
list: List of instantiated plugin objects found in the directory
|
|
530
|
+
|
|
531
|
+
Scans for .py files and attempts to import each as a module. Looks for
|
|
532
|
+
a 'Plugin' class in each module and instantiates it if found.
|
|
533
|
+
|
|
534
|
+
Features:
|
|
535
|
+
- Automatic dependency installation for missing imports (via pip/pipx)
|
|
536
|
+
- Compatibility layer for import paths (plugins vs mmrelay.plugins)
|
|
537
|
+
- Proper sys.path management for plugin directory imports
|
|
538
|
+
- Comprehensive error handling and logging
|
|
539
|
+
|
|
540
|
+
Skips files that don't define a Plugin class or have import errors
|
|
541
|
+
that can't be automatically resolved.
|
|
542
|
+
"""
|
|
500
543
|
plugins = []
|
|
501
544
|
if os.path.isdir(directory):
|
|
502
545
|
for root, _dirs, files in os.walk(directory):
|
|
@@ -617,6 +660,26 @@ def load_plugins_from_directory(directory, recursive=False):
|
|
|
617
660
|
|
|
618
661
|
|
|
619
662
|
def load_plugins(passed_config=None):
|
|
663
|
+
"""Load and initialize all active plugins based on configuration.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
passed_config (dict, optional): Configuration dictionary to use.
|
|
667
|
+
If None, uses global config variable.
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
list: List of active plugin instances sorted by priority
|
|
671
|
+
|
|
672
|
+
This is the main plugin loading function that:
|
|
673
|
+
- Loads core plugins from mmrelay.plugins package
|
|
674
|
+
- Processes custom plugins from ~/.mmrelay/plugins/custom and plugins/custom
|
|
675
|
+
- Downloads and loads community plugins from configured Git repositories
|
|
676
|
+
- Filters plugins based on active status in configuration
|
|
677
|
+
- Sorts active plugins by priority and calls their start() method
|
|
678
|
+
- Sets up proper plugin configuration and channel mapping
|
|
679
|
+
|
|
680
|
+
Only plugins explicitly marked as active=true in config are loaded.
|
|
681
|
+
Custom and community plugins are cloned/updated automatically.
|
|
682
|
+
"""
|
|
620
683
|
global sorted_active_plugins
|
|
621
684
|
global plugins_loaded
|
|
622
685
|
global config
|
|
@@ -20,6 +20,27 @@ config = None
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class BasePlugin(ABC):
|
|
23
|
+
"""Abstract base class for all mmrelay plugins.
|
|
24
|
+
|
|
25
|
+
Provides common functionality for plugin development including:
|
|
26
|
+
- Configuration management and validation
|
|
27
|
+
- Database storage for plugin-specific data
|
|
28
|
+
- Channel and direct message handling
|
|
29
|
+
- Matrix message sending capabilities
|
|
30
|
+
- Scheduling support for background tasks
|
|
31
|
+
- Command matching and routing
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
plugin_name (str): Unique identifier for the plugin
|
|
35
|
+
max_data_rows_per_node (int): Maximum data rows stored per node (default: 100)
|
|
36
|
+
priority (int): Plugin execution priority (lower = higher priority, default: 10)
|
|
37
|
+
|
|
38
|
+
Subclasses must:
|
|
39
|
+
- Set plugin_name as a class attribute
|
|
40
|
+
- Implement handle_meshtastic_message() and handle_room_message()
|
|
41
|
+
- Optionally override other methods for custom behavior
|
|
42
|
+
"""
|
|
43
|
+
|
|
23
44
|
# Class-level default attributes
|
|
24
45
|
plugin_name = None # Must be overridden in subclasses
|
|
25
46
|
max_data_rows_per_node = 100
|
|
@@ -27,9 +48,32 @@ class BasePlugin(ABC):
|
|
|
27
48
|
|
|
28
49
|
@property
|
|
29
50
|
def description(self):
|
|
51
|
+
"""Get the plugin description for help text.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
str: Human-readable description of plugin functionality
|
|
55
|
+
|
|
56
|
+
Override this property in subclasses to provide meaningful help text
|
|
57
|
+
that will be displayed by the help plugin.
|
|
58
|
+
"""
|
|
30
59
|
return ""
|
|
31
60
|
|
|
32
61
|
def __init__(self, plugin_name=None) -> None:
|
|
62
|
+
"""Initialize the plugin with configuration and logging.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
plugin_name (str, optional): Plugin name override. If not provided,
|
|
66
|
+
uses class-level plugin_name attribute.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If plugin_name is not set via parameter or class attribute
|
|
70
|
+
|
|
71
|
+
Sets up:
|
|
72
|
+
- Plugin-specific logger
|
|
73
|
+
- Configuration from global config
|
|
74
|
+
- Channel mapping and validation
|
|
75
|
+
- Response delay settings
|
|
76
|
+
"""
|
|
33
77
|
# Allow plugin_name to be passed as a parameter for simpler initialization
|
|
34
78
|
# This maintains backward compatibility while providing a cleaner API
|
|
35
79
|
super().__init__()
|
|
@@ -96,6 +140,19 @@ class BasePlugin(ABC):
|
|
|
96
140
|
)
|
|
97
141
|
|
|
98
142
|
def start(self):
|
|
143
|
+
"""Start the plugin and set up scheduled tasks if configured.
|
|
144
|
+
|
|
145
|
+
Called automatically when the plugin is loaded. Checks plugin configuration
|
|
146
|
+
for scheduling settings and sets up background jobs accordingly.
|
|
147
|
+
|
|
148
|
+
Supported schedule formats in config:
|
|
149
|
+
- schedule.hours + schedule.at: Run every N hours at specific time
|
|
150
|
+
- schedule.minutes + schedule.at: Run every N minutes at specific time
|
|
151
|
+
- schedule.hours: Run every N hours
|
|
152
|
+
- schedule.minutes: Run every N minutes
|
|
153
|
+
|
|
154
|
+
Creates a daemon thread to run the scheduler if any schedule is configured.
|
|
155
|
+
"""
|
|
99
156
|
if "schedule" not in self.config or (
|
|
100
157
|
"at" not in self.config["schedule"]
|
|
101
158
|
and "hours" not in self.config["schedule"]
|
|
@@ -137,9 +194,27 @@ class BasePlugin(ABC):
|
|
|
137
194
|
|
|
138
195
|
# trunk-ignore(ruff/B027)
|
|
139
196
|
def background_job(self):
|
|
197
|
+
"""Background task executed on schedule.
|
|
198
|
+
|
|
199
|
+
Override this method in subclasses to implement scheduled functionality.
|
|
200
|
+
Called automatically based on schedule configuration in start().
|
|
201
|
+
|
|
202
|
+
Default implementation does nothing.
|
|
203
|
+
"""
|
|
140
204
|
pass # Implement in subclass if needed
|
|
141
205
|
|
|
142
206
|
def strip_raw(self, data):
|
|
207
|
+
"""Recursively remove 'raw' keys from data structures.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
data: Data structure (dict, list, or other) to clean
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Cleaned data structure with 'raw' keys removed
|
|
214
|
+
|
|
215
|
+
Useful for cleaning packet data before logging or storage to remove
|
|
216
|
+
binary protobuf data that's not human-readable.
|
|
217
|
+
"""
|
|
143
218
|
if isinstance(data, dict):
|
|
144
219
|
data.pop("raw", None)
|
|
145
220
|
for k, v in data.items():
|
|
@@ -149,19 +224,59 @@ class BasePlugin(ABC):
|
|
|
149
224
|
return data
|
|
150
225
|
|
|
151
226
|
def get_response_delay(self):
|
|
227
|
+
"""Get the configured response delay for meshtastic messages.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
int: Delay in seconds before sending responses (default: 3)
|
|
231
|
+
|
|
232
|
+
Used to prevent message flooding and ensure proper radio etiquette.
|
|
233
|
+
Delay is configured via meshtastic.plugin_response_delay in config.
|
|
234
|
+
"""
|
|
152
235
|
return self.response_delay
|
|
153
236
|
|
|
154
|
-
# Modified method to accept is_direct_message parameter
|
|
155
237
|
def is_channel_enabled(self, channel, is_direct_message=False):
|
|
238
|
+
"""Check if the plugin should respond on a specific channel.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
channel: Channel identifier to check
|
|
242
|
+
is_direct_message (bool): Whether this is a direct message
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
bool: True if plugin should respond, False otherwise
|
|
246
|
+
|
|
247
|
+
Direct messages always return True if the plugin is active.
|
|
248
|
+
For channel messages, checks if channel is in plugin's configured channels list.
|
|
249
|
+
"""
|
|
156
250
|
if is_direct_message:
|
|
157
251
|
return True # Always respond to DMs if the plugin is active
|
|
158
252
|
else:
|
|
159
253
|
return channel in self.channels
|
|
160
254
|
|
|
161
255
|
def get_matrix_commands(self):
|
|
256
|
+
"""Get list of Matrix commands this plugin responds to.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
list: List of command strings (without ! prefix)
|
|
260
|
+
|
|
261
|
+
Default implementation returns [plugin_name]. Override to provide
|
|
262
|
+
custom commands or multiple command aliases.
|
|
263
|
+
"""
|
|
162
264
|
return [self.plugin_name]
|
|
163
265
|
|
|
164
266
|
async def send_matrix_message(self, room_id, message, formatted=True):
|
|
267
|
+
"""Send a message to a Matrix room.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
room_id (str): Matrix room identifier
|
|
271
|
+
message (str): Message content to send
|
|
272
|
+
formatted (bool): Whether to send as formatted HTML (default: True)
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
dict: Response from Matrix API room_send
|
|
276
|
+
|
|
277
|
+
Connects to Matrix using matrix_utils and sends a room message
|
|
278
|
+
with optional HTML formatting via markdown.
|
|
279
|
+
"""
|
|
165
280
|
from mmrelay.matrix_utils import connect_matrix
|
|
166
281
|
|
|
167
282
|
matrix_client = await connect_matrix()
|
|
@@ -178,9 +293,26 @@ class BasePlugin(ABC):
|
|
|
178
293
|
)
|
|
179
294
|
|
|
180
295
|
def get_mesh_commands(self):
|
|
296
|
+
"""Get list of mesh/radio commands this plugin responds to.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
list: List of command strings (without ! prefix)
|
|
300
|
+
|
|
301
|
+
Default implementation returns empty list. Override to handle
|
|
302
|
+
commands sent over the mesh radio network.
|
|
303
|
+
"""
|
|
181
304
|
return []
|
|
182
305
|
|
|
183
306
|
def store_node_data(self, meshtastic_id, node_data):
|
|
307
|
+
"""Store data for a specific node, appending to existing data.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
meshtastic_id (str): Node identifier
|
|
311
|
+
node_data: Data to store (single item or list)
|
|
312
|
+
|
|
313
|
+
Retrieves existing data, appends new data, trims to max_data_rows_per_node,
|
|
314
|
+
and stores back to database. Use for accumulating time-series data.
|
|
315
|
+
"""
|
|
184
316
|
data = self.get_node_data(meshtastic_id=meshtastic_id)
|
|
185
317
|
data = data[-self.max_data_rows_per_node :]
|
|
186
318
|
if isinstance(node_data, list):
|
|
@@ -190,21 +322,56 @@ class BasePlugin(ABC):
|
|
|
190
322
|
store_plugin_data(self.plugin_name, meshtastic_id, data)
|
|
191
323
|
|
|
192
324
|
def set_node_data(self, meshtastic_id, node_data):
|
|
325
|
+
"""Replace all data for a specific node.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
meshtastic_id (str): Node identifier
|
|
329
|
+
node_data: Data to store (replaces existing data)
|
|
330
|
+
|
|
331
|
+
Completely replaces existing data for the node, trimming to
|
|
332
|
+
max_data_rows_per_node if needed. Use when you want to reset
|
|
333
|
+
or completely replace a node's data.
|
|
334
|
+
"""
|
|
193
335
|
node_data = node_data[-self.max_data_rows_per_node :]
|
|
194
336
|
store_plugin_data(self.plugin_name, meshtastic_id, node_data)
|
|
195
337
|
|
|
196
338
|
def delete_node_data(self, meshtastic_id):
|
|
339
|
+
"""Delete all stored data for a specific node.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
meshtastic_id (str): Node identifier
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
bool: True if deletion succeeded, False otherwise
|
|
346
|
+
"""
|
|
197
347
|
return delete_plugin_data(self.plugin_name, meshtastic_id)
|
|
198
348
|
|
|
199
349
|
def get_node_data(self, meshtastic_id):
|
|
350
|
+
"""Retrieve stored data for a specific node.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
meshtastic_id (str): Node identifier
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
list: Stored data for the node (JSON deserialized)
|
|
357
|
+
"""
|
|
200
358
|
return get_plugin_data_for_node(self.plugin_name, meshtastic_id)
|
|
201
359
|
|
|
202
360
|
def get_data(self):
|
|
361
|
+
"""Retrieve all stored data for this plugin across all nodes.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
list: List of tuples containing raw data entries
|
|
365
|
+
|
|
366
|
+
Returns raw data without JSON deserialization. Use get_node_data()
|
|
367
|
+
for individual node data that's automatically deserialized.
|
|
368
|
+
"""
|
|
203
369
|
return get_plugin_data(self.plugin_name)
|
|
204
370
|
|
|
205
371
|
def get_plugin_data_dir(self, subdir=None):
|
|
206
372
|
"""
|
|
207
373
|
Returns the directory for storing plugin-specific data files.
|
|
374
|
+
|
|
208
375
|
Creates the directory if it doesn't exist.
|
|
209
376
|
|
|
210
377
|
Args:
|
|
@@ -230,6 +397,17 @@ class BasePlugin(ABC):
|
|
|
230
397
|
return plugin_dir
|
|
231
398
|
|
|
232
399
|
def matches(self, event):
|
|
400
|
+
"""Check if a Matrix event matches this plugin's commands.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
event: Matrix room event to check
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
bool: True if event matches plugin commands, False otherwise
|
|
407
|
+
|
|
408
|
+
Uses bot_command() utility to check if the event contains any of
|
|
409
|
+
the plugin's matrix commands with proper bot command syntax.
|
|
410
|
+
"""
|
|
233
411
|
from mmrelay.matrix_utils import bot_command
|
|
234
412
|
|
|
235
413
|
# Pass the entire event to bot_command
|
|
@@ -2,6 +2,19 @@ from mmrelay.plugins.base_plugin import BasePlugin
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class Plugin(BasePlugin):
|
|
5
|
+
"""Debug plugin for logging packet information.
|
|
6
|
+
|
|
7
|
+
A low-priority plugin that logs all received meshtastic packets
|
|
8
|
+
for debugging and development purposes. Strips raw binary data
|
|
9
|
+
before logging to keep output readable.
|
|
10
|
+
|
|
11
|
+
Configuration:
|
|
12
|
+
priority: 1 (runs first, before other plugins)
|
|
13
|
+
|
|
14
|
+
Never intercepts messages (always returns False) so other plugins
|
|
15
|
+
can still process the same packets.
|
|
16
|
+
"""
|
|
17
|
+
|
|
5
18
|
plugin_name = "debug"
|
|
6
19
|
priority = 1
|
|
7
20
|
|
|
@@ -5,13 +5,28 @@ from mmrelay.plugins.base_plugin import BasePlugin
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Plugin(BasePlugin):
|
|
8
|
-
|
|
8
|
+
"""Help command plugin for listing available commands.
|
|
9
|
+
|
|
10
|
+
Provides users with information about available relay commands
|
|
11
|
+
and plugin functionality.
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
!help: List all available commands
|
|
15
|
+
!help <command>: Show detailed help for a specific command
|
|
16
|
+
|
|
17
|
+
Dynamically discovers available commands from all loaded plugins
|
|
18
|
+
and their descriptions.
|
|
19
|
+
"""
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
# The BasePlugin will automatically use the class-level plugin_name
|
|
21
|
+
plugin_name = "help"
|
|
12
22
|
|
|
13
23
|
@property
|
|
14
24
|
def description(self):
|
|
25
|
+
"""Get plugin description.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
str: Description of help functionality
|
|
29
|
+
"""
|
|
15
30
|
return "List supported relay commands"
|
|
16
31
|
|
|
17
32
|
async def handle_meshtastic_message(
|
|
@@ -20,9 +35,19 @@ class Plugin(BasePlugin):
|
|
|
20
35
|
return False
|
|
21
36
|
|
|
22
37
|
def get_matrix_commands(self):
|
|
38
|
+
"""Get Matrix commands handled by this plugin.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
list: List containing the help command
|
|
42
|
+
"""
|
|
23
43
|
return [self.plugin_name]
|
|
24
44
|
|
|
25
45
|
def get_mesh_commands(self):
|
|
46
|
+
"""Get mesh commands handled by this plugin.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
list: Empty list (help only works via Matrix)
|
|
50
|
+
"""
|
|
26
51
|
return []
|
|
27
52
|
|
|
28
53
|
async def handle_room_message(self, room, event, full_message):
|
|
@@ -156,6 +156,19 @@ class TextLabel(staticmaps.Object):
|
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def anonymize_location(lat, lon, radius=1000):
|
|
159
|
+
"""Add random offset to GPS coordinates for privacy protection.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
lat (float): Original latitude
|
|
163
|
+
lon (float): Original longitude
|
|
164
|
+
radius (int): Maximum offset distance in meters (default: 1000)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
tuple: (new_lat, new_lon) with random offset applied
|
|
168
|
+
|
|
169
|
+
Adds random offset within specified radius to obscure exact locations
|
|
170
|
+
while maintaining general geographic area for mapping purposes.
|
|
171
|
+
"""
|
|
159
172
|
# Generate random offsets for latitude and longitude
|
|
160
173
|
lat_offset = random.uniform(-radius / 111320, radius / 111320)
|
|
161
174
|
lon_offset = random.uniform(
|
|
@@ -233,6 +246,24 @@ async def send_image(client: AsyncClient, room_id: str, image: Image.Image):
|
|
|
233
246
|
|
|
234
247
|
|
|
235
248
|
class Plugin(BasePlugin):
|
|
249
|
+
"""Static map generation plugin for mesh node locations.
|
|
250
|
+
|
|
251
|
+
Generates static maps showing positions of mesh nodes with labeled markers.
|
|
252
|
+
Supports customizable zoom levels, image sizes, and privacy features.
|
|
253
|
+
|
|
254
|
+
Commands:
|
|
255
|
+
!map: Generate map with default settings
|
|
256
|
+
!map zoom=N: Set zoom level (0-30)
|
|
257
|
+
!map size=W,H: Set image dimensions (max 1000x1000)
|
|
258
|
+
|
|
259
|
+
Configuration:
|
|
260
|
+
zoom (int): Default zoom level (default: 8)
|
|
261
|
+
image_width/image_height (int): Default image size (default: 1000x1000)
|
|
262
|
+
anonymize (bool): Whether to offset coordinates for privacy (default: true)
|
|
263
|
+
radius (int): Anonymization offset radius in meters (default: 1000)
|
|
264
|
+
|
|
265
|
+
Uploads generated maps as images to Matrix rooms.
|
|
266
|
+
"""
|
|
236
267
|
plugin_name = "map"
|
|
237
268
|
|
|
238
269
|
# No __init__ method needed with the simplified plugin system
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# Note: This plugin was experimental and is not functional.
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import json
|
|
3
5
|
import re
|
|
@@ -8,12 +10,36 @@ from mmrelay.plugins.base_plugin import BasePlugin, config
|
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class Plugin(BasePlugin):
|
|
13
|
+
"""Core mesh-to-Matrix relay plugin.
|
|
14
|
+
|
|
15
|
+
Handles bidirectional message relay between Meshtastic mesh network
|
|
16
|
+
and Matrix chat rooms. Processes radio packets and forwards them
|
|
17
|
+
to configured Matrix rooms, and vice versa.
|
|
18
|
+
|
|
19
|
+
This plugin is fundamental to the relay's core functionality and
|
|
20
|
+
typically runs with high priority to ensure messages are properly
|
|
21
|
+
bridged between the two networks.
|
|
22
|
+
|
|
23
|
+
Configuration:
|
|
24
|
+
max_data_rows_per_node: 50 (reduced storage for performance)
|
|
25
|
+
"""
|
|
26
|
+
|
|
11
27
|
plugin_name = "mesh_relay"
|
|
12
28
|
max_data_rows_per_node = 50
|
|
13
29
|
|
|
14
30
|
def normalize(self, dict_obj):
|
|
15
|
-
"""
|
|
16
|
-
|
|
31
|
+
"""Normalize packet data to consistent dictionary format.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
dict_obj: Packet data (dict, JSON string, or plain string)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: Normalized packet dictionary with raw data stripped
|
|
38
|
+
|
|
39
|
+
Handles various packet formats:
|
|
40
|
+
- Dict objects (passed through)
|
|
41
|
+
- JSON strings (parsed)
|
|
42
|
+
- Plain strings (wrapped in TEXT_MESSAGE_APP format)
|
|
17
43
|
"""
|
|
18
44
|
if not isinstance(dict_obj, dict):
|
|
19
45
|
try:
|
|
@@ -24,6 +50,17 @@ class Plugin(BasePlugin):
|
|
|
24
50
|
return self.strip_raw(dict_obj)
|
|
25
51
|
|
|
26
52
|
def process(self, packet):
|
|
53
|
+
"""Process and prepare packet data for relay.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
packet: Raw packet data to process
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
dict: Processed packet with base64-encoded binary payloads
|
|
60
|
+
|
|
61
|
+
Normalizes packet format and encodes binary payloads as base64
|
|
62
|
+
for JSON serialization and Matrix transmission.
|
|
63
|
+
"""
|
|
27
64
|
packet = self.normalize(packet)
|
|
28
65
|
|
|
29
66
|
if "decoded" in packet and "payload" in packet["decoded"]:
|
|
@@ -35,14 +72,39 @@ class Plugin(BasePlugin):
|
|
|
35
72
|
return packet
|
|
36
73
|
|
|
37
74
|
def get_matrix_commands(self):
|
|
75
|
+
"""Get Matrix commands handled by this plugin.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
list: Empty list (this plugin handles all traffic, not specific commands)
|
|
79
|
+
"""
|
|
38
80
|
return []
|
|
39
81
|
|
|
40
82
|
def get_mesh_commands(self):
|
|
83
|
+
"""Get mesh commands handled by this plugin.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
list: Empty list (this plugin handles all traffic, not specific commands)
|
|
87
|
+
"""
|
|
41
88
|
return []
|
|
42
89
|
|
|
43
90
|
async def handle_meshtastic_message(
|
|
44
91
|
self, packet, formatted_message, longname, meshnet_name
|
|
45
92
|
):
|
|
93
|
+
"""Handle incoming meshtastic message and relay to Matrix.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
packet: Raw packet data (dict or JSON) to relay
|
|
97
|
+
formatted_message (str): Human-readable message extracted from packet
|
|
98
|
+
longname (str): Long name of the sender node
|
|
99
|
+
meshnet_name (str): Name of the mesh network
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
bool: Always returns False to allow other plugins to process the same packet
|
|
103
|
+
|
|
104
|
+
Processes the packet by normalizing and preparing it, connects to the Matrix client,
|
|
105
|
+
checks if the meshtastic channel is mapped to a Matrix room based on config,
|
|
106
|
+
and sends the packet to the appropriate Matrix room.
|
|
107
|
+
"""
|
|
46
108
|
from mmrelay.matrix_utils import connect_matrix
|
|
47
109
|
|
|
48
110
|
packet = self.process(packet)
|
|
@@ -80,6 +142,17 @@ class Plugin(BasePlugin):
|
|
|
80
142
|
return False
|
|
81
143
|
|
|
82
144
|
def matches(self, event):
|
|
145
|
+
"""Check if Matrix event is a relayed radio packet.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
event: Matrix room event object
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
bool: True if event contains embedded meshtastic packet JSON
|
|
152
|
+
|
|
153
|
+
Identifies Matrix messages that contain embedded meshtastic packet
|
|
154
|
+
data by matching the default relay message format "Processed <portnum> radio packet".
|
|
155
|
+
"""
|
|
83
156
|
# Check for the presence of necessary keys in the event
|
|
84
157
|
content = event.source.get("content", {})
|
|
85
158
|
body = content.get("body", "")
|
|
@@ -90,6 +163,20 @@ class Plugin(BasePlugin):
|
|
|
90
163
|
return False
|
|
91
164
|
|
|
92
165
|
async def handle_room_message(self, room, event, full_message):
|
|
166
|
+
"""Handle incoming Matrix room message and relay to meshtastic mesh.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
room: Matrix Room object where message was received
|
|
170
|
+
event: Matrix room event containing the message
|
|
171
|
+
full_message (str): Raw message body text
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
bool: True if packet relaying succeeded, False otherwise
|
|
175
|
+
|
|
176
|
+
Checks if the Matrix event matches the expected embedded packet format,
|
|
177
|
+
retrieves the packet JSON, decodes it, reconstructs a MeshPacket,
|
|
178
|
+
connects to the meshtastic client, and sends the packet via the radio.
|
|
179
|
+
"""
|
|
93
180
|
# Use the event for matching instead of full_message
|
|
94
181
|
if not self.matches(event):
|
|
95
182
|
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmrelay
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
5
5
|
Home-page: https://github.com/geoffwhittington/meshtastic-matrix-relay
|
|
6
6
|
Author: Geoff Whittington, Jeremiah K., and contributors
|
|
@@ -25,7 +25,7 @@ Requires-Dist: schedule==1.2.2
|
|
|
25
25
|
Requires-Dist: platformdirs==4.3.8
|
|
26
26
|
Requires-Dist: py-staticmaps>=0.4.0
|
|
27
27
|
Requires-Dist: rich==14.0.0
|
|
28
|
-
Requires-Dist: setuptools==80.
|
|
28
|
+
Requires-Dist: setuptools==80.9.0
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
31
|
# M<>M Relay
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|