kopia lustrzana https://github.com/mate-dev/meshtastic-matrix-relay
Add Channel Utilization plugin
rodzic
c433baca76
commit
eae9dd965b
|
@ -2,6 +2,7 @@
|
|||
.vscode
|
||||
config.yaml
|
||||
custom_plugins/*
|
||||
plugins/.env
|
||||
meshtastic.sqlite
|
||||
__pycache__/
|
||||
./plugins/__pycache__/
|
||||
./plugins/__pycache__/
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
Ładowanie…
Reference in New Issue