Feature: Aggregat-Gerät + Energie-Dashboard Sensoren (v1.2.0)

- main.py: AGG_SENSOR_IDS/AGGREGATE_META — Mapping sensor_id → Aggregat-Bucket
  _compute_aggregates() summiert alle online Geräte nach jedem Poll
  /api/data liefert jetzt auch "aggregates" Schlüssel
- mqtt_publisher.py: publish_aggregates() + _publish_aggregate_discovery()
  Eigenes HA-Gerät "ShineBridge Gesamt" (device_id: shinebridge_aggregate)
  MQTT Topic: shinebridge/aggregate/state
- index.html: renderAggregates() — "Gesamt"-Sektion oben im Live-Tab

Aggregierte Sensoren (alle kompatibel mit HA Energie-Dashboard):
  PV: total_pv_power, total_ac_power, total_energy_today, total_energy_total
  Netz (SDM-630): grid_power, grid_import_kwh, grid_export_kwh
  Batterie (SPH): bat_charge/discharge_power/total, bat_soc (Ø)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-04-26 21:40:01 +02:00
parent 33c6a15644
commit 456bfb34d6
4 changed files with 167 additions and 22 deletions
+41 -3
View File
@@ -225,6 +225,21 @@ function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
const AGG_META = {
total_pv_power: {name:"PV Gesamtleistung", unit:"W", device_class:"power", icon:"mdi:solar-power"},
total_ac_power: {name:"AC Gesamtleistung", unit:"W", device_class:"power", icon:"mdi:flash"},
total_energy_today: {name:"Energie Heute Gesamt", unit:"kWh", device_class:"energy", icon:"mdi:solar-power"},
total_energy_total: {name:"Energie Gesamt", unit:"kWh", device_class:"energy", icon:"mdi:solar-power"},
grid_power: {name:"Netzleistung", unit:"W", device_class:"power", icon:"mdi:transmission-tower"},
grid_import_kwh: {name:"Netzbezug Gesamt", unit:"kWh", device_class:"energy", icon:"mdi:transmission-tower-import"},
grid_export_kwh: {name:"Einspeisung Gesamt", unit:"kWh", device_class:"energy", icon:"mdi:transmission-tower-export"},
bat_charge_power: {name:"Batterie Ladeleistung Ges.", unit:"W", device_class:"power", icon:"mdi:battery-plus"},
bat_discharge_power: {name:"Batterie Entladeleist. Ges.", unit:"W", device_class:"power", icon:"mdi:battery-minus"},
bat_charge_total: {name:"Batterie Ladung Gesamt", unit:"kWh", device_class:"energy", icon:"mdi:battery-plus"},
bat_discharge_total: {name:"Batterie Entladung Gesamt", unit:"kWh", device_class:"energy", icon:"mdi:battery-minus"},
bat_soc: {name:"Batterie Ladezustand Ø", unit:"%", device_class:"battery", icon:"mdi:battery"},
};
const DC_COLORS = {
power: '#f0c040', voltage: '#58a6ff', current: '#ffa657',
energy: '#3fb950', temperature: '#f85149', battery: '#bc8cff',
@@ -297,19 +312,42 @@ async function refreshData() {
const keys = Object.keys(d.inverters || {});
document.getElementById("subtitle").textContent =
keys.length ? `${keys.length} Gerät${keys.length !== 1 ? "e" : ""}` : "Keine Geräte";
renderLive(d.inverters || {});
renderLive(d.inverters || {}, d.aggregates || {});
} catch(e) {
document.getElementById("pill-mqtt").className = "pill err";
}
}
function renderLive(inverters) {
function renderAggregates(aggregates) {
if (!aggregates || !Object.keys(aggregates).length) return '';
const cards = Object.entries(AGG_META).map(([id, meta]) => {
const val = aggregates[id];
if (val === undefined) return '';
const dcClass = meta.device_class ? `dc-${meta.device_class}` : '';
return `<div class="sensor-card ${dcClass}">
<div class="sensor-icon">${ICON_MAP[meta.icon]||"📊"}</div>
<div class="sensor-name">${esc(meta.name)}</div>
<div class="sensor-value">${fmtVal(val)}<span class="sensor-unit">${esc(meta.unit)}</span></div>
</div>`;
}).filter(Boolean).join('');
if (!cards) return '';
return `<div class="inv-section">
<div class="inv-header">
<div class="inv-title">Gesamt</div>
<div class="inv-badge ok">aggregiert</div>
</div>
<div class="sensor-grid">${cards}</div>
</div>`;
}
function renderLive(inverters, aggregates) {
const el = document.getElementById("live-content");
if (!Object.keys(inverters).length) {
el.innerHTML = '<div class="no-data">Keine Geräte konfiguriert.<br>Bitte im Tab „Geräte" hinzufügen.</div>';
return;
}
el.innerHTML = Object.values(inverters).map(inv => {
const aggHtml = renderAggregates(aggregates);
el.innerHTML = aggHtml + Object.values(inverters).map(inv => {
const ago = inv.last_update ? Math.round(Date.now()/1000 - inv.last_update) + "s" : "—";
const cards = (inv.sensors || []).map(s => {
const val = inv.values[s.id];