Add more graphs.

This commit is contained in:
Jason Michalski
2024-07-29 21:43:15 -07:00
parent ddfd2550ac
commit 33879f3eda
5 changed files with 198 additions and 34 deletions

BIN
meshview/1x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View File

@@ -8,8 +8,8 @@
<script src="https://unpkg.com/htmx.org@1.9.11/dist/ext/sse.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
{% block head %}
{% endblock %}

View File

@@ -28,7 +28,7 @@
>
<div class="row">
<div class="col-md mb-3">
<div class="card text-white bg-primary" id="node_info">
<div class="card" id="node_info">
{% if node %}
<div class="card-header">
{{node.long_name}} ({{node.node_id|node_id_to_hex}})
@@ -42,12 +42,7 @@
<dt>role</dt>
<dd>{{node.role}}</dd>
</dl>
{% if has_telemetry %}
<a href="/graph/power/{{node_id}}"><img src="/graph/power/{{node_id}}" height="200em" width="200em"/></a>
{% endif %}
{% if neighbors %}
<a href="/graph/neighbors2/{{node_id}}"><img src="/graph/neighbors/{{node_id}}" height="200em" width="200em"/></a>
{% endif %}
{% include "node_graphs.html" %}
</div>
{% else %}
<div class="card-body">

View File

@@ -0,0 +1,39 @@
{% macro graph(name) %}
<a href="/graph/{{name}}/{{node_id}}"><img src="/graph/{{name}}/{{node_id}}" height="200em" width="200em"/></a>
{% endmacro %}
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab">Overview</button>
</li>
<li>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#neighbors" type="button" role="tab" >Neighbors</button>
</li>
<li>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#weather" type="button" role="tab" >Weather</button>
</li>
<li>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#power" type="button" role="tab" >Power</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="overview" role="tabpanel">
{{ graph("power") }}
{{ graph("chutil") }}
</div>
<div class="tab-pane fade" id="neighbors" role="tabpanel">
<a href="/graph/neighbors2/{{node_id}}"><img src="/graph/neighbors/{{node_id}}" height="200em" width="200em"/></a>
</div>
<div class="tab-pane fade" id="weather" role="tabpanel">
{{ graph("temperature") }}
{{ graph("humidity") }}
{{ graph("wind_speed") }}
{{ graph("wind_direction") }}
</div>
<div class="tab-pane fade" id="power" role="tabpanel">
{{ graph("power_metrics") }}
</div>
</div>

View File

@@ -6,6 +6,7 @@ import datetime
from aiohttp_sse import sse_response
import ssl
import re
import os
import pydot
from pandas import DataFrame
@@ -25,6 +26,11 @@ from meshview import models
from meshview import decode_payload
from meshview import notify
with open(os.path.join(os.path.dirname(__file__), '1x1.png'), 'rb') as png:
empty_png = png.read()
env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape())
@@ -399,7 +405,7 @@ async def packet_details(request):
packet = await store.get_packet(packet_id)
from_node_cord = None
if packet.from_node.last_lat:
if packet.from_node and packet.from_node.last_lat:
from_node_cord = [packet.from_node.last_lat * 1e-7 , packet.from_node.last_long * 1e-7]
uplinked_cord = []
@@ -470,44 +476,60 @@ async def packet(request):
)
@routes.get("/graph/power/{node_id}")
async def graph_power(request):
date = []
battery = []
voltage = []
for p in await store.get_packets_from(int(request.match_info['node_id']), PortNum.TELEMETRY_APP):
async def graph_telemetry(node_id, payload_type, graph_config):
data = {'date': []}
fields = []
for c in graph_config:
fields.extend(c['fields'])
for field in fields:
data[field] = []
for p in await store.get_packets_from(node_id, PortNum.TELEMETRY_APP):
_, payload = decode_payload.decode(p)
if not payload:
continue
if not payload.HasField('device_metrics'):
if not payload.HasField(payload_type):
continue
data_field = getattr(payload, payload_type)
timestamp = p.import_time
date.append(timestamp)
battery.append(payload.device_metrics.battery_level)
voltage.append(payload.device_metrics.voltage)
data['date'].append(timestamp)
for field in fields:
data[field].append(getattr(data_field, field))
if not date:
if not data['date']:
return web.Response(
body=empty_png,
status=404,
content_type="image/png",
)
max_time = datetime.timedelta(days=4)
newest = date[0]
for i, d in enumerate(date):
newest = data['date'][0]
for i, d in enumerate(data['date']):
if d < newest - max_time:
break
fig, ax1 = plt.subplots(figsize=(10, 10))
fig, ax = plt.subplots(figsize=(10, 10))
fig.autofmt_xdate()
ax1.set_xlabel('time')
ax1.set_ylabel('battery level', color='tab:blue')
ax2 = ax1.twinx()
ax2.set_ylabel('voltage', color='tab:red')
sns.lineplot(x=date[:i], y=battery[:i], ax=ax1, color='tab:blue')
sns.lineplot(x=date[:i], y=voltage[:i], ax=ax2, color='tab:red')
ax.set_xlabel('time')
axes = {0: ax}
date = data.pop('date')
df = DataFrame(data, index=date)
for i, ax_config in enumerate(graph_config):
args = {}
if 'color' in ax_config:
args['color'] = 'tab:' + ax_config['color']
if i:
ax = ax.twinx()
ax.set_ylabel(ax_config['label'], **args)
ax_df = df[ax_config['fields']]
args = {}
if 'palette' in ax_config:
args['palette'] = ax_config['palette']
sns.lineplot(data=ax_df, ax=ax, **args)
png = io.BytesIO()
plt.savefig(png, dpi=100)
@@ -519,6 +541,115 @@ async def graph_power(request):
)
@routes.get("/graph/power/{node_id}")
async def graph_power(request):
return await graph_telemetry(
int(request.match_info['node_id']),
'device_metrics',
[
{
'label': 'battery level',
'fields': ['battery_level'],
},
{
'label': 'voltage',
'fields': ['voltage'],
'palette': 'Set2',
},
],
)
@routes.get("/graph/chutil/{node_id}")
async def graph_chutil(request):
return await graph_telemetry(
int(request.match_info['node_id']),
'device_metrics',
[
{
'label': 'utilization',
'fields': ['channel_utilization', 'air_util_tx'],
},
],
)
@routes.get("/graph/wind_speed/{node_id}")
async def graph_wind_speed(request):
return await graph_telemetry(
int(request.match_info['node_id']),
'environment_metrics',
[
{
'label': 'wind speed m/s',
'fields': ['wind_speed'],
},
],
)
@routes.get("/graph/wind_direction/{node_id}")
async def graph_wind_direction(request):
return await graph_telemetry(
int(request.match_info['node_id']),
'environment_metrics',
[
{
'label': 'wind direction',
'fields': ['wind_direction'],
},
],
)
@routes.get("/graph/temperature/{node_id}")
async def graph_temperature(request):
return await graph_telemetry(
int(request.match_info['node_id']),
'environment_metrics',
[
{
'label': 'temperature C',
'fields': ['temperature'],
},
],
)
@routes.get("/graph/humidity/{node_id}")
async def graph_humidity(request):
return await graph_telemetry(
int(request.match_info['node_id']),
'environment_metrics',
[
{
'label': 'humidity',
'fields': ['relative_humidity'],
},
],
)
@routes.get("/graph/power_metrics/{node_id}")
async def graph_power_metrics(request):
return await graph_telemetry(
int(request.match_info['node_id']),
'power_metrics',
[
{
'label': 'voltage',
'fields': ['ch1_voltage', 'ch2_voltage', 'ch3_voltage'],
},
{
'label': 'current',
'fields': ['ch1_current', 'ch2_current', 'ch3_current'],
'palette': 'Set2',
},
],
)
@routes.get("/graph/neighbors/{node_id}")
async def graph_neighbors(request):
oldest = datetime.datetime.utcnow() - datetime.timedelta(days=4)
@@ -601,7 +732,6 @@ async def graph_neighbors2(request):
d['node_name'] = node_id_to_hex(node_id)
df = DataFrame(data)
print(df, flush=True)
fig = px.line(df, x="time", y="snr", color="node_name", markers=True)
html = fig.to_html(full_html=True, include_plotlyjs='cdn')
return web.Response(
@@ -874,7 +1004,7 @@ async def graph_network(request):
@routes.get("/net")
async def graph_net(request):
async def net(request):
if "date" in request.query:
start_date = datetime.date.fromisoformat(request.query["date"])
else: