Skip to content

Commit

Permalink
Add help command to list active plugin commands (#43)
Browse files Browse the repository at this point in the history
* Add help command to list active plugin commands
* Format plugin responses
  • Loading branch information
geoffwhittington authored May 17, 2023
1 parent d2421e5 commit 1efa58d
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 38 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# M<>M Relay
# M<>M Relay

### (Meshtastic <=> Matrix Relay)

A powerful and easy-to-use relay between Meshtastic devices and Matrix chat rooms, allowing seamless communication across platforms. This opens the door for bridging Meshtastic devices to [many other platforms](https://matrix.org/bridges/).
Expand Down Expand Up @@ -34,8 +35,8 @@ Produce high-level details about your mesh

The relay can run on:

* Linux
* MacOS
* Windows
- Linux
- MacOS
- Windows

Refer to [the development instructions](DEVELOPMENT.md) for more details.
6 changes: 5 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from meshtastic_utils import (
connect_meshtastic,
on_meshtastic_message,
on_lost_meshtastic_connection,
logger as meshtastic_logger,
)

Expand Down Expand Up @@ -56,7 +57,10 @@ async def main():
pub.subscribe(
on_meshtastic_message, "meshtastic.receive", loop=asyncio.get_event_loop()
)

pub.subscribe(
on_lost_meshtastic_connection,
"meshtastic.connection.lost",
)
# Register the message callback

matrix_logger.info(f"Listening for inbound matrix messages ...")
Expand Down
16 changes: 9 additions & 7 deletions plugin_loader.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
from plugins.health_plugin import Plugin as HealthPlugin
from plugins.map_plugin import Plugin as MapPlugin
from plugins.mesh_relay_plugin import Plugin as MeshRelayPlugin
from plugins.ping_plugin import Plugin as PingPlugin
from plugins.telemetry_plugin import Plugin as TelemetryPlugin
from plugins.weather_plugin import Plugin as WeatherPlugin

from log_utils import get_logger

logger = get_logger(name="Plugins")
Expand All @@ -13,6 +6,14 @@


def load_plugins():
from plugins.health_plugin import Plugin as HealthPlugin
from plugins.map_plugin import Plugin as MapPlugin
from plugins.mesh_relay_plugin import Plugin as MeshRelayPlugin
from plugins.ping_plugin import Plugin as PingPlugin
from plugins.telemetry_plugin import Plugin as TelemetryPlugin
from plugins.weather_plugin import Plugin as WeatherPlugin
from plugins.help_plugin import Plugin as HelpPlugin

global plugins
if active_plugins:
return active_plugins
Expand All @@ -24,6 +25,7 @@ def load_plugins():
PingPlugin(),
TelemetryPlugin(),
WeatherPlugin(),
HelpPlugin(),
]

for plugin in plugins:
Expand Down
27 changes: 27 additions & 0 deletions plugins/base_plugin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import markdown
from abc import ABC, abstractmethod
from log_utils import get_logger
from config import relay_config
Expand All @@ -13,13 +14,39 @@ class BasePlugin(ABC):
plugin_name = None
max_data_rows_per_node = 100

@property
def description(self):
return f""

def __init__(self) -> None:
super().__init__()
self.logger = get_logger(f"Plugin:{self.plugin_name}")
self.config = {"active": False}
if "plugins" in relay_config and self.plugin_name in relay_config["plugins"]:
self.config = relay_config["plugins"][self.plugin_name]

def get_matrix_commands(self):
return [self.plugin_name]

async def send_matrix_message(self, room_id, message):
from matrix_utils import connect_matrix

matrix_client = await connect_matrix()

return await matrix_client.room_send(
room_id=room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": message,
"formatted_body": markdown.markdown(message),
},
)

def get_mesh_commands(self):
return []

def store_node_data(self, meshtastic_id, node_data):
data = self.get_node_data(meshtastic_id=meshtastic_id)
data = data[-self.max_data_rows_per_node :]
Expand Down
25 changes: 11 additions & 14 deletions plugins/health_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
class Plugin(BasePlugin):
plugin_name = "health"

@property
def description(self):
return "Show mesh health using avg battery, SNR, AirUtil"

def generate_response(self):
from meshtastic_utils import connect_meshtastic

Expand All @@ -30,11 +34,11 @@ def generate_response(self):
avg_snr = statistics.mean(snr) if snr else 0
mdn_snr = statistics.median(snr)

return f"""Nodes: {radios}
Battery: {avg_battery:.1f}% / {mdn_battery:.1f}% (avg / median)
Nodes with Low Battery (< 10): {low_battery}
Air Util: {avg_air:.2f} / {mdn_air:.2f} (avg / median)
SNR: {avg_snr:.2f} / {mdn_snr:.2f} (avg / median)
return f"""**Nodes**: {radios}
**Battery**: {avg_battery:.1f}% / {mdn_battery:.1f}% (avg / median)
**Nodes with Low Battery (< 10)**: {low_battery}
**Air Util**: {avg_air:.2f} / {mdn_air:.2f} (avg / median)
**SNR**: {avg_snr:.2f} / {mdn_snr:.2f} (avg / median)
"""

async def handle_meshtastic_message(
Expand All @@ -49,15 +53,8 @@ async def handle_room_message(self, room, event, full_message):
if not self.matches(full_message):
return False

matrix_client = await connect_matrix()

response = await matrix_client.room_send(
room_id=room.room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": self.generate_response(),
},
response = await self.send_matrix_message(
room.room_id, self.generate_response()
)

return True
51 changes: 51 additions & 0 deletions plugins/help_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import re

from plugins.base_plugin import BasePlugin
from plugin_loader import load_plugins


class Plugin(BasePlugin):
plugin_name = "help"

@property
def description(self):
return f"List supported relay commands"

async def handle_meshtastic_message(
self, packet, formatted_message, longname, meshnet_name
):
return False

def get_matrix_commands(self):
return [self.plugin_name]

def get_mesh_commands(self):
return []

async def handle_room_message(self, room, event, full_message):
full_message = full_message.strip()
if not self.matches(full_message):
return False

command = None

match = re.match(r"^.*: !help\s+(.+)$", full_message)
if match:
command = match.group(1)

plugins = load_plugins()

if command:
reply = f"No such command: {command}"

for plugin in plugins:
if command in plugin.get_matrix_commands():
reply = f"`!{command}`: {plugin.description}"
else:
commands = []
for plugin in plugins:
commands.extend(plugin.get_matrix_commands())
reply = "Available commands: " + ", ".join(commands)

response = await self.send_matrix_message(room.room_id, reply)
return True
12 changes: 12 additions & 0 deletions plugins/map_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,23 @@ async def send_image(client: AsyncClient, room_id: str, image: Image.Image):
class Plugin(BasePlugin):
plugin_name = "map"

@property
def description(self):
return (
f"Map of mesh radio nodes. Supports `zoom` and `size` options to customize"
)

async def handle_meshtastic_message(
self, packet, formatted_message, longname, meshnet_name
):
return False

def get_matrix_commands(self):
return [self.plugin_name]

def get_mesh_commands(self):
return []

async def handle_room_message(self, room, event, full_message):
full_message = full_message.strip()
if not self.matches(full_message):
Expand Down
6 changes: 6 additions & 0 deletions plugins/mesh_relay_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ def process(self, packet):

return packet

def get_matrix_commands(self):
return []

def get_mesh_commands(self):
return []

async def handle_meshtastic_message(
self, packet, formatted_message, longname, meshnet_name
):
Expand Down
22 changes: 11 additions & 11 deletions plugins/ping_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
class Plugin(BasePlugin):
plugin_name = "ping"

@property
def description(self):
return f"Check connectivity with the relay"

async def handle_meshtastic_message(
self, packet, formatted_message, longname, meshnet_name
):
Expand All @@ -26,20 +30,16 @@ async def handle_meshtastic_message(
meshtastic_client.sendText(text="pong!", destinationId=packet["fromId"])
return True

def get_matrix_commands(self):
return [self.plugin_name]

def get_mesh_commands(self):
return [self.plugin_name]

async def handle_room_message(self, room, event, full_message):
full_message = full_message.strip()
if not self.matches(full_message):
return False

from matrix_utils import connect_matrix

matrix_client = await connect_matrix()
response = await matrix_client.room_send(
room_id=room.room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": "pong!",
},
)
response = await self.send_matrix_message(room.room_id, "pong!")
return True
9 changes: 9 additions & 0 deletions plugins/telemetry_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class Plugin(BasePlugin):
def commands(self):
return ["batteryLevel", "voltage", "airUtilTx"]

def description(self):
return f"Graph of avg Mesh telemetry value for last 12 hours"

def _generate_timeperiods(self, hours=12):
# Calculate the start and end times
end_time = datetime.now()
Expand Down Expand Up @@ -56,6 +59,12 @@ async def handle_meshtastic_message(
self.set_node_data(meshtastic_id=packet["fromId"], node_data=telemetry_data)
return False

def get_matrix_commands(self):
return ["batteryLevel", "voltage", "airUtilTx"]

def get_mesh_commands(self):
return []

def matches(self, payload):
from matrix_utils import bot_command

Expand Down
10 changes: 10 additions & 0 deletions plugins/weather_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
class Plugin(BasePlugin):
plugin_name = "weather"

@property
def description(self):
return f"Show weather forecast for a radio node using GPS location"

def generate_forecast(self, latitude, longitude):
url = f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&hourly=temperature_2m,precipitation_probability,weathercode,cloudcover&forecast_days=1&current_weather=true"

Expand Down Expand Up @@ -101,5 +105,11 @@ async def handle_meshtastic_message(
)
return True

def get_matrix_commands(self):
return []

def get_mesh_commands(self):
return [self.plugin_name]

async def handle_room_message(self, room, event, full_message):
return False
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ meshtastic==2.1.6
py-staticmaps==0.4.0
matrix-nio==0.20.2
matplotlib==3.7.1
requests
requests==2.30.0
markdown==3.4.3

0 comments on commit 1efa58d

Please sign in to comment.