-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from geoffwhittington/feature/map_plugin
Map and Health plugins
- Loading branch information
Showing
6 changed files
with
234 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,7 @@ jobs: | |
- name: Build installer | ||
uses: nadeemjazmawe/[email protected] | ||
with: | ||
filepath: "./mmrelay.iss" | ||
filepath: "/DAppVersion=${{ github.ref_name }} ./mmrelay.iss" | ||
|
||
- name: Upload setup.exe to release | ||
uses: svenstaro/upload-release-action@v2 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from abc import ABC, abstractmethod | ||
|
||
|
||
class BasePlugin(ABC): | ||
def configure(self, matrix_client, meshtastic_client) -> None: | ||
self.matrix_client = matrix_client | ||
self.meshtastic_client = meshtastic_client | ||
|
||
@abstractmethod | ||
async def handle_meshtastic_message( | ||
packet, formatted_message, longname, meshnet_name | ||
): | ||
print("Base plugin: handling Meshtastic message") | ||
|
||
@abstractmethod | ||
async def handle_room_message(room, event, full_message): | ||
print("Base plugin: handling room message") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import re | ||
import statistics | ||
from base_plugin import BasePlugin | ||
|
||
|
||
class Plugin(BasePlugin): | ||
async def handle_meshtastic_message( | ||
self, packet, formatted_message, longname, meshnet_name | ||
): | ||
return | ||
|
||
async def handle_room_message(self, room, event, full_message): | ||
match = re.match(r"^.*: !health$", full_message) | ||
if match: | ||
battery_levels = [] | ||
air_util_tx = [] | ||
snr = [] | ||
|
||
for node, info in self.meshtastic_client.nodes.items(): | ||
if "deviceMetrics" in info: | ||
battery_levels.append(info["deviceMetrics"]["batteryLevel"]) | ||
air_util_tx.append(info["deviceMetrics"]["airUtilTx"]) | ||
if "snr" in info: | ||
snr.append(info["snr"]) | ||
|
||
low_battery = len([n for n in battery_levels if n <= 10]) | ||
radios = len(self.meshtastic_client.nodes) | ||
avg_battery = statistics.mean(battery_levels) if battery_levels else 0 | ||
mdn_battery = statistics.median(battery_levels) | ||
avg_air = statistics.mean(air_util_tx) if air_util_tx else 0 | ||
mdn_air = statistics.median(air_util_tx) | ||
avg_snr = statistics.mean(snr) if snr else 0 | ||
mdn_snr = statistics.median(snr) | ||
|
||
response = await self.matrix_client.room_send( | ||
room_id=room.room_id, | ||
message_type="m.room.message", | ||
content={ | ||
"msgtype": "m.text", | ||
"body": 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) | ||
""", | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import staticmaps | ||
import math | ||
import random | ||
import io | ||
import re | ||
from PIL import Image | ||
from nio import AsyncClient, UploadResponse | ||
from base_plugin import BasePlugin | ||
|
||
|
||
def anonymize_location(lat, lon, radius=1000): | ||
# Generate random offsets for latitude and longitude | ||
lat_offset = random.uniform(-radius / 111320, radius / 111320) | ||
lon_offset = random.uniform( | ||
-radius / (111320 * math.cos(lat)), radius / (111320 * math.cos(lat)) | ||
) | ||
|
||
# Apply the offsets to the location coordinates | ||
new_lat = lat + lat_offset | ||
new_lon = lon + lon_offset | ||
|
||
return new_lat, new_lon | ||
|
||
|
||
def get_map(locations, zoom=None, image_size=None, radius=10000): | ||
""" | ||
Anonymize a location to 10km by default | ||
""" | ||
context = staticmaps.Context() | ||
context.set_tile_provider(staticmaps.tile_provider_OSM) | ||
context.set_zoom(zoom) | ||
|
||
for location in locations: | ||
new_location = anonymize_location( | ||
lat=float(location["lat"]), | ||
lon=float(location["lon"]), | ||
radius=radius, | ||
) | ||
radio = staticmaps.create_latlng(new_location[0], new_location[1]) | ||
context.add_object(staticmaps.Marker(radio, size=10)) | ||
|
||
# render non-anti-aliased png | ||
if image_size: | ||
return context.render_pillow(image_size[0], image_size[1]) | ||
else: | ||
return context.render_pillow(1000, 1000) | ||
|
||
|
||
async def upload_image(client: AsyncClient, image: Image.Image) -> UploadResponse: | ||
buffer = io.BytesIO() | ||
image.save(buffer, format="PNG") | ||
image_data = buffer.getvalue() | ||
|
||
response, maybe_keys = await client.upload( | ||
io.BytesIO(image_data), | ||
content_type="image/png", | ||
filename="location.png", | ||
filesize=len(image_data), | ||
) | ||
|
||
return response | ||
|
||
|
||
async def send_room_image( | ||
client: AsyncClient, room_id: str, upload_response: UploadResponse | ||
): | ||
response = await client.room_send( | ||
room_id=room_id, | ||
message_type="m.room.message", | ||
content={"msgtype": "m.image", "url": upload_response.content_uri, "body": ""}, | ||
) | ||
|
||
|
||
async def send_image(client: AsyncClient, room_id: str, image: Image.Image): | ||
response = await upload_image(client=client, image=image) | ||
await send_room_image(client, room_id, upload_response=response) | ||
|
||
|
||
class Plugin(BasePlugin): | ||
async def handle_meshtastic_message( | ||
self, packet, formatted_message, longname, meshnet_name | ||
): | ||
return | ||
|
||
async def handle_room_message(self, room, event, full_message): | ||
pattern = r"^.*:(?: !map(?: zoom=(\d+))?(?: size=(\d+),(\d+))?)?$" | ||
match = re.match(pattern, full_message) | ||
if match: | ||
zoom = match.group(1) | ||
image_size = match.group(2, 3) | ||
|
||
try: | ||
zoom = int(zoom) | ||
except: | ||
zoom = 8 | ||
|
||
if zoom < 0 or zoom > 30: | ||
zoom = 8 | ||
|
||
try: | ||
image_size = (int(image_size[0]), int(image_size[1])) | ||
except: | ||
image_size = (1000, 1000) | ||
|
||
if image_size[0] > 1000 or image_size[1] > 1000: | ||
image_size = (1000, 1000) | ||
|
||
locations = [] | ||
for node, info in self.meshtastic_client.nodes.items(): | ||
if "position" in info and "latitude" in info["position"]: | ||
locations.append( | ||
{ | ||
"lat": info["position"]["latitude"], | ||
"lon": info["position"]["longitude"], | ||
} | ||
) | ||
|
||
pillow_image = get_map( | ||
locations=locations, zoom=zoom, image_size=image_size | ||
) | ||
|
||
await send_image(self.matrix_client, room.room_id, pillow_image) |