HAOS Add-on v1.1.1: Sparkline-Graphen (letzte 5 Minuten)
- SVG-Sparkline pro Sensor-Karte, farbkodiert nach device_class - Backend: (timestamp, value) deque pro Sensor, API filtert auf 300s - Kein Datenverlust bei Neustart (In-Memory, reicht für Trendanzeige) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
name: Growatt ShineLAN-X
|
name: Growatt ShineLAN-X
|
||||||
version: "1.1.0"
|
version: "1.1.1"
|
||||||
slug: growatt_shinelan_x
|
slug: growatt_shinelan_x
|
||||||
description: Growatt Wechselrichter via ShineLAN-X (NuttX Modbus TCP) - MQTT Discovery + Web UI
|
description: Growatt Wechselrichter via ShineLAN-X (NuttX Modbus TCP) - MQTT Discovery + Web UI
|
||||||
url: https://gitea.bitfire.work/retr0/Growatt-Wechselrichter-HAOS
|
url: https://gitea.bitfire.work/retr0/Growatt-Wechselrichter-HAOS
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import deque
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from flask import Flask, jsonify, request, send_from_directory
|
from flask import Flask, jsonify, request, send_from_directory
|
||||||
@@ -112,6 +113,12 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
|
|||||||
d["last_update"] = time.time()
|
d["last_update"] = time.time()
|
||||||
d["modbus_ok"] = True
|
d["modbus_ok"] = True
|
||||||
d["poll_count"] = d.get("poll_count", 0) + 1
|
d["poll_count"] = d.get("poll_count", 0) + 1
|
||||||
|
# History: (timestamp, value) pro Sensor, maximal 5 Minuten
|
||||||
|
hist = d.setdefault("history", {})
|
||||||
|
now = time.time()
|
||||||
|
for sid, val in values.items():
|
||||||
|
q = hist.setdefault(sid, deque(maxlen=300))
|
||||||
|
q.append((now, val))
|
||||||
if _publisher:
|
if _publisher:
|
||||||
_publisher.publish_data(values, prefix)
|
_publisher.publish_data(values, prefix)
|
||||||
_publisher.publish_status("online", prefix)
|
_publisher.publish_status("online", prefix)
|
||||||
@@ -209,10 +216,17 @@ def api_get_data():
|
|||||||
model_id = inv_cfg.get("inverter_model", "MIC_1500_TL_X")
|
model_id = inv_cfg.get("inverter_model", "MIC_1500_TL_X")
|
||||||
inverter = INVERTERS.get(model_id, INVERTERS["MIC_1500_TL_X"])
|
inverter = INVERTERS.get(model_id, INVERTERS["MIC_1500_TL_X"])
|
||||||
d = State.inv_data.get(inv_id, {})
|
d = State.inv_data.get(inv_id, {})
|
||||||
|
cutoff = time.time() - 300 # letzte 5 Minuten
|
||||||
|
raw_hist = d.get("history", {})
|
||||||
|
history = {
|
||||||
|
sid: [v for (t, v) in q if t >= cutoff]
|
||||||
|
for sid, q in raw_hist.items()
|
||||||
|
}
|
||||||
result[inv_id] = {
|
result[inv_id] = {
|
||||||
"name": inv_cfg.get("name", inverter.name),
|
"name": inv_cfg.get("name", inverter.name),
|
||||||
"inverter_name": inverter.name,
|
"inverter_name": inverter.name,
|
||||||
"values": d.get("values", {}),
|
"values": d.get("values", {}),
|
||||||
|
"history": history,
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{"id": s.id, "name": s.name, "unit": s.unit,
|
{"id": s.id, "name": s.name, "unit": s.unit,
|
||||||
"icon": s.icon, "device_class": s.device_class}
|
"icon": s.icon, "device_class": s.device_class}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
.sensor-value { font-size: 20px; font-weight: 700;
|
.sensor-value { font-size: 20px; font-weight: 700;
|
||||||
font-variant-numeric: tabular-nums; }
|
font-variant-numeric: tabular-nums; }
|
||||||
.sensor-unit { font-size: 11px; color: var(--text-dim); margin-left: 2px; }
|
.sensor-unit { font-size: 11px; color: var(--text-dim); margin-left: 2px; }
|
||||||
|
.sparkline { margin-top: 8px; opacity: .7; }
|
||||||
.dc-power .sensor-value { color: var(--accent); }
|
.dc-power .sensor-value { color: var(--accent); }
|
||||||
.dc-voltage .sensor-value { color: var(--blue); }
|
.dc-voltage .sensor-value { color: var(--blue); }
|
||||||
.dc-current .sensor-value { color: var(--orange); }
|
.dc-current .sensor-value { color: var(--orange); }
|
||||||
@@ -220,6 +221,30 @@
|
|||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const DC_COLORS = {
|
||||||
|
power: '#f0c040', voltage: '#58a6ff', current: '#ffa657',
|
||||||
|
energy: '#3fb950', temperature: '#f85149', battery: '#bc8cff',
|
||||||
|
frequency: '#8b949e', default: '#8b949e',
|
||||||
|
};
|
||||||
|
|
||||||
|
function sparkline(values, dc) {
|
||||||
|
if (!values || values.length < 2) return '';
|
||||||
|
const color = DC_COLORS[dc] || DC_COLORS.default;
|
||||||
|
const min = Math.min(...values);
|
||||||
|
const max = Math.max(...values);
|
||||||
|
const range = max - min || 1;
|
||||||
|
const W = 138, H = 28, pad = 1;
|
||||||
|
const pts = values.map((v, i) => {
|
||||||
|
const x = pad + (i / (values.length - 1)) * (W - pad * 2);
|
||||||
|
const y = pad + (1 - (v - min) / range) * (H - pad * 2);
|
||||||
|
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||||
|
}).join(' ');
|
||||||
|
return `<svg class="sparkline" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||||
|
<polyline points="${pts}" fill="none" stroke="${color}"
|
||||||
|
stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
const ICON_MAP = {
|
const ICON_MAP = {
|
||||||
"mdi:solar-panel":"☀️","mdi:flash":"⚡","mdi:sine-wave":"〜",
|
"mdi:solar-panel":"☀️","mdi:flash":"⚡","mdi:sine-wave":"〜",
|
||||||
"mdi:solar-power":"🔆","mdi:thermometer":"🌡️","mdi:battery":"🔋",
|
"mdi:solar-power":"🔆","mdi:thermometer":"🌡️","mdi:battery":"🔋",
|
||||||
@@ -286,10 +311,12 @@ function renderLive(inverters) {
|
|||||||
const val = inv.values[s.id];
|
const val = inv.values[s.id];
|
||||||
const display = val !== undefined ? fmtVal(val) : "—";
|
const display = val !== undefined ? fmtVal(val) : "—";
|
||||||
const dcClass = s.device_class ? `dc-${s.device_class}` : "";
|
const dcClass = s.device_class ? `dc-${s.device_class}` : "";
|
||||||
|
const hist = (inv.history || {})[s.id] || [];
|
||||||
return `<div class="sensor-card ${dcClass}">
|
return `<div class="sensor-card ${dcClass}">
|
||||||
<div class="sensor-icon">${ICON_MAP[s.icon]||"📊"}</div>
|
<div class="sensor-icon">${ICON_MAP[s.icon]||"📊"}</div>
|
||||||
<div class="sensor-name">${s.name}</div>
|
<div class="sensor-name">${s.name}</div>
|
||||||
<div class="sensor-value">${display}<span class="sensor-unit">${s.unit}</span></div>
|
<div class="sensor-value">${display}<span class="sensor-unit">${s.unit}</span></div>
|
||||||
|
${sparkline(hist, s.device_class)}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
return `<div class="inv-section">
|
return `<div class="inv-section">
|
||||||
|
|||||||
Reference in New Issue
Block a user