From 33ada90df4d104656b16a76e3c265aa8c9aff5e4 Mon Sep 17 00:00:00 2001 From: retr0 <42kdesigners@gmail.com> Date: Mon, 4 May 2026 12:02:37 +0200 Subject: [PATCH] =?UTF-8?q?v1.8.12:=20Mindest-Laufzeit=20pro=20=C3=9Cbersc?= =?UTF-8?q?huss-Ger=C3=A4t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - min_on_minutes pro Gerät: Gerät bleibt mindestens X Minuten an auch wenn Überschuss unter Ausschaltschwelle fällt (Boiler-Fix) - Live-Tab zeigt verbleibende Mindestlaufzeit während Gerät läuft - get_states() liefert on_minutes für Laufzeit-Tracking Co-Authored-By: Claude Sonnet 4.6 --- haos-addon/config.yaml | 2 +- haos-addon/src/surplus_devices.py | 28 ++++++++++++++++++++++++---- haos-addon/src/web/index.html | 17 +++++++++++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index 6c82241..98b429a 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.8.11" +version: "1.8.12" slug: shinebridge description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI url: https://gitea.bitfire.work/retr0/shinebridge diff --git a/haos-addon/src/surplus_devices.py b/haos-addon/src/surplus_devices.py index 62d0c1b..01bf06f 100644 --- a/haos-addon/src/surplus_devices.py +++ b/haos-addon/src/surplus_devices.py @@ -1,6 +1,7 @@ import json import logging -from typing import Any, Dict, List +import time +from typing import Any, Dict, List, Optional log = logging.getLogger(__name__) @@ -13,6 +14,7 @@ class SurplusDeviceController: self._base = z2m_base self._devices: List[Dict[str, Any]] = [] self._states: Dict[str, bool] = {} + self._on_since: Dict[str, float] = {} # z2m_name → Einschaltzeitpunkt def set_config(self, devices: List[Dict[str, Any]], z2m_base: str = "zigbee2mqtt"): enabled_now = {d["z2m_name"] for d in devices if d.get("enabled", True)} @@ -24,6 +26,7 @@ class SurplusDeviceController: self._base = z2m_base def update(self, surplus_w: float): + now = time.time() for dev in self._devices: name = dev["z2m_name"] if not dev.get("enabled", True): @@ -32,14 +35,29 @@ class SurplusDeviceController: continue threshold = float(dev.get("threshold_w", 500)) hysteresis = float(dev.get("hysteresis_w", 150)) + min_on_s = float(dev.get("min_on_minutes", 0)) * 60 currently_on = self._states.get(name, False) + if not currently_on and surplus_w >= threshold: self._send(name, True) + self._on_since[name] = now elif currently_on and surplus_w < (threshold - hysteresis): - self._send(name, False) + on_since = self._on_since.get(name, 0.0) + if now - on_since >= min_on_s: + self._send(name, False) + else: + remaining = int((min_on_s - (now - on_since)) / 60) + log.info("[SurplusDevices] %s bleibt an — Mindestlaufzeit: noch %dmin", name, remaining) - def get_states(self) -> Dict[str, bool]: - return dict(self._states) + def get_states(self) -> Dict[str, Any]: + now = time.time() + result = {} + for name, on in self._states.items(): + entry: Dict[str, Any] = {"on": on} + if on and name in self._on_since: + entry["on_minutes"] = int((now - self._on_since[name]) / 60) + result[name] = entry + return result def _send(self, z2m_name: str, on: bool): topic = f"{self._base}/{z2m_name}/set" @@ -47,6 +65,8 @@ class SurplusDeviceController: try: self._pub.publish_raw(topic, payload) self._states[z2m_name] = on + if not on: + self._on_since.pop(z2m_name, None) log.info("[SurplusDevices] %s → %s", z2m_name, "ON" if on else "OFF") except Exception as e: log.error("[SurplusDevices] MQTT Fehler: %s", e) diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index e2cfbbe..941acd1 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -996,6 +996,7 @@ function renderSurplusDeviceRow(dev) { + @@ -1041,7 +1042,8 @@ function _collectSurplusDevices() { name: row.querySelector('[data-field=name]').value.trim(), z2m_name: row.querySelector('[data-field=z2m_name]').value.trim(), threshold_w: parseFloat(row.querySelector('[data-field=threshold_w]').value) || 0, - hysteresis_w: parseFloat(row.querySelector('[data-field=hysteresis_w]').value) || 0, + hysteresis_w: parseFloat(row.querySelector('[data-field=hysteresis_w]').value) || 0, + min_on_minutes: parseFloat(row.querySelector('[data-field=min_on_minutes]').value) || 0, enabled: row.querySelector('[data-field=enabled]').checked, })); } @@ -1069,12 +1071,23 @@ function renderSurplusStatus(surplusData) { const rows = surplusData.devices .filter(d => d.enabled !== false) .map(d => { - const on = states[d.z2m_name] === true; + const st = states[d.z2m_name] || {}; + const on = st.on === true; + const onMin = st.on_minutes ?? null; + const minOn = d.min_on_minutes || 0; const dot = ``; + let info = ''; + if (on && minOn > 0 && onMin !== null) { + const remaining = Math.max(0, minOn - onMin); + info = remaining > 0 + ? `(${remaining}min Mindestlaufzeit)` + : `(${onMin}min)`; + } return `
${dot} ${esc(d.name||d.z2m_name)} ab ${d.threshold_w}W + ${info} ${on?'EIN':'AUS'}
`; }).join('');