Add Channel Utilization plugin

feature/plugins-parameters
mate71pl 2024-07-14 17:33:19 +02:00
rodzic c433baca76
commit eae9dd965b
7 zmienionych plików z 328 dodań i 14 usunięć

3
.gitignore vendored
Wyświetl plik

@ -2,6 +2,7 @@
.vscode
config.yaml
custom_plugins/*
plugins/.env
meshtastic.sqlite
__pycache__/
./plugins/__pycache__/
./plugins/__pycache__/

44
command_wrapper.py 100644
Wyświetl plik

@ -0,0 +1,44 @@
import os
import subprocess
import time
output_file = '/home/mesh/app/command_output.txt'
flag_file = '/home/mesh/app/.commands_executed'
# Remove the output file if it exists
if os.path.exists(output_file):
os.remove(output_file)
def log_to_file(message):
with open(output_file, 'a') as f:
f.write(message + "\n")
def execute_meshtastic_command(options):
"""Execute a meshtastic command with the given options."""
command = ["meshtastic", "--host", "mmrelaydevice", "--port", "4403"] + options.split()
log_to_file(f"Executing command: {' '.join(command)}")
result = subprocess.run(command, capture_output=True, text=True)
log_to_file("Standard Output:\n" + result.stdout)
log_to_file("Standard Error:\n" + result.stderr)
time.sleep(1) # Pause for 1 second between commands
if os.path.exists(flag_file):
log_to_file("Commands have already been executed previously. Skipping.")
else:
# Print all environment variables at the start
log_to_file("All environment variables:\n" + str(os.environ))
# Loop through environment variables in sequence
index = 1
while True:
command = os.environ.get(f'MESHTASTIC_COMMAND_{index}')
if command:
log_to_file(f"Found command variable: MESHTASTIC_COMMAND_{index} with value: {command}")
execute_meshtastic_command(command)
index += 1
else:
log_to_file(f"No more MESHTASTIC_COMMAND variables found, ending at index {index-1}.")
# Create the flag file to indicate that all commands have been executed.
with open(flag_file, 'w') as f:
f.write("Commands executed on: " + time.ctime())
break

62
conf_wrapper.py 100644
Wyświetl plik

@ -0,0 +1,62 @@
import os
import yaml
# Read environment variables and construct the configuration dictionary
relay_config = {
"matrix": {
"homeserver": os.environ.get('MATRIX_HOMESERVER'),
"access_token": os.environ.get('MATRIX_ACCESS_TOKEN'),
"bot_user_id": os.environ.get('MATRIX_BOT_USER_ID')
},
"meshtastic": {
"connection_type": os.environ.get('MESHTASTIC_CONNECTION_TYPE'),
"serial_port": os.environ.get('MESHTASTIC_SERIAL_PORT'),
"host": os.environ.get('MESHTASTIC_HOST'),
"meshnet_name": os.environ.get('MESHTASTIC_MESHNET_NAME'),
"broadcast_enabled": os.environ.get('MESHTASTIC_BROADCAST_ENABLED') == 'true'
},
"logging": {
"level": os.environ.get('LOGGING_LEVEL')
}
}
# Construct the matrix_rooms list based on environment variables
matrix_rooms = []
for i in range(1, 9): # Loop for 8 rooms
room_id = os.environ.get(f'MATRIX_ROOMS_ID_{i}')
meshtastic_channel = os.environ.get(f'MATRIX_ROOMS_MESHTASTIC_CHANNEL_{i}')
if room_id and meshtastic_channel is not None:
matrix_rooms.append({
"id": room_id,
"meshtastic_channel": int(meshtastic_channel)
})
# Add the matrix_rooms list to the relay_config dictionary
relay_config["matrix_rooms"] = matrix_rooms
# Construct the plugins dictionary based on environment variables
plugins_config = {}
health_plugin_active = os.environ.get('HEALTH_PLUGIN_ACTIVE')
if health_plugin_active:
plugins_config["health"] = {"active": health_plugin_active.lower() == "true"}
map_plugin_active = os.environ.get('MAP_PLUGIN_ACTIVE')
if map_plugin_active:
plugins_config["map"] = {"active": map_plugin_active.lower() == "true"}
nodes_plugin_active = os.environ.get('NODES_PLUGIN_ACTIVE')
if nodes_plugin_active:
plugins_config["nodes"] = {"active": nodes_plugin_active.lower() == "true"}
chutilz_plugin_active = os.environ.get('CHUTILZ_PLUGIN_ACTIVE')
if nodes_plugin_active:
plugins_config["chutilz"] = {"active": chutilz_plugin_active.lower() == "true"}
# Add the plugins dictionary to the relay_config if it's not empty
if plugins_config:
relay_config["plugins"] = plugins_config
# Write the configuration to config.yaml
with open("config.yaml", "w") as f:
yaml.dump(relay_config, f)

Wyświetl plik

@ -16,6 +16,7 @@ def load_plugins():
from plugins.nodes_plugin import Plugin as NodesPlugin
from plugins.drop_plugin import Plugin as DropPlugin
from plugins.debug_plugin import Plugin as DebugPlugin
from plugins.chutilz_plugin import Plugin as ChutilzPlugin
global sorted_active_plugins
if sorted_active_plugins:
@ -32,6 +33,7 @@ def load_plugins():
NodesPlugin(),
DropPlugin(),
DebugPlugin(),
ChutilzPlugin(),
]
active_plugins = []
@ -47,3 +49,4 @@ def load_plugins():
sorted_active_plugins = sorted(active_plugins, key=lambda plugin: plugin.priority)
return sorted_active_plugins

Wyświetl plik

@ -0,0 +1,122 @@
import time
import io
import os
import requests
from PIL import Image
from nio import AsyncClient, UploadResponse
from plugins.base_plugin import BasePlugin
def load_env_variable(key):
env_path = os.path.join(os.path.dirname(__file__), '.env')
with open(env_path) as f:
for line in f:
if line.strip().startswith(key):
return line.strip().split('=')[1].strip().strip('"')
return None
class Plugin(BasePlugin):
plugin_name = "chutilz"
@property
def description(self):
return "Generates and returns Channels utilization."
async def get_image_url(self):
base_url = "http://172.18.0.1:3000/render/d-solo/fdr9ym4rdfhmoa/loramesh"
org_id = "1"
panel_id = "3"
width = "1000"
height = "500"
scale = "1"
tz = "Europe/Warsaw"
to_time = int(time.time() * 1000)
from_time = to_time - 24 * 60 * 60 * 1000
url = (
f"{base_url}?orgId={org_id}&from={from_time}&to={to_time}&"
f"panelId={panel_id}&width={width}&height={height}&scale={scale}&tz={tz}"
)
return url
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
from matrix_utils import connect_matrix
matrix_client = await connect_matrix()
url = await self.get_image_url()
token = load_env_variable('GRAFANA_API_KEY')
headers = {
"Authorization": f"Bearer {token}"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
self.logger.info("Image successfully fetched from Grafana")
except requests.exceptions.RequestException as e:
self.logger.error(f"Failed to fetch image: {e}")
return False
try:
image = Image.open(io.BytesIO(response.content))
await self.send_image(matrix_client, room.room_id, image)
self.logger.info("Image successfully sent to room")
except Exception as e:
self.logger.error(f"Failed to process or send image: {e}")
return False
return True
async def upload_image(self, client: AsyncClient, image: Image.Image) -> UploadResponse:
try:
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="graph.png",
filesize=len(image_data),
)
self.logger.info("Image successfully uploaded to Matrix")
return response
except Exception as e:
self.logger.error(f"Failed to upload image: {e}")
raise
async def send_room_image(self, client: AsyncClient, room_id: str, upload_response: UploadResponse):
try:
await client.room_send(
room_id=room_id,
message_type="m.room.message",
content={"msgtype": "m.image", "url": upload_response.content_uri, "body": ""},
)
self.logger.info("Image successfully sent to room")
except Exception as e:
self.logger.error(f"Failed to send image to room: {e}")
raise
async def send_image(self, client: AsyncClient, room_id: str, image: Image.Image):
try:
response = await self.upload_image(client=client, image=image)
await self.send_room_image(client, room_id, upload_response=response)
except Exception as e:
self.logger.error(f"Failed to send image: {e}")
raise

Wyświetl plik

@ -6,17 +6,12 @@ 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
return dt.strftime("%b %d, %Y")
elif days >= 1:
return f"{days} days ago"
elif seconds >= 3600:
@ -42,8 +37,8 @@ $shortname $longname / $devicemodel / $battery $voltage / $snr / $lastseen
from meshtastic_utils import connect_meshtastic
meshtastic_client = connect_meshtastic()
response = f"Nodes: {len(meshtastic_client.nodes)}\n"
response = f">**Nodes: {len(meshtastic_client.nodes)}**\n\n"
for node, info in meshtastic_client.nodes.items():
snr = ""
@ -54,15 +49,18 @@ $shortname $longname / $devicemodel / $battery $voltage / $snr / $lastseen
if "lastHeard" in info and info["lastHeard"] is not None:
last_heard = get_relative_time(info["lastHeard"])
voltage = "?V"
battery = "?%"
voltage = ""
battery = ""
if "deviceMetrics" in info:
if "voltage" in info["deviceMetrics"] and info["deviceMetrics"]["voltage"] is not None:
voltage = f"{info['deviceMetrics']['voltage']}V "
if "batteryLevel" in info["deviceMetrics"] and info["deviceMetrics"]["batteryLevel"] is not None:
battery = f"{info['deviceMetrics']['batteryLevel']}% "
response += f"{info['user']['shortName']} {info['user']['longName']} / {info['user']['hwModel']} / {battery} {voltage} / {snr} / {last_heard}\n"
response += f"><hr/>\n\n"\
f">**[{info['user']['shortName']} - {info['user']['longName']}]**\n"\
f">{info['user']['hwModel']} {battery}{voltage}\n"\
f">{snr}{last_heard}\n\n"
return response
@ -77,7 +75,8 @@ $shortname $longname / $devicemodel / $battery $voltage / $snr / $lastseen
return False
response = await self.send_matrix_message(
room_id=room.room_id, message=self.generate_response(), formatted=False
room_id=room.room_id, message=self.generate_response(), formatted=True
)
return True

Wyświetl plik

@ -0,0 +1,83 @@
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():
snr = ""
if "snr" in info and info['snr'] is not None:
snr = f"{info['snr']} dB "
last_heard = None
if "lastHeard" in info and info["lastHeard"] is not None:
last_heard = get_relative_time(info["lastHeard"])
voltage = "?V"
battery = "?%"
if "deviceMetrics" in info:
if "voltage" in info["deviceMetrics"] and info["deviceMetrics"]["voltage"] is not None:
voltage = f"{info['deviceMetrics']['voltage']}V "
if "batteryLevel" in info["deviceMetrics"] and info["deviceMetrics"]["batteryLevel"] is not None:
battery = f"{info['deviceMetrics']['batteryLevel']}% "
response += f"{info['user']['shortName']} {info['user']['longName']} / {info['user']['hwModel']} / {battery} {voltage} / {snr} / {last_heard}\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