ShineBridge v1.8.31 — EMS-Fixes: konfigurierbarer Max-Ladestrom, Zwangsladen-Retry, Fault-State-Handling
- ems_max_current_ma konfigurierbar statt hardcoded 32A (Zwangsladen darf jetzt nicht mehr mehr fordern als die tatsächliche Wallbox liefern kann, z.B. 16A bei einer 11kW-Wallbox) - Zwangsladen setzt den Ladestrom bei jedem Poll erneut (statt nur einmal beim Start) — ein einzelner fehlgeschlagener Modbus-Write hängt nicht mehr die ganze Nacht - Unbekannte/Fehler-Ladezustände (z.B. Fahrzeug-Störung) werden nicht mehr wie 'abgesteckt' behandelt — Strom wird pausiert, aber der Zwangslade-Timer bleibt erhalten - Neues Settings-Feld 'Max. Ladestrom (A)' im EMS-Dialog Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,3 +6,7 @@ shinelanx-modbus/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# versehentlich getrackte Alt-Dateien, gehoeren nicht ins Repo
|
||||||
|
Modbus-Server-Kathrein.pdf
|
||||||
|
ShineLAN-X/releases/*.bin
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: ShineBridge
|
name: ShineBridge
|
||||||
version: "1.8.30"
|
version: "1.8.31"
|
||||||
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/shinebridge
|
url: https://gitea.bitfire.work/retr0/shinebridge
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ TARGET_HOUR = 6 # Zielstunde: Vollladung bis 06:00
|
|||||||
POLL_INTERVAL_S = 30 # Sekunden zwischen EMS-Updates
|
POLL_INTERVAL_S = 30 # Sekunden zwischen EMS-Updates
|
||||||
PHASES = 3 # Anzahl der Phasen
|
PHASES = 3 # Anzahl der Phasen
|
||||||
VOLTAGE_V = 230 # Netzspannung
|
VOLTAGE_V = 230 # Netzspannung
|
||||||
|
MAX_CHARGE_MA = 32000 # Absolutes Wallbox-Limit (Hardware-Grenze WallboxReader).
|
||||||
|
# ACHTUNG: die tatsächlich installierte Wallbox/Zuleitung kann
|
||||||
|
# niedriger dimensioniert sein (z.B. 11kW-Wallbox = 16A) — dann
|
||||||
|
# über max_charge_ma im Konstruktor auf den echten Wert begrenzen,
|
||||||
|
# sonst fordert das Zwangsladen mehr an als das Gerät liefern kann.
|
||||||
|
|
||||||
# Charging-State Werte (Kathrein Modbus)
|
# Charging-State Werte (Kathrein Modbus)
|
||||||
STATE_IDLE = 0
|
STATE_IDLE = 0
|
||||||
@@ -39,11 +44,13 @@ class EmsController:
|
|||||||
pv_timeout_h: float = PV_TIMEOUT_H,
|
pv_timeout_h: float = PV_TIMEOUT_H,
|
||||||
target_hour: int = TARGET_HOUR,
|
target_hour: int = TARGET_HOUR,
|
||||||
phases: int = PHASES,
|
phases: int = PHASES,
|
||||||
|
max_charge_ma: int = MAX_CHARGE_MA,
|
||||||
):
|
):
|
||||||
self.min_pv_power = min_pv_power
|
self.min_pv_power = min_pv_power
|
||||||
self.pv_timeout_h = pv_timeout_h
|
self.pv_timeout_h = pv_timeout_h
|
||||||
self.target_hour = target_hour
|
self.target_hour = target_hour
|
||||||
self.phases = phases
|
self.phases = phases
|
||||||
|
self.max_charge_ma = max_charge_ma
|
||||||
|
|
||||||
self._pv_ok_since: Optional[float] = None # Zeitpunkt letzter ausreichender PV
|
self._pv_ok_since: Optional[float] = None # Zeitpunkt letzter ausreichender PV
|
||||||
self._no_pv_since: Optional[float] = None # Zeitpunkt seit kein PV
|
self._no_pv_since: Optional[float] = None # Zeitpunkt seit kein PV
|
||||||
@@ -66,7 +73,7 @@ class EmsController:
|
|||||||
total_w = surplus_w + wallbox_w
|
total_w = surplus_w + wallbox_w
|
||||||
current_a = total_w / (VOLTAGE_V * self.phases)
|
current_a = total_w / (VOLTAGE_V * self.phases)
|
||||||
ma = int(current_a * 1000)
|
ma = int(current_a * 1000)
|
||||||
return max(6000, min(32000, ma))
|
return max(6000, min(self.max_charge_ma, ma))
|
||||||
|
|
||||||
def update(self, wallbox_reader, pv_surplus_w: float, charging_state: int,
|
def update(self, wallbox_reader, pv_surplus_w: float, charging_state: int,
|
||||||
wallbox_power_w: float = 0.0) -> str:
|
wallbox_power_w: float = 0.0) -> str:
|
||||||
@@ -84,8 +91,8 @@ class EmsController:
|
|||||||
if wallbox_reader.enable_ems():
|
if wallbox_reader.enable_ems():
|
||||||
self._ems_enabled = True
|
self._ems_enabled = True
|
||||||
log.info("[EMS] EMS aktiviert")
|
log.info("[EMS] EMS aktiviert")
|
||||||
else:
|
elif charging_state == STATE_IDLE:
|
||||||
# Auto weg → alles zurücksetzen
|
# Wirklich kein Fahrzeug angesteckt → alles zurücksetzen, Timer neu starten
|
||||||
if self._ems_enabled:
|
if self._ems_enabled:
|
||||||
wallbox_reader.set_current(0)
|
wallbox_reader.set_current(0)
|
||||||
self._ems_enabled = False
|
self._ems_enabled = False
|
||||||
@@ -93,6 +100,16 @@ class EmsController:
|
|||||||
self._no_pv_since = None
|
self._no_pv_since = None
|
||||||
log.info("[EMS] Auto abgesteckt — EMS zurückgesetzt")
|
log.info("[EMS] Auto abgesteckt — EMS zurückgesetzt")
|
||||||
return "kein Fahrzeug"
|
return "kein Fahrzeug"
|
||||||
|
else:
|
||||||
|
# Unbekannter/nicht abgedeckter Ladezustand (z.B. Fehler/Störung am Fahrzeug).
|
||||||
|
# NICHT wie "abgesteckt" behandeln — sonst verliert der Zwangslade-Timer seinen
|
||||||
|
# Fortschritt und die Deadline (Zielzeit) wird nie mehr erreicht, obwohl das Auto
|
||||||
|
# die ganze Zeit angesteckt war. Strom sicherheitshalber pausieren, aber Zustand/
|
||||||
|
# Timer erhalten, damit nach Rückkehr in einen bekannten Zustand nahtlos weitergeht.
|
||||||
|
wallbox_reader.set_current(0)
|
||||||
|
log.warning("[EMS] Unbekannter Ladezustand %d (evtl. Störung am Fahrzeug) — "
|
||||||
|
"Strom pausiert, Zwangslade-Timer bleibt erhalten", charging_state)
|
||||||
|
return f"unbekannter Zustand ({charging_state})"
|
||||||
|
|
||||||
if charging_state == STATE_COMPLETED:
|
if charging_state == STATE_COMPLETED:
|
||||||
wallbox_reader.set_current(0)
|
wallbox_reader.set_current(0)
|
||||||
@@ -116,13 +133,16 @@ class EmsController:
|
|||||||
no_pv_h = (now - self._no_pv_since) / 3600.0
|
no_pv_h = (now - self._no_pv_since) / 3600.0
|
||||||
|
|
||||||
if no_pv_h >= self.pv_timeout_h:
|
if no_pv_h >= self.pv_timeout_h:
|
||||||
# Zwangsladen bis Zielzeit
|
# Zwangsladen bis Zielzeit — Strom bei JEDEM Update erneut setzen (nicht nur beim
|
||||||
|
# ersten Eintritt), sonst bleibt ein einzelner fehlgeschlagener/verworfener Write
|
||||||
|
# (Modbus-Hänger, Fahrzeug-Störung) unbemerkt für den Rest der Nacht stehen.
|
||||||
target = self._next_target()
|
target = self._next_target()
|
||||||
mins_left = (target - datetime.now()).total_seconds() / 60
|
mins_left = (target - datetime.now()).total_seconds() / 60
|
||||||
if not self._forced_charging:
|
if not self._forced_charging:
|
||||||
wallbox_reader.set_current(32000, self.phases)
|
|
||||||
self._forced_charging = True
|
self._forced_charging = True
|
||||||
log.info("[EMS] Zwangsladen gestartet — %dmin bis %02d:00", int(mins_left), self.target_hour)
|
log.info("[EMS] Zwangsladen gestartet — %dmin bis %02d:00, max %.0fA",
|
||||||
|
int(mins_left), self.target_hour, self.max_charge_ma / 1000)
|
||||||
|
wallbox_reader.set_current(self.max_charge_ma, self.phases)
|
||||||
return f"Zwangsladen ({no_pv_h:.1f}h kein PV, {int(mins_left)}min bis {self.target_hour:02d}:00)"
|
return f"Zwangsladen ({no_pv_h:.1f}h kein PV, {int(mins_left)}min bis {self.target_hour:02d}:00)"
|
||||||
|
|
||||||
# Warten auf PV
|
# Warten auf PV
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
|
|||||||
pv_timeout_h=inv_cfg.get("ems_timeout", 4.0),
|
pv_timeout_h=inv_cfg.get("ems_timeout", 4.0),
|
||||||
target_hour=inv_cfg.get("ems_target_hour", 6),
|
target_hour=inv_cfg.get("ems_target_hour", 6),
|
||||||
phases=inv_cfg.get("ems_phases", 3),
|
phases=inv_cfg.get("ems_phases", 3),
|
||||||
|
max_charge_ma=inv_cfg.get("ems_max_current_ma", 32000),
|
||||||
)
|
)
|
||||||
log.info("[%s] Poll-Loop: %s @ %s:%s (Kathrein EMS) alle %ds",
|
log.info("[%s] Poll-Loop: %s @ %s:%s (Kathrein EMS) alle %ds",
|
||||||
inv_id, inverter.name, host, port, interval)
|
inv_id, inverter.name, host, port, interval)
|
||||||
|
|||||||
@@ -495,6 +495,9 @@ GND → GND
|
|||||||
<option value="2">2-phasig</option>
|
<option value="2">2-phasig</option>
|
||||||
<option value="3" selected>3-phasig</option>
|
<option value="3" selected>3-phasig</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
|
<div class="field"><label>Max. Ladestrom (A)</label>
|
||||||
|
<input type="number" id="ems-max-current" value="32" min="6" max="32" step="1">
|
||||||
|
<div style="font-size:11px;color:var(--muted);margin-top:3px">Obergrenze für Zwangsladen — auf die tatsächliche Wallbox-/Zuleitungs-Absicherung setzen (z.B. 16A bei einer 11kW-Wallbox), nicht höher als die Hardware zulässt</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -1195,6 +1198,7 @@ async function openModal(invId) {
|
|||||||
document.getElementById("ems-timeout").value = inv.ems_timeout ?? 4;
|
document.getElementById("ems-timeout").value = inv.ems_timeout ?? 4;
|
||||||
document.getElementById("ems-target-hour").value = inv.ems_target_hour ?? 6;
|
document.getElementById("ems-target-hour").value = inv.ems_target_hour ?? 6;
|
||||||
document.getElementById("ems-phases").value = inv.ems_phases ?? 3;
|
document.getElementById("ems-phases").value = inv.ems_phases ?? 3;
|
||||||
|
document.getElementById("ems-max-current").value = inv.ems_max_current_ma ? inv.ems_max_current_ma / 1000 : 32;
|
||||||
document.getElementById("ems-enabled").checked = inv.ems_enabled ?? true;
|
document.getElementById("ems-enabled").checked = inv.ems_enabled ?? true;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("modal-title").textContent = "Gerät hinzufügen";
|
document.getElementById("modal-title").textContent = "Gerät hinzufügen";
|
||||||
@@ -1209,6 +1213,7 @@ async function openModal(invId) {
|
|||||||
document.getElementById("ems-timeout").value = "4";
|
document.getElementById("ems-timeout").value = "4";
|
||||||
document.getElementById("ems-target-hour").value = "6";
|
document.getElementById("ems-target-hour").value = "6";
|
||||||
document.getElementById("ems-phases").value = "3";
|
document.getElementById("ems-phases").value = "3";
|
||||||
|
document.getElementById("ems-max-current").value = "32";
|
||||||
}
|
}
|
||||||
toggleEmsSection(modelSel.value);
|
toggleEmsSection(modelSel.value);
|
||||||
modal.classList.add("open");
|
modal.classList.add("open");
|
||||||
@@ -1243,6 +1248,7 @@ async function saveInverter() {
|
|||||||
inv.ems_timeout = parseFloat(document.getElementById("ems-timeout").value);
|
inv.ems_timeout = parseFloat(document.getElementById("ems-timeout").value);
|
||||||
inv.ems_target_hour = parseInt(document.getElementById("ems-target-hour").value);
|
inv.ems_target_hour = parseInt(document.getElementById("ems-target-hour").value);
|
||||||
inv.ems_phases = parseInt(document.getElementById("ems-phases").value);
|
inv.ems_phases = parseInt(document.getElementById("ems-phases").value);
|
||||||
|
inv.ems_max_current_ma = Math.round(parseFloat(document.getElementById("ems-max-current").value) * 1000);
|
||||||
inv.ems_enabled = document.getElementById("ems-enabled").checked;
|
inv.ems_enabled = document.getElementById("ems-enabled").checked;
|
||||||
}
|
}
|
||||||
const idx = invertersList.findIndex(i => i.id === id);
|
const idx = invertersList.findIndex(i => i.id === id);
|
||||||
|
|||||||
Reference in New Issue
Block a user