From 48a6f34ceed916fc6ffbb264b0d042adf9ac3a6b Mon Sep 17 00:00:00 2001 From: Geoff Whittington Date: Fri, 12 May 2023 09:43:57 -0400 Subject: [PATCH] New weather plugin, improvements to ping and telemetry --- db_utils.py | 10 ++++ plugins/base_plugin.py | 21 +++++++- plugins/ping.py | 19 ++++++- plugins/telemetry.py | 2 +- plugins/weather_plugin.py | 105 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 plugins/weather_plugin.py diff --git a/db_utils.py b/db_utils.py index 9ebfa97..4c475c2 100644 --- a/db_utils.py +++ b/db_utils.py @@ -25,6 +25,16 @@ def store_plugin_data(plugin_name, meshtastic_id, data): conn.commit() +def delete_plugin_data(plugin_name, meshtastic_id): + with sqlite3.connect("meshtastic.sqlite") as conn: + cursor = conn.cursor() + cursor.execute( + "DELETE FROM plugin_data WHERE plugin_name=? AND meshtastic_id=?", + (plugin_name, meshtastic_id), + ) + conn.commit() + + # Get the data for a given plugin and Meshtastic ID def get_plugin_data_for_node(plugin_name, meshtastic_id): with sqlite3.connect("meshtastic.sqlite") as conn: diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py index 2beefe2..13f2223 100644 --- a/plugins/base_plugin.py +++ b/plugins/base_plugin.py @@ -1,7 +1,12 @@ from abc import ABC, abstractmethod from log_utils import get_logger from config import relay_config -from db_utils import store_plugin_data, get_plugin_data, get_plugin_data_for_node +from db_utils import ( + store_plugin_data, + get_plugin_data, + get_plugin_data_for_node, + delete_plugin_data, +) from matrix_utils import bot_command @@ -16,10 +21,22 @@ def __init__(self) -> None: if "plugins" in relay_config and self.plugin_name in relay_config["plugins"]: self.config = relay_config["plugins"][self.plugin_name] - def store_node_data(self, meshtastic_id, data): + 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 :] + if type(node_data) is list: + data.extend(node_data) + else: + data.append(node_data) store_plugin_data(self.plugin_name, meshtastic_id, data) + def set_node_data(self, meshtastic_id, node_data): + node_data = node_data[-self.max_data_rows_per_node :] + store_plugin_data(self.plugin_name, meshtastic_id, node_data) + + def delete_node_data(self, meshtastic_id): + return delete_plugin_data(self.plugin_name, meshtastic_id) + def get_node_data(self, meshtastic_id): return get_plugin_data_for_node(self.plugin_name, meshtastic_id) diff --git a/plugins/ping.py b/plugins/ping.py index d2fd30d..d469561 100644 --- a/plugins/ping.py +++ b/plugins/ping.py @@ -2,6 +2,7 @@ from plugins.base_plugin import BasePlugin from matrix_utils import connect_matrix +from meshtastic_utils import connect_meshtastic class Plugin(BasePlugin): @@ -10,12 +11,25 @@ class Plugin(BasePlugin): async def handle_meshtastic_message( self, packet, formatted_message, longname, meshnet_name ): - pass + if ( + "decoded" in packet + and "portnum" in packet["decoded"] + and packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP" + and "text" in packet["decoded"] + ): + message = packet["decoded"]["text"] + message = message.strip() + if f"!{self.plugin_name}" not in message: + return + + meshtastic_client = connect_meshtastic() + meshtastic_client.sendText(text="pong!", destinationId=packet["fromId"]) + return True async def handle_room_message(self, room, event, full_message): full_message = full_message.strip() if not self.matches(full_message): - return + return False matrix_client = await connect_matrix() response = await matrix_client.room_send( @@ -26,3 +40,4 @@ async def handle_room_message(self, room, event, full_message): "body": "pong!", }, ) + return True diff --git a/plugins/telemetry.py b/plugins/telemetry.py index 82a48e4..2f2bcf3 100644 --- a/plugins/telemetry.py +++ b/plugins/telemetry.py @@ -51,7 +51,7 @@ async def handle_meshtastic_message( "airUtilTx": packet_data["deviceMetrics"]["airUtilTx"], } ) - self.store_node_data(meshtastic_id=packet["fromId"], data=telemetry_data) + self.set_node_data(meshtastic_id=packet["fromId"], node_data=telemetry_data) return False def matches(self, payload): diff --git a/plugins/weather_plugin.py b/plugins/weather_plugin.py new file mode 100644 index 0000000..0e93b34 --- /dev/null +++ b/plugins/weather_plugin.py @@ -0,0 +1,105 @@ +import re +import requests + +from plugins.base_plugin import BasePlugin +from matrix_utils import connect_matrix +from meshtastic_utils import connect_meshtastic + + +class Plugin(BasePlugin): + plugin_name = "weather" + + 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¤t_weather=true" + + try: + response = requests.get(url) + data = response.json() + + # Extract relevant weather data + current_temp = data["current_weather"]["temperature"] + current_weather_code = data["current_weather"]["weathercode"] + is_day = data["current_weather"]["is_day"] + + forecast_2h_temp = data["hourly"]["temperature_2m"][2] + forecast_2h_precipitation = data["hourly"]["precipitation_probability"][2] + forecast_2h_weather_code = data["hourly"]["weathercode"][2] + + forecast_5h_temp = data["hourly"]["temperature_2m"][5] + forecast_5h_precipitation = data["hourly"]["precipitation_probability"][5] + forecast_5h_weather_code = data["hourly"]["weathercode"][5] + + def weather_code_to_text(weather_code, is_day): + weather_mapping = { + 0: "☀️ Sunny" if is_day else "🌙 Clear", + 1: "⛅️ Partly Cloudy" if is_day else "🌙⛅️ Clear", + 2: "🌤️ Mostly Clear" if is_day else "🌙🌤️ Mostly Clear", + 3: "🌥️ Mostly Cloudy" if is_day else "🌙🌥️ Mostly Clear", + 4: "☁️ Cloudy" if is_day else "🌙☁️ Cloudy", + 5: "🌧️ Rainy" if is_day else "🌙🌧️ Rainy", + 6: "⛈️ Thunderstorm" if is_day else "🌙⛈️ Thunderstorm", + 7: "❄️ Snowy" if is_day else "🌙❄️ Snowy", + 8: "🌧️❄️ Wintry Mix" if is_day else "🌙🌧️❄️ Wintry Mix", + 9: "🌫️ Foggy" if is_day else "🌙🌫️ Foggy", + 10: "💨 Windy" if is_day else "🌙💨 Windy", + 11: "🌧️☈️ Stormy/Hail" if is_day else "🌙🌧️☈️ Stormy/Hail", + 12: "🌫️ Foggy" if is_day else "🌙🌫️ Foggy", + 13: "🌫️ Foggy" if is_day else "🌙🌫️ Foggy", + 14: "🌫️ Foggy" if is_day else "🌙🌫️ Foggy", + 15: "🌋 Volcanic Ash" if is_day else "🌙🌋 Volcanic Ash", + 16: "🌧️ Rainy" if is_day else "🌙🌧️ Rainy", + 17: "🌫️ Foggy" if is_day else "🌙🌫️ Foggy", + 18: "🌪️ Tornado" if is_day else "🌙🌪️ Tornado", + } + + return weather_mapping.get(weather_code, "❓ Unknown") + + # Generate one-line weather forecast + forecast = f"Now: {weather_code_to_text(current_weather_code, is_day)} - {current_temp}°C | " + forecast += f"+2h: {weather_code_to_text(forecast_2h_weather_code, is_day)} - {forecast_2h_temp}°C {forecast_2h_precipitation}% | " + forecast += f"+5h: {weather_code_to_text(forecast_5h_weather_code, is_day)} - {forecast_5h_temp}°C {forecast_5h_precipitation}%" + + return forecast + + except requests.exceptions.RequestException as e: + print(f"Error: {e}") + return None + + async def handle_meshtastic_message( + self, packet, formatted_message, longname, meshnet_name + ): + if ( + "decoded" in packet + and "portnum" in packet["decoded"] + and packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP" + and "text" in packet["decoded"] + ): + message = packet["decoded"]["text"] + message = message.strip() + + if f"!{self.plugin_name}" not in message: + return False + + meshtastic_client = connect_meshtastic() + if packet["fromId"] in meshtastic_client.nodes: + weather_notice = "Cannot determine location" + requesting_node = meshtastic_client.nodes.get(packet["fromId"]) + if ( + requesting_node + and "position" in requesting_node + and "latitude" in requesting_node["position"] + and "longitude" in requesting_node["position"] + ): + weather_notice = self.generate_forecast( + latitude=requesting_node["position"]["latitude"], + longitude=requesting_node["position"]["longitude"], + ) + + meshtastic_client.sendText( + text=weather_notice, + destinationId=packet["fromId"], + ) + return True + + async def handle_room_message(self, room, event, full_message): + return False