Feature: Eastron SDM-630 Support + Float32 Decode (v1.1.5)
- inverters.py: data_type Feld in Sensor (uint16/uint32/float32) + SDM-630 Definition (16 Sensoren: U/I/P L1-L3, PF, Freq, kWh) + read_ranges: [(0, 76)] — alle Sensoren in einem Batch - modbus_client.py: Float32 IEEE 754 Decode via struct.unpack (SDM-630 liefert Floats, Growatt liefert skalierte Integer) - index.html: "Wechselrichter" → "Gerät" — Add-on unterstützt jetzt beliebige Modbus-Geräte, nicht nur Wechselrichter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
name: ShineBridge
|
name: ShineBridge
|
||||||
version: "1.1.4"
|
version: "1.1.5"
|
||||||
slug: shinebridge
|
slug: shinebridge
|
||||||
description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI
|
description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI
|
||||||
url: https://gitea.bitfire.work/retr0/Growatt-Wechselrichter-HAOS
|
url: https://gitea.bitfire.work/retr0/Growatt-Wechselrichter-HAOS
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ class Sensor:
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
reg: int
|
reg: int
|
||||||
count: int # 1 = uint16, 2 = uint32 (high word first)
|
count: int # number of 16-bit registers (1 or 2)
|
||||||
scale: float
|
scale: float
|
||||||
unit: str
|
unit: str
|
||||||
device_class: Optional[str]
|
device_class: Optional[str]
|
||||||
state_class: str
|
state_class: str
|
||||||
icon: str
|
icon: str
|
||||||
|
data_type: str = "uint16" # "uint16", "uint32", "float32"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -95,6 +96,28 @@ def _mod_sensors() -> List[Sensor]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _sdm630_sensors() -> List[Sensor]:
|
||||||
|
f = "float32"
|
||||||
|
return [
|
||||||
|
Sensor("voltage_l1", "Spannung L1", 0, 2, 1.0, "V", "voltage", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("voltage_l2", "Spannung L2", 2, 2, 1.0, "V", "voltage", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("voltage_l3", "Spannung L3", 4, 2, 1.0, "V", "voltage", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("current_l1", "Strom L1", 6, 2, 1.0, "A", "current", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("current_l2", "Strom L2", 8, 2, 1.0, "A", "current", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("current_l3", "Strom L3", 10, 2, 1.0, "A", "current", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("power_l1", "Wirkleistung L1", 12, 2, 1.0, "W", "power", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("power_l2", "Wirkleistung L2", 14, 2, 1.0, "W", "power", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("power_l3", "Wirkleistung L3", 16, 2, 1.0, "W", "power", "measurement", "mdi:flash", f),
|
||||||
|
Sensor("power_factor_l1", "Leistungsfaktor L1", 30, 2, 1.0, "", None, "measurement", "mdi:sine-wave", f),
|
||||||
|
Sensor("power_factor_l2", "Leistungsfaktor L2", 32, 2, 1.0, "", None, "measurement", "mdi:sine-wave", f),
|
||||||
|
Sensor("power_factor_l3", "Leistungsfaktor L3", 34, 2, 1.0, "", None, "measurement", "mdi:sine-wave", f),
|
||||||
|
Sensor("total_power", "Gesamtwirkleistung", 48, 2, 1.0, "W", "power", "measurement", "mdi:transmission-tower", f),
|
||||||
|
Sensor("frequency", "Frequenz", 70, 2, 1.0, "Hz", "frequency", "measurement", "mdi:sine-wave", f),
|
||||||
|
Sensor("import_kwh", "Bezug Gesamt", 72, 2, 1.0, "kWh", "energy", "total_increasing", "mdi:transmission-tower-import", f),
|
||||||
|
Sensor("export_kwh", "Einspeisung Gesamt", 74, 2, 1.0, "kWh", "energy", "total_increasing", "mdi:transmission-tower-export", f),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
INVERTERS = {
|
INVERTERS = {
|
||||||
"MIC_1500_TL_X": Inverter(
|
"MIC_1500_TL_X": Inverter(
|
||||||
id="MIC_1500_TL_X",
|
id="MIC_1500_TL_X",
|
||||||
@@ -124,4 +147,11 @@ INVERTERS = {
|
|||||||
sensors=_mod_sensors(),
|
sensors=_mod_sensors(),
|
||||||
read_ranges=[(3, 91), (93, 1)],
|
read_ranges=[(3, 91), (93, 1)],
|
||||||
),
|
),
|
||||||
|
"SDM_630": Inverter(
|
||||||
|
id="SDM_630",
|
||||||
|
name="Eastron SDM-630",
|
||||||
|
manufacturer="Eastron",
|
||||||
|
sensors=_sdm630_sensors(),
|
||||||
|
read_ranges=[(0, 76)], # regs 0-75, alle 16 Sensoren
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import struct
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
@@ -66,12 +67,18 @@ def _extract_sensors(sensors: list, regs: Dict[int, int]) -> Dict[str, float]:
|
|||||||
if s.reg not in regs:
|
if s.reg not in regs:
|
||||||
log.warning("Register %d fehlt in Antwort (%s)", s.reg, s.id)
|
log.warning("Register %d fehlt in Antwort (%s)", s.reg, s.id)
|
||||||
continue
|
continue
|
||||||
if s.count == 2:
|
data_type = getattr(s, "data_type", "uint16")
|
||||||
|
if data_type == "float32":
|
||||||
if s.reg + 1 not in regs:
|
if s.reg + 1 not in regs:
|
||||||
log.warning("Register %d (high word) fehlt (%s)", s.reg + 1, s.id)
|
log.warning("Register %d+1 fehlt (%s)", s.reg, s.id)
|
||||||
continue
|
continue
|
||||||
raw = (regs[s.reg] << 16) | regs[s.reg + 1]
|
raw_val = struct.unpack(">f", struct.pack(">HH", regs[s.reg], regs[s.reg + 1]))[0]
|
||||||
|
elif s.count == 2:
|
||||||
|
if s.reg + 1 not in regs:
|
||||||
|
log.warning("Register %d+1 fehlt (%s)", s.reg, s.id)
|
||||||
|
continue
|
||||||
|
raw_val = ((regs[s.reg] << 16) | regs[s.reg + 1]) * s.scale
|
||||||
else:
|
else:
|
||||||
raw = regs[s.reg]
|
raw_val = regs[s.reg] * s.scale
|
||||||
values[s.id] = round(raw * s.scale, 3)
|
values[s.id] = round(raw_val, 3)
|
||||||
return values
|
return values
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" onclick="switchTab('live')">Live-Daten</div>
|
<div class="tab active" onclick="switchTab('live')">Live-Daten</div>
|
||||||
<div class="tab" onclick="switchTab('inverters')">Wechselrichter</div>
|
<div class="tab" onclick="switchTab('inverters')">Geräte</div>
|
||||||
<div class="tab" onclick="switchTab('settings')">Einstellungen</div>
|
<div class="tab" onclick="switchTab('settings')">Einstellungen</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,11 +195,11 @@
|
|||||||
<!-- Inverter Edit Modal -->
|
<!-- Inverter Edit Modal -->
|
||||||
<div class="modal-backdrop" id="modal-backdrop" onclick="closeModal(event)">
|
<div class="modal-backdrop" id="modal-backdrop" onclick="closeModal(event)">
|
||||||
<div class="modal" onclick="event.stopPropagation()">
|
<div class="modal" onclick="event.stopPropagation()">
|
||||||
<h2 id="modal-title">Wechselrichter hinzufügen</h2>
|
<h2 id="modal-title">Gerät hinzufügen</h2>
|
||||||
<input type="hidden" id="modal-id">
|
<input type="hidden" id="modal-id">
|
||||||
<div class="field"><label>Name</label>
|
<div class="field"><label>Name</label>
|
||||||
<input type="text" id="modal-name" placeholder="z.B. Dach Süd"></div>
|
<input type="text" id="modal-name" placeholder="z.B. Dach Süd"></div>
|
||||||
<div class="field"><label>Modell</label>
|
<div class="field"><label>Gerätetyp</label>
|
||||||
<select id="modal-model"></select></div>
|
<select id="modal-model"></select></div>
|
||||||
<div class="field"><label>IP-Adresse</label>
|
<div class="field"><label>IP-Adresse</label>
|
||||||
<input type="text" id="modal-ip" placeholder="10.10.20.190"></div>
|
<input type="text" id="modal-ip" placeholder="10.10.20.190"></div>
|
||||||
@@ -296,7 +296,7 @@ async function refreshData() {
|
|||||||
document.getElementById("pill-mqtt").className = `pill ${d.mqtt_ok ? "ok" : "err"}`;
|
document.getElementById("pill-mqtt").className = `pill ${d.mqtt_ok ? "ok" : "err"}`;
|
||||||
const keys = Object.keys(d.inverters || {});
|
const keys = Object.keys(d.inverters || {});
|
||||||
document.getElementById("subtitle").textContent =
|
document.getElementById("subtitle").textContent =
|
||||||
keys.length ? `${keys.length} Wechselrichter` : "Keine Wechselrichter";
|
keys.length ? `${keys.length} Gerät${keys.length !== 1 ? "e" : ""}` : "Keine Geräte";
|
||||||
renderLive(d.inverters || {});
|
renderLive(d.inverters || {});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
document.getElementById("pill-mqtt").className = "pill err";
|
document.getElementById("pill-mqtt").className = "pill err";
|
||||||
@@ -306,7 +306,7 @@ async function refreshData() {
|
|||||||
function renderLive(inverters) {
|
function renderLive(inverters) {
|
||||||
const el = document.getElementById("live-content");
|
const el = document.getElementById("live-content");
|
||||||
if (!Object.keys(inverters).length) {
|
if (!Object.keys(inverters).length) {
|
||||||
el.innerHTML = '<div class="no-data">Keine Wechselrichter konfiguriert.<br>Bitte im Tab „Wechselrichter" hinzufügen.</div>';
|
el.innerHTML = '<div class="no-data">Keine Geräte konfiguriert.<br>Bitte im Tab „Geräte" hinzufügen.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.innerHTML = Object.values(inverters).map(inv => {
|
el.innerHTML = Object.values(inverters).map(inv => {
|
||||||
@@ -366,7 +366,7 @@ function renderInverterList() {
|
|||||||
<div class="inv-card-header">
|
<div class="inv-card-header">
|
||||||
<div class="inv-card-icon">☀️</div>
|
<div class="inv-card-icon">☀️</div>
|
||||||
<div class="inv-card-info">
|
<div class="inv-card-info">
|
||||||
<div class="inv-card-name">${esc(inv.name || "Wechselrichter")}</div>
|
<div class="inv-card-name">${esc(inv.name || "Gerät")}</div>
|
||||||
<div class="inv-card-model">${esc(model.name || inv.inverter_model)}</div>
|
<div class="inv-card-model">${esc(model.name || inv.inverter_model)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -381,7 +381,7 @@ function renderInverterList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
el.innerHTML = cards + `<div class="add-btn" onclick="openModal()">+ Wechselrichter hinzufügen</div>`;
|
el.innerHTML = cards + `<div class="add-btn" onclick="openModal()">+ Gerät hinzufügen</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openModal(invId) {
|
async function openModal(invId) {
|
||||||
@@ -389,7 +389,7 @@ async function openModal(invId) {
|
|||||||
const modal = document.getElementById("modal-backdrop");
|
const modal = document.getElementById("modal-backdrop");
|
||||||
if (invId) {
|
if (invId) {
|
||||||
const inv = invertersList.find(i => i.id === invId) || {};
|
const inv = invertersList.find(i => i.id === invId) || {};
|
||||||
document.getElementById("modal-title").textContent = "Wechselrichter bearbeiten";
|
document.getElementById("modal-title").textContent = "Gerät bearbeiten";
|
||||||
document.getElementById("modal-id").value = inv.id || "";
|
document.getElementById("modal-id").value = inv.id || "";
|
||||||
document.getElementById("modal-name").value = inv.name || "";
|
document.getElementById("modal-name").value = inv.name || "";
|
||||||
document.getElementById("modal-model").value = inv.inverter_model || "MIC_1500_TL_X";
|
document.getElementById("modal-model").value = inv.inverter_model || "MIC_1500_TL_X";
|
||||||
@@ -399,7 +399,7 @@ async function openModal(invId) {
|
|||||||
document.getElementById("modal-prefix").value = inv.mqtt_topic_prefix || "";
|
document.getElementById("modal-prefix").value = inv.mqtt_topic_prefix || "";
|
||||||
document.getElementById("modal-interval").value = inv.update_interval || 30;
|
document.getElementById("modal-interval").value = inv.update_interval || 30;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("modal-title").textContent = "Wechselrichter hinzufügen";
|
document.getElementById("modal-title").textContent = "Gerät hinzufügen";
|
||||||
document.getElementById("modal-id").value = "";
|
document.getElementById("modal-id").value = "";
|
||||||
document.getElementById("modal-name").value = "";
|
document.getElementById("modal-name").value = "";
|
||||||
document.getElementById("modal-ip").value = "";
|
document.getElementById("modal-ip").value = "";
|
||||||
@@ -450,7 +450,7 @@ async function saveInverter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteInverter(id) {
|
async function deleteInverter(id) {
|
||||||
if (!confirm("Wechselrichter wirklich löschen?")) return;
|
if (!confirm("Gerät wirklich löschen?")) return;
|
||||||
invertersList = invertersList.filter(i => i.id !== id);
|
invertersList = invertersList.filter(i => i.id !== id);
|
||||||
try {
|
try {
|
||||||
await fetchJSON(api("api/inverters-config"), {
|
await fetchJSON(api("api/inverters-config"), {
|
||||||
|
|||||||
Reference in New Issue
Block a user