Merge branch 'geoffwhittington:main' into main

pull/1/head
Jeremiah K 2023-05-18 13:28:13 -05:00 zatwierdzone przez GitHub
commit d0a2083090
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
13 zmienionych plików z 237 dodań i 38 usunięć

Wyświetl plik

@ -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/).
@ -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.

Wyświetl plik

@ -22,6 +22,7 @@ from log_utils import get_logger
from meshtastic_utils import (
connect_meshtastic,
on_meshtastic_message,
on_lost_meshtastic_connection,
logger as meshtastic_logger,
)
@ -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 ...")
matrix_client.add_event_callback(

Wyświetl plik

@ -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")
@ -13,6 +6,15 @@ active_plugins = []
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
from plugins.nodes_plugin import Plugin as NodesPlugin
global plugins
if active_plugins:
return active_plugins
@ -24,6 +26,8 @@ def load_plugins():
PingPlugin(),
TelemetryPlugin(),
WeatherPlugin(),
HelpPlugin(),
NodesPlugin(),
]
for plugin in plugins:

Wyświetl plik

@ -1,3 +1,4 @@
import markdown
from abc import ABC, abstractmethod
from log_utils import get_logger
from config import relay_config
@ -13,6 +14,10 @@ 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}")
@ -20,6 +25,28 @@ class BasePlugin(ABC):
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, formatted=True):
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" if formatted else None,
"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 :]

Wyświetl plik

@ -6,6 +6,10 @@ from plugins.base_plugin import BasePlugin
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
@ -30,11 +34,11 @@ class Plugin(BasePlugin):
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(
@ -49,15 +53,8 @@ SNR: {avg_snr:.2f} / {mdn_snr:.2f} (avg / median)
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

Wyświetl plik

@ -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

Wyświetl plik

@ -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):

Wyświetl plik

@ -52,6 +52,12 @@ class Plugin(BasePlugin):
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
):

Wyświetl plik

@ -0,0 +1,77 @@
import re
import statistics
from plugins.base_plugin import BasePlugin
from datetime import datetime
def get_relative_time(timestamp):
now = datetime.now()
dt = datetime.fromtimestamp(timestamp)
# Calculate the time difference between the current time and the given timestamp
delta = now - dt
# Extract the relevant components from the time difference
days = delta.days
seconds = delta.seconds
# Convert the time difference into a relative timeframe
if days > 7:
return dt.strftime(
"%b %d, %Y"
) # Return the timestamp in a specific format if it's older than 7 days
elif days >= 1:
return f"{days} days ago"
elif seconds >= 3600:
hours = seconds // 3600
return f"{hours} hours ago"
elif seconds >= 60:
minutes = seconds // 60
return f"{minutes} minutes ago"
else:
return "Just now"
class Plugin(BasePlugin):
plugin_name = "nodes"
@property
def description(self):
return """Show mesh radios and node data
$shortname $longname / $devicemodel / $battery $voltage / $snr / $lastseen
"""
def generate_response(self):
from meshtastic_utils import connect_meshtastic
meshtastic_client = connect_meshtastic()
response = f"Nodes: {len(meshtastic_client.nodes)}\n"
for node, info in meshtastic_client.nodes.items():
if "snr" in info:
snr = f"{info['snr']} dB"
else:
snr = ""
response += f"{info['user']['shortName']} {info['user']['longName']} / {info['user']['hwModel']} / {info['deviceMetrics']['batteryLevel']}% {info['deviceMetrics']['voltage']}V / {snr} / {get_relative_time(info['lastHeard'])}\n"
return response
async def handle_meshtastic_message(
self, packet, formatted_message, longname, meshnet_name
):
return False
async def handle_room_message(self, room, event, full_message):
from matrix_utils import connect_matrix
full_message = full_message.strip()
if not self.matches(full_message):
return False
response = await self.send_matrix_message(
room_id=room.room_id, message=self.generate_response(), formatted=False
)
return True

Wyświetl plik

@ -6,6 +6,10 @@ from plugins.base_plugin import BasePlugin
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
):
@ -26,20 +30,16 @@ class Plugin(BasePlugin):
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

Wyświetl plik

@ -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()
@ -56,6 +59,12 @@ class Plugin(BasePlugin):
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

Wyświetl plik

@ -7,6 +7,10 @@ from plugins.base_plugin import BasePlugin
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"
@ -101,5 +105,11 @@ class Plugin(BasePlugin):
)
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

Wyświetl plik

@ -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