mmrelay 1.0__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.
- mmrelay/__init__.py +9 -0
- mmrelay/cli.py +384 -0
- mmrelay/config.py +218 -0
- mmrelay/config_checker.py +133 -0
- mmrelay/db_utils.py +309 -0
- mmrelay/log_utils.py +107 -0
- mmrelay/main.py +281 -0
- mmrelay/matrix_utils.py +754 -0
- mmrelay/meshtastic_utils.py +569 -0
- mmrelay/plugin_loader.py +336 -0
- mmrelay/plugins/__init__.py +3 -0
- mmrelay/plugins/base_plugin.py +212 -0
- mmrelay/plugins/debug_plugin.py +17 -0
- mmrelay/plugins/drop_plugin.py +120 -0
- mmrelay/plugins/health_plugin.py +64 -0
- mmrelay/plugins/help_plugin.py +55 -0
- mmrelay/plugins/map_plugin.py +323 -0
- mmrelay/plugins/mesh_relay_plugin.py +134 -0
- mmrelay/plugins/nodes_plugin.py +92 -0
- mmrelay/plugins/ping_plugin.py +118 -0
- mmrelay/plugins/telemetry_plugin.py +179 -0
- mmrelay/plugins/weather_plugin.py +208 -0
- mmrelay/setup_utils.py +263 -0
- mmrelay-1.0.dist-info/METADATA +160 -0
- mmrelay-1.0.dist-info/RECORD +29 -0
- mmrelay-1.0.dist-info/WHEEL +5 -0
- mmrelay-1.0.dist-info/entry_points.txt +2 -0
- mmrelay-1.0.dist-info/licenses/LICENSE +21 -0
- mmrelay-1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import re
|
|
3
|
+
import string
|
|
4
|
+
|
|
5
|
+
from meshtastic.mesh_interface import BROADCAST_NUM
|
|
6
|
+
|
|
7
|
+
from mmrelay.plugins.base_plugin import BasePlugin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def match_case(source, target):
|
|
11
|
+
return "".join(
|
|
12
|
+
c.upper() if s.isupper() else c.lower() for s, c in zip(source, target)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Plugin(BasePlugin):
|
|
17
|
+
plugin_name = "ping"
|
|
18
|
+
punctuation = string.punctuation
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.plugin_name = "ping"
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def description(self):
|
|
26
|
+
return "Check connectivity with the relay or respond to pings over the mesh"
|
|
27
|
+
|
|
28
|
+
async def handle_meshtastic_message(
|
|
29
|
+
self, packet, formatted_message, longname, meshnet_name
|
|
30
|
+
):
|
|
31
|
+
if "decoded" in packet and "text" in packet["decoded"]:
|
|
32
|
+
message = packet["decoded"]["text"].strip()
|
|
33
|
+
channel = packet.get("channel", 0) # Default to channel 0 if not provided
|
|
34
|
+
|
|
35
|
+
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
36
|
+
|
|
37
|
+
meshtastic_client = connect_meshtastic()
|
|
38
|
+
|
|
39
|
+
# Determine if the message is a direct message
|
|
40
|
+
toId = packet.get("to")
|
|
41
|
+
myId = meshtastic_client.myInfo.my_node_num # Get relay's own node number
|
|
42
|
+
|
|
43
|
+
if toId == myId:
|
|
44
|
+
# Direct message to us
|
|
45
|
+
is_direct_message = True
|
|
46
|
+
elif toId == BROADCAST_NUM:
|
|
47
|
+
is_direct_message = False
|
|
48
|
+
else:
|
|
49
|
+
# Message to someone else; we may ignore it
|
|
50
|
+
is_direct_message = False
|
|
51
|
+
|
|
52
|
+
# Pass is_direct_message to is_channel_enabled
|
|
53
|
+
if not self.is_channel_enabled(
|
|
54
|
+
channel, is_direct_message=is_direct_message
|
|
55
|
+
):
|
|
56
|
+
# Removed unnecessary logging
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
# Updated regex to match optional punctuation before and after "ping"
|
|
60
|
+
match = re.search(
|
|
61
|
+
r"(?<!\w)([!?]*)(ping)([!?]*)(?!\w)", message, re.IGNORECASE
|
|
62
|
+
)
|
|
63
|
+
if match:
|
|
64
|
+
# Log that the plugin is processing the message
|
|
65
|
+
self.logger.info(
|
|
66
|
+
f"Processing message from {longname} on channel {channel} with plugin '{self.plugin_name}'"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Extract matched text and punctuation
|
|
70
|
+
pre_punc = match.group(1)
|
|
71
|
+
matched_text = match.group(2)
|
|
72
|
+
post_punc = match.group(3)
|
|
73
|
+
|
|
74
|
+
total_punc_length = len(pre_punc) + len(post_punc)
|
|
75
|
+
|
|
76
|
+
# Define the base response
|
|
77
|
+
base_response = "pong"
|
|
78
|
+
|
|
79
|
+
# Adjust base_response to match the case pattern of matched_text
|
|
80
|
+
base_response = match_case(matched_text, base_response)
|
|
81
|
+
|
|
82
|
+
# Construct the reply message
|
|
83
|
+
if total_punc_length > 5:
|
|
84
|
+
reply_message = "Pong..."
|
|
85
|
+
else:
|
|
86
|
+
reply_message = pre_punc + base_response + post_punc
|
|
87
|
+
|
|
88
|
+
# Wait for the response delay
|
|
89
|
+
await asyncio.sleep(self.get_response_delay())
|
|
90
|
+
|
|
91
|
+
fromId = packet.get("fromId")
|
|
92
|
+
|
|
93
|
+
if is_direct_message:
|
|
94
|
+
# Send reply as DM
|
|
95
|
+
meshtastic_client.sendText(
|
|
96
|
+
text=reply_message,
|
|
97
|
+
destinationId=fromId,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
# Send the reply back to the same channel
|
|
101
|
+
meshtastic_client.sendText(text=reply_message, channelIndex=channel)
|
|
102
|
+
return True
|
|
103
|
+
else:
|
|
104
|
+
return False # No match, do not process
|
|
105
|
+
|
|
106
|
+
def get_matrix_commands(self):
|
|
107
|
+
return [self.plugin_name]
|
|
108
|
+
|
|
109
|
+
def get_mesh_commands(self):
|
|
110
|
+
return [self.plugin_name]
|
|
111
|
+
|
|
112
|
+
async def handle_room_message(self, room, event, full_message):
|
|
113
|
+
# Pass the event to matches()
|
|
114
|
+
if not self.matches(event):
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
await self.send_matrix_message(room.room_id, "pong!")
|
|
118
|
+
return True
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
from mmrelay.plugins.base_plugin import BasePlugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Plugin(BasePlugin):
|
|
13
|
+
plugin_name = "telemetry"
|
|
14
|
+
max_data_rows_per_node = 50
|
|
15
|
+
|
|
16
|
+
def commands(self):
|
|
17
|
+
return ["batteryLevel", "voltage", "airUtilTx"]
|
|
18
|
+
|
|
19
|
+
def description(self):
|
|
20
|
+
return "Graph of avg Mesh telemetry value for last 12 hours"
|
|
21
|
+
|
|
22
|
+
def _generate_timeperiods(self, hours=12):
|
|
23
|
+
# Calculate the start and end times
|
|
24
|
+
end_time = datetime.now()
|
|
25
|
+
start_time = end_time - timedelta(hours=hours)
|
|
26
|
+
|
|
27
|
+
# Create a list of hourly intervals for the last 12 hours
|
|
28
|
+
hourly_intervals = []
|
|
29
|
+
current_time = start_time
|
|
30
|
+
while current_time <= end_time:
|
|
31
|
+
hourly_intervals.append(current_time)
|
|
32
|
+
current_time += timedelta(hours=1)
|
|
33
|
+
return hourly_intervals
|
|
34
|
+
|
|
35
|
+
async def handle_meshtastic_message(
|
|
36
|
+
self, packet, formatted_message, longname, meshnet_name
|
|
37
|
+
):
|
|
38
|
+
# Support deviceMetrics only for now
|
|
39
|
+
if (
|
|
40
|
+
"decoded" in packet
|
|
41
|
+
and "portnum" in packet["decoded"]
|
|
42
|
+
and packet["decoded"]["portnum"] == "TELEMETRY_APP"
|
|
43
|
+
and "telemetry" in packet["decoded"]
|
|
44
|
+
and "deviceMetrics" in packet["decoded"]["telemetry"]
|
|
45
|
+
):
|
|
46
|
+
telemetry_data = []
|
|
47
|
+
data = self.get_node_data(meshtastic_id=packet["fromId"])
|
|
48
|
+
if data:
|
|
49
|
+
telemetry_data = data
|
|
50
|
+
packet_data = packet["decoded"]["telemetry"]
|
|
51
|
+
|
|
52
|
+
telemetry_data.append(
|
|
53
|
+
{
|
|
54
|
+
"time": packet_data["time"],
|
|
55
|
+
"batteryLevel": (
|
|
56
|
+
packet_data["deviceMetrics"]["batteryLevel"]
|
|
57
|
+
if "batteryLevel" in packet_data["deviceMetrics"]
|
|
58
|
+
else 0
|
|
59
|
+
),
|
|
60
|
+
"voltage": (
|
|
61
|
+
packet_data["deviceMetrics"]["voltage"]
|
|
62
|
+
if "voltage" in packet_data["deviceMetrics"]
|
|
63
|
+
else 0
|
|
64
|
+
),
|
|
65
|
+
"airUtilTx": (
|
|
66
|
+
packet_data["deviceMetrics"]["airUtilTx"]
|
|
67
|
+
if "airUtilTx" in packet_data["deviceMetrics"]
|
|
68
|
+
else 0
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
self.set_node_data(meshtastic_id=packet["fromId"], node_data=telemetry_data)
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def get_matrix_commands(self):
|
|
76
|
+
return ["batteryLevel", "voltage", "airUtilTx"]
|
|
77
|
+
|
|
78
|
+
def get_mesh_commands(self):
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
def matches(self, event):
|
|
82
|
+
from mmrelay.matrix_utils import bot_command
|
|
83
|
+
|
|
84
|
+
# Use bot_command() to check if any of the commands match
|
|
85
|
+
for command in self.get_matrix_commands():
|
|
86
|
+
if bot_command(command, event):
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
async def handle_room_message(self, room, event, full_message):
|
|
91
|
+
# Pass the event to matches()
|
|
92
|
+
if not self.matches(event):
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
match = re.search(
|
|
96
|
+
r":\s+!(batteryLevel|voltage|airUtilTx)(?:\s+(.+))?$", full_message
|
|
97
|
+
)
|
|
98
|
+
if not match:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
telemetry_option = match.group(1)
|
|
102
|
+
node = match.group(2)
|
|
103
|
+
|
|
104
|
+
hourly_intervals = self._generate_timeperiods()
|
|
105
|
+
from mmrelay.matrix_utils import connect_matrix
|
|
106
|
+
|
|
107
|
+
matrix_client = await connect_matrix()
|
|
108
|
+
|
|
109
|
+
# Compute the hourly averages for each node
|
|
110
|
+
hourly_averages = {}
|
|
111
|
+
|
|
112
|
+
def calculate_averages(node_data_rows):
|
|
113
|
+
for record in node_data_rows:
|
|
114
|
+
record_time = datetime.fromtimestamp(
|
|
115
|
+
record["time"]
|
|
116
|
+
) # Replace with your timestamp field name
|
|
117
|
+
telemetry_value = record[
|
|
118
|
+
telemetry_option
|
|
119
|
+
] # Replace with your battery level field name
|
|
120
|
+
for i in range(len(hourly_intervals) - 1):
|
|
121
|
+
if hourly_intervals[i] <= record_time < hourly_intervals[i + 1]:
|
|
122
|
+
if i not in hourly_averages:
|
|
123
|
+
hourly_averages[i] = []
|
|
124
|
+
hourly_averages[i].append(telemetry_value)
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
if node:
|
|
128
|
+
node_data_rows = self.get_node_data(node)
|
|
129
|
+
calculate_averages(node_data_rows)
|
|
130
|
+
else:
|
|
131
|
+
for node_data_json in self.get_data():
|
|
132
|
+
node_data_rows = json.loads(node_data_json[0])
|
|
133
|
+
calculate_averages(node_data_rows)
|
|
134
|
+
|
|
135
|
+
# Compute the final hourly averages
|
|
136
|
+
final_averages = {}
|
|
137
|
+
for i, interval in enumerate(hourly_intervals[:-1]):
|
|
138
|
+
if i in hourly_averages:
|
|
139
|
+
final_averages[interval] = sum(hourly_averages[i]) / len(
|
|
140
|
+
hourly_averages[i]
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
final_averages[interval] = 0.0
|
|
144
|
+
|
|
145
|
+
# Extract the hourly intervals and average values into separate lists
|
|
146
|
+
hourly_intervals = list(final_averages.keys())
|
|
147
|
+
average_values = list(final_averages.values())
|
|
148
|
+
|
|
149
|
+
# Convert the hourly intervals to strings
|
|
150
|
+
hourly_strings = [hour.strftime("%H") for hour in hourly_intervals]
|
|
151
|
+
|
|
152
|
+
# Create the plot
|
|
153
|
+
fig, ax = plt.subplots()
|
|
154
|
+
ax.plot(hourly_strings, average_values)
|
|
155
|
+
|
|
156
|
+
# Set the plot title and axis labels
|
|
157
|
+
if node:
|
|
158
|
+
title = f"{node} Hourly {telemetry_option} Averages"
|
|
159
|
+
else:
|
|
160
|
+
title = f"Network Hourly {telemetry_option} Averages"
|
|
161
|
+
ax.set_title(title)
|
|
162
|
+
ax.set_xlabel("Hour")
|
|
163
|
+
ax.set_ylabel(f"{telemetry_option}")
|
|
164
|
+
|
|
165
|
+
# Rotate the x-axis labels for readability
|
|
166
|
+
plt.xticks(rotation=45)
|
|
167
|
+
|
|
168
|
+
# Save the plot as a PIL image
|
|
169
|
+
buf = io.BytesIO()
|
|
170
|
+
fig.canvas.print_png(buf)
|
|
171
|
+
buf.seek(0)
|
|
172
|
+
img = Image.open(buf)
|
|
173
|
+
pil_image = Image.frombytes(mode="RGBA", size=img.size, data=img.tobytes())
|
|
174
|
+
|
|
175
|
+
from mmrelay.matrix_utils import send_room_image, upload_image
|
|
176
|
+
|
|
177
|
+
upload_response = await upload_image(matrix_client, pil_image, "graph.png")
|
|
178
|
+
await send_room_image(matrix_client, room.room_id, upload_response)
|
|
179
|
+
return True
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from meshtastic.mesh_interface import BROADCAST_NUM
|
|
5
|
+
|
|
6
|
+
from mmrelay.plugins.base_plugin import BasePlugin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Plugin(BasePlugin):
|
|
10
|
+
plugin_name = "weather"
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.plugin_name = "weather"
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def description(self):
|
|
18
|
+
return "Show weather forecast for a radio node using GPS location"
|
|
19
|
+
|
|
20
|
+
def generate_forecast(self, latitude, longitude):
|
|
21
|
+
units = self.config.get("units", "metric") # Default to metric
|
|
22
|
+
temperature_unit = "°C" if units == "metric" else "°F"
|
|
23
|
+
|
|
24
|
+
url = (
|
|
25
|
+
f"https://api.open-meteo.com/v1/forecast?"
|
|
26
|
+
f"latitude={latitude}&longitude={longitude}&"
|
|
27
|
+
f"hourly=temperature_2m,precipitation_probability,weathercode,cloudcover&"
|
|
28
|
+
f"forecast_days=1¤t_weather=true"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
response = requests.get(url, timeout=10)
|
|
33
|
+
data = response.json()
|
|
34
|
+
|
|
35
|
+
# Extract relevant weather data
|
|
36
|
+
current_temp = data["current_weather"]["temperature"]
|
|
37
|
+
current_weather_code = data["current_weather"]["weathercode"]
|
|
38
|
+
is_day = data["current_weather"]["is_day"]
|
|
39
|
+
|
|
40
|
+
# Get indices for +2h and +5h forecasts
|
|
41
|
+
# Assuming hourly data starts from current hour
|
|
42
|
+
forecast_2h_index = 2
|
|
43
|
+
forecast_5h_index = 5
|
|
44
|
+
|
|
45
|
+
forecast_2h_temp = data["hourly"]["temperature_2m"][forecast_2h_index]
|
|
46
|
+
forecast_2h_precipitation = data["hourly"]["precipitation_probability"][
|
|
47
|
+
forecast_2h_index
|
|
48
|
+
]
|
|
49
|
+
forecast_2h_weather_code = data["hourly"]["weathercode"][forecast_2h_index]
|
|
50
|
+
|
|
51
|
+
forecast_5h_temp = data["hourly"]["temperature_2m"][forecast_5h_index]
|
|
52
|
+
forecast_5h_precipitation = data["hourly"]["precipitation_probability"][
|
|
53
|
+
forecast_5h_index
|
|
54
|
+
]
|
|
55
|
+
forecast_5h_weather_code = data["hourly"]["weathercode"][forecast_5h_index]
|
|
56
|
+
|
|
57
|
+
if units == "imperial":
|
|
58
|
+
# Convert temperatures from Celsius to Fahrenheit
|
|
59
|
+
current_temp = current_temp * 9 / 5 + 32
|
|
60
|
+
forecast_2h_temp = forecast_2h_temp * 9 / 5 + 32
|
|
61
|
+
forecast_5h_temp = forecast_5h_temp * 9 / 5 + 32
|
|
62
|
+
|
|
63
|
+
current_temp = round(current_temp, 1)
|
|
64
|
+
forecast_2h_temp = round(forecast_2h_temp, 1)
|
|
65
|
+
forecast_5h_temp = round(forecast_5h_temp, 1)
|
|
66
|
+
|
|
67
|
+
def weather_code_to_text(weather_code, is_day):
|
|
68
|
+
weather_mapping = {
|
|
69
|
+
0: "☀️ Clear sky" if is_day else "🌙 Clear sky",
|
|
70
|
+
1: "🌤️ Mainly clear" if is_day else "🌙🌤️ Mainly clear",
|
|
71
|
+
2: "⛅️ Partly cloudy" if is_day else "🌙⛅️ Partly cloudy",
|
|
72
|
+
3: "☁️ Overcast" if is_day else "🌙☁️ Overcast",
|
|
73
|
+
45: "🌫️ Fog" if is_day else "🌙🌫️ Fog",
|
|
74
|
+
48: (
|
|
75
|
+
"🌫️ Depositing rime fog" if is_day else "🌙🌫️ Depositing rime fog"
|
|
76
|
+
),
|
|
77
|
+
51: "🌧️ Light drizzle",
|
|
78
|
+
53: "🌧️ Moderate drizzle",
|
|
79
|
+
55: "🌧️ Dense drizzle",
|
|
80
|
+
56: "🌧️ Light freezing drizzle",
|
|
81
|
+
57: "🌧️ Dense freezing drizzle",
|
|
82
|
+
61: "🌧️ Light rain",
|
|
83
|
+
63: "🌧️ Moderate rain",
|
|
84
|
+
65: "🌧️ Heavy rain",
|
|
85
|
+
66: "🌧️ Light freezing rain",
|
|
86
|
+
67: "🌧️ Heavy freezing rain",
|
|
87
|
+
71: "❄️ Light snow fall",
|
|
88
|
+
73: "❄️ Moderate snow fall",
|
|
89
|
+
75: "❄️ Heavy snow fall",
|
|
90
|
+
77: "❄️ Snow grains",
|
|
91
|
+
80: "🌧️ Light rain showers",
|
|
92
|
+
81: "🌧️ Moderate rain showers",
|
|
93
|
+
82: "🌧️ Violent rain showers",
|
|
94
|
+
85: "❄️ Light snow showers",
|
|
95
|
+
86: "❄️ Heavy snow showers",
|
|
96
|
+
95: "⛈️ Thunderstorm",
|
|
97
|
+
96: "⛈️ Thunderstorm with slight hail",
|
|
98
|
+
99: "⛈️ Thunderstorm with heavy hail",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return weather_mapping.get(weather_code, "❓ Unknown")
|
|
102
|
+
|
|
103
|
+
# Generate one-line weather forecast
|
|
104
|
+
forecast = (
|
|
105
|
+
f"Now: {weather_code_to_text(current_weather_code, is_day)} - "
|
|
106
|
+
f"{current_temp}{temperature_unit} | "
|
|
107
|
+
)
|
|
108
|
+
forecast += (
|
|
109
|
+
f"+2h: {weather_code_to_text(forecast_2h_weather_code, is_day)} - "
|
|
110
|
+
f"{forecast_2h_temp}{temperature_unit} {forecast_2h_precipitation}% | "
|
|
111
|
+
)
|
|
112
|
+
forecast += (
|
|
113
|
+
f"+5h: {weather_code_to_text(forecast_5h_weather_code, is_day)} - "
|
|
114
|
+
f"{forecast_5h_temp}{temperature_unit} {forecast_5h_precipitation}%"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return forecast
|
|
118
|
+
|
|
119
|
+
except requests.exceptions.RequestException as e:
|
|
120
|
+
self.logger.error(f"Error fetching weather data: {e}")
|
|
121
|
+
return "Error fetching weather data."
|
|
122
|
+
|
|
123
|
+
async def handle_meshtastic_message(
|
|
124
|
+
self, packet, formatted_message, longname, meshnet_name
|
|
125
|
+
):
|
|
126
|
+
if (
|
|
127
|
+
"decoded" in packet
|
|
128
|
+
and "portnum" in packet["decoded"]
|
|
129
|
+
and packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP"
|
|
130
|
+
and "text" in packet["decoded"]
|
|
131
|
+
):
|
|
132
|
+
message = packet["decoded"]["text"].strip()
|
|
133
|
+
channel = packet.get("channel", 0) # Default to channel 0 if not provided
|
|
134
|
+
|
|
135
|
+
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
136
|
+
|
|
137
|
+
meshtastic_client = connect_meshtastic()
|
|
138
|
+
|
|
139
|
+
# Determine if the message is a direct message
|
|
140
|
+
toId = packet.get("to")
|
|
141
|
+
myId = meshtastic_client.myInfo.my_node_num # Get relay's own node number
|
|
142
|
+
|
|
143
|
+
if toId == myId:
|
|
144
|
+
# Direct message to us
|
|
145
|
+
is_direct_message = True
|
|
146
|
+
elif toId == BROADCAST_NUM:
|
|
147
|
+
is_direct_message = False
|
|
148
|
+
else:
|
|
149
|
+
# Message to someone else; we may ignore it
|
|
150
|
+
is_direct_message = False
|
|
151
|
+
|
|
152
|
+
# Pass is_direct_message to is_channel_enabled
|
|
153
|
+
if not self.is_channel_enabled(
|
|
154
|
+
channel, is_direct_message=is_direct_message
|
|
155
|
+
):
|
|
156
|
+
# Channel not enabled for plugin
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
if f"!{self.plugin_name}" not in message.lower():
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
# Log that the plugin is processing the message
|
|
163
|
+
self.logger.info(
|
|
164
|
+
f"Processing message from {longname} on channel {channel} with plugin '{self.plugin_name}'"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
fromId = packet.get("fromId")
|
|
168
|
+
if fromId in meshtastic_client.nodes:
|
|
169
|
+
weather_notice = "Cannot determine location"
|
|
170
|
+
requesting_node = meshtastic_client.nodes.get(fromId)
|
|
171
|
+
if (
|
|
172
|
+
requesting_node
|
|
173
|
+
and "position" in requesting_node
|
|
174
|
+
and "latitude" in requesting_node["position"]
|
|
175
|
+
and "longitude" in requesting_node["position"]
|
|
176
|
+
):
|
|
177
|
+
weather_notice = self.generate_forecast(
|
|
178
|
+
latitude=requesting_node["position"]["latitude"],
|
|
179
|
+
longitude=requesting_node["position"]["longitude"],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Wait for the response delay
|
|
183
|
+
await asyncio.sleep(self.get_response_delay())
|
|
184
|
+
|
|
185
|
+
if is_direct_message:
|
|
186
|
+
# Respond via DM
|
|
187
|
+
meshtastic_client.sendText(
|
|
188
|
+
text=weather_notice,
|
|
189
|
+
destinationId=fromId,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
# Respond in the same channel (broadcast)
|
|
193
|
+
meshtastic_client.sendText(
|
|
194
|
+
text=weather_notice,
|
|
195
|
+
channelIndex=channel,
|
|
196
|
+
)
|
|
197
|
+
return True
|
|
198
|
+
else:
|
|
199
|
+
return False # Not a text message or port does not match
|
|
200
|
+
|
|
201
|
+
def get_matrix_commands(self):
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
def get_mesh_commands(self):
|
|
205
|
+
return [self.plugin_name]
|
|
206
|
+
|
|
207
|
+
async def handle_room_message(self, room, event, full_message):
|
|
208
|
+
return False # Not handling Matrix messages in this plugin
|