kopia lustrzana https://github.com/mate-dev/meshtastic-matrix-relay
Add Channel Utilization plugin
rodzic
c433baca76
commit
eae9dd965b
|
@ -2,6 +2,7 @@
|
||||||
.vscode
|
.vscode
|
||||||
config.yaml
|
config.yaml
|
||||||
custom_plugins/*
|
custom_plugins/*
|
||||||
|
plugins/.env
|
||||||
meshtastic.sqlite
|
meshtastic.sqlite
|
||||||
__pycache__/
|
__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.nodes_plugin import Plugin as NodesPlugin
|
||||||
from plugins.drop_plugin import Plugin as DropPlugin
|
from plugins.drop_plugin import Plugin as DropPlugin
|
||||||
from plugins.debug_plugin import Plugin as DebugPlugin
|
from plugins.debug_plugin import Plugin as DebugPlugin
|
||||||
|
from plugins.chutilz_plugin import Plugin as ChutilzPlugin
|
||||||
|
|
||||||
global sorted_active_plugins
|
global sorted_active_plugins
|
||||||
if sorted_active_plugins:
|
if sorted_active_plugins:
|
||||||
|
@ -32,6 +33,7 @@ def load_plugins():
|
||||||
NodesPlugin(),
|
NodesPlugin(),
|
||||||
DropPlugin(),
|
DropPlugin(),
|
||||||
DebugPlugin(),
|
DebugPlugin(),
|
||||||
|
ChutilzPlugin(),
|
||||||
]
|
]
|
||||||
|
|
||||||
active_plugins = []
|
active_plugins = []
|
||||||
|
@ -47,3 +49,4 @@ def load_plugins():
|
||||||
|
|
||||||
sorted_active_plugins = sorted(active_plugins, key=lambda plugin: plugin.priority)
|
sorted_active_plugins = sorted(active_plugins, key=lambda plugin: plugin.priority)
|
||||||
return sorted_active_plugins
|
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):
|
def get_relative_time(timestamp):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
dt = datetime.fromtimestamp(timestamp)
|
dt = datetime.fromtimestamp(timestamp)
|
||||||
|
|
||||||
# Calculate the time difference between the current time and the given timestamp
|
|
||||||
delta = now - dt
|
delta = now - dt
|
||||||
|
|
||||||
# Extract the relevant components from the time difference
|
|
||||||
days = delta.days
|
days = delta.days
|
||||||
seconds = delta.seconds
|
seconds = delta.seconds
|
||||||
|
|
||||||
# Convert the time difference into a relative timeframe
|
|
||||||
if days > 7:
|
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:
|
elif days >= 1:
|
||||||
return f"{days} days ago"
|
return f"{days} days ago"
|
||||||
elif seconds >= 3600:
|
elif seconds >= 3600:
|
||||||
|
@ -43,7 +38,7 @@ $shortname $longname / $devicemodel / $battery $voltage / $snr / $lastseen
|
||||||
|
|
||||||
meshtastic_client = 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():
|
for node, info in meshtastic_client.nodes.items():
|
||||||
snr = ""
|
snr = ""
|
||||||
|
@ -54,15 +49,18 @@ $shortname $longname / $devicemodel / $battery $voltage / $snr / $lastseen
|
||||||
if "lastHeard" in info and info["lastHeard"] is not None:
|
if "lastHeard" in info and info["lastHeard"] is not None:
|
||||||
last_heard = get_relative_time(info["lastHeard"])
|
last_heard = get_relative_time(info["lastHeard"])
|
||||||
|
|
||||||
voltage = "?V"
|
voltage = ""
|
||||||
battery = "?%"
|
battery = ""
|
||||||
if "deviceMetrics" in info:
|
if "deviceMetrics" in info:
|
||||||
if "voltage" in info["deviceMetrics"] and info["deviceMetrics"]["voltage"] is not None:
|
if "voltage" in info["deviceMetrics"] and info["deviceMetrics"]["voltage"] is not None:
|
||||||
voltage = f"{info['deviceMetrics']['voltage']}V "
|
voltage = f"{info['deviceMetrics']['voltage']}V "
|
||||||
if "batteryLevel" in info["deviceMetrics"] and info["deviceMetrics"]["batteryLevel"] is not None:
|
if "batteryLevel" in info["deviceMetrics"] and info["deviceMetrics"]["batteryLevel"] is not None:
|
||||||
battery = f"{info['deviceMetrics']['batteryLevel']}% "
|
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
|
return response
|
||||||
|
|
||||||
|
@ -77,7 +75,8 @@ $shortname $longname / $devicemodel / $battery $voltage / $snr / $lastseen
|
||||||
return False
|
return False
|
||||||
|
|
||||||
response = await self.send_matrix_message(
|
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
|
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