v1.8.9: Z2M Geräte-Dropdown für Überschuss-Steuerung

- MQTT Subscribe auf zigbee2mqtt/bridge/devices beim Connect
- Neuer Endpoint GET /api/z2m-devices liefert Friendly Names + Beschreibung
- Eingabefeld für Z2M Name als Datalist-Combo (tippen oder aus Liste wählen)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-05-04 10:57:52 +02:00
parent 83035fed0e
commit 10e21f031a
3 changed files with 59 additions and 2 deletions
+28
View File
@@ -76,6 +76,7 @@ class State:
inv_data: Dict[str, Dict[str, Any]] = {} inv_data: Dict[str, Dict[str, Any]] = {}
surplus_devices_cfg: List[Dict[str, Any]] = [] surplus_devices_cfg: List[Dict[str, Any]] = []
z2m_base: str = "zigbee2mqtt" z2m_base: str = "zigbee2mqtt"
z2m_devices: List[Dict[str, Any]] = []
_publisher: Optional[MqttPublisher] = None _publisher: Optional[MqttPublisher] = None
_surplus_ctrl: Optional[SurplusDeviceController] = None _surplus_ctrl: Optional[SurplusDeviceController] = None
@@ -370,6 +371,28 @@ def _restart_all():
_surplus_ctrl.set_config(devices, z2m_base) _surplus_ctrl.set_config(devices, z2m_base)
_start_surplus_loop() _start_surplus_loop()
def _on_z2m_devices(topic, payload):
try:
devices_raw = json.loads(payload)
if not isinstance(devices_raw, list):
return
parsed = [
{
"friendly_name": d.get("friendly_name", ""),
"description": (d.get("definition") or {}).get("description", ""),
"type": d.get("type", ""),
}
for d in devices_raw
if d.get("friendly_name") and d.get("friendly_name") != "Coordinator"
]
with State.lock:
State.z2m_devices = parsed
log.info("Z2M: %d Geräte empfangen", len(parsed))
except Exception as e:
log.warning("Z2M bridge/devices Fehler: %s", e)
_publisher.subscribe(f"{z2m_base}/bridge/devices", _on_z2m_devices)
for inv_cfg in State.inverters_cfg: for inv_cfg in State.inverters_cfg:
_start_inverter(inv_cfg) _start_inverter(inv_cfg)
@@ -488,6 +511,11 @@ def api_period_energy():
return jsonify(result) return jsonify(result)
@app.get("/api/z2m-devices")
def api_z2m_devices():
with State.lock:
return jsonify(State.z2m_devices)
@app.get("/api/surplus-devices") @app.get("/api/surplus-devices")
def api_get_surplus_devices(): def api_get_surplus_devices():
with State.lock: with State.lock:
+17
View File
@@ -21,11 +21,13 @@ class MqttPublisher:
self._registered: List[Tuple] = [] self._registered: List[Tuple] = []
self._agg_meta: Dict = agg_meta or {} self._agg_meta: Dict = agg_meta or {}
self._subscriptions: List[Tuple[str, any]] = []
self._client = mqtt.Client(client_id="shinebridge_hub", clean_session=True) self._client = mqtt.Client(client_id="shinebridge_hub", clean_session=True)
if user: if user:
self._client.username_pw_set(user, password) self._client.username_pw_set(user, password)
self._client.on_connect = self._on_connect self._client.on_connect = self._on_connect
self._client.on_disconnect = self._on_disconnect self._client.on_disconnect = self._on_disconnect
self._client.on_message = self._on_message
def _on_connect(self, client, userdata, flags, rc): def _on_connect(self, client, userdata, flags, rc):
if rc == 0: if rc == 0:
@@ -35,6 +37,8 @@ class MqttPublisher:
self._publish_discovery(*entry) self._publish_discovery(*entry)
if self._agg_meta: if self._agg_meta:
self._publish_aggregate_discovery() self._publish_aggregate_discovery()
for topic, _ in self._subscriptions:
client.subscribe(topic)
else: else:
log.error("MQTT Verbindungsfehler rc=%d", rc) log.error("MQTT Verbindungsfehler rc=%d", rc)
@@ -42,6 +46,19 @@ class MqttPublisher:
self._connected = False self._connected = False
log.warning("MQTT getrennt rc=%d", rc) log.warning("MQTT getrennt rc=%d", rc)
def _on_message(self, client, userdata, msg):
for topic, callback in self._subscriptions:
if mqtt.topic_matches_sub(topic, msg.topic):
try:
callback(msg.topic, msg.payload)
except Exception as e:
log.error("MQTT message handler Fehler [%s]: %s", msg.topic, e)
def subscribe(self, topic: str, callback):
self._subscriptions.append((topic, callback))
if self._connected:
self._client.subscribe(topic)
def connect(self): def connect(self):
try: try:
self._client.connect_async(self._broker, self._port, keepalive=60) self._client.connect_async(self._broker, self._port, keepalive=60)
+14 -2
View File
@@ -335,6 +335,7 @@
</div> </div>
</div> </div>
<datalist id="z2m-device-list"></datalist>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script> <script>
@@ -992,7 +993,7 @@ function renderSurplusDeviceRow(dev) {
return `<div class="surplus-device-row" data-id="${esc(id)}"> return `<div class="surplus-device-row" data-id="${esc(id)}">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center"> <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="text" placeholder="Name" style="flex:1;min-width:110px" value="${esc(dev.name||'')}" data-field="name"> <input type="text" placeholder="Name" style="flex:1;min-width:110px" value="${esc(dev.name||'')}" data-field="name">
<input type="text" placeholder="Z2M Friendly Name" style="flex:1;min-width:140px" value="${esc(dev.z2m_name||'')}" data-field="z2m_name"> <input type="text" placeholder="Z2M Friendly Name" list="z2m-device-list" style="flex:1;min-width:140px" value="${esc(dev.z2m_name||'')}" data-field="z2m_name">
<input type="number" placeholder="Ab (W)" title="Schwellwert" style="width:76px" value="${dev.threshold_w||500}" min="0" step="50" data-field="threshold_w"> <input type="number" placeholder="Ab (W)" title="Schwellwert" style="width:76px" value="${dev.threshold_w||500}" min="0" step="50" data-field="threshold_w">
<input type="number" placeholder="Hysterese (W)" title="Hysterese" style="width:90px" value="${dev.hysteresis_w||150}" min="0" step="50" data-field="hysteresis_w"> <input type="number" placeholder="Hysterese (W)" title="Hysterese" style="width:90px" value="${dev.hysteresis_w||150}" min="0" step="50" data-field="hysteresis_w">
<label style="cursor:pointer;display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap"> <label style="cursor:pointer;display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap">
@@ -1003,8 +1004,19 @@ function renderSurplusDeviceRow(dev) {
</div>`; </div>`;
} }
async function loadZ2mDevices() {
const devices = await fetchJSON(api("api/z2m-devices")).catch(() => []);
const dl = document.getElementById("z2m-device-list");
dl.innerHTML = devices.map(d =>
`<option value="${esc(d.friendly_name)}">${esc(d.friendly_name)}${d.description ? ' — ' + esc(d.description) : ''}</option>`
).join('');
}
async function loadSurplusDevices() { async function loadSurplusDevices() {
const d = await fetchJSON(api("api/surplus-devices")).catch(() => ({})); const [d] = await Promise.all([
fetchJSON(api("api/surplus-devices")).catch(() => ({})),
loadZ2mDevices(),
]);
surplusDevices = d.devices || []; surplusDevices = d.devices || [];
document.getElementById("cfg-z2m-base").value = d.z2m_base || "zigbee2mqtt"; document.getElementById("cfg-z2m-base").value = d.z2m_base || "zigbee2mqtt";
const list = document.getElementById("surplus-device-list"); const list = document.getElementById("surplus-device-list");