From cc96e990fa6272fface30fff93c72de32c5f183e Mon Sep 17 00:00:00 2001 From: "m0@nas" Date: Wed, 9 Sep 2026 00:07:04 +0200 Subject: [PATCH] Messwerte je Wert zuteilen, damit Gen2-Shellys ueber MQTT gelesen werden Das Discovery-Modul adressiert die neueren Shellys neu: ihre Messwerte stehen nicht mehr als Feldname zu einer HTTP-Adresse in actor_states, sondern als Topic mit dem Feld in value_path. Geschaltet werden sie weiter ueber HTTP - das Geraet bleibt also ein http://-Aktor, waehrend seine Werte ueber den Broker kommen. Der Runner teilte Messwerte bisher allein nach der Geraete-URL zu. Die umgestellten Werte waeren damit beim HTTP-Transport gelandet, der in der JSON-Antwort nach einem Feld namens "Power_EG/status/em:0" gesucht und nie gefunden haette: die Werte waeren still auf ihrem letzten Stand eingefroren. Zugeteilt wird deshalb je Messwert - was wie ein Topic aussieht (ist_topic()), liest der MQTT-Transport, alles andere geht den alten Weg. Fuer die aelteren Shellys aendert sich nichts, sie koennen kein MQTT und bleiben ganz bei HTTP. Zweitens haette der Runner den Wechsel gar nicht bemerkt. Sein Fingerabdruck fuers Neuladen zaehlt die Zeilen von actor_states, und ein Discovery-Lauf schreibt die Zeilen um, statt neue anzulegen - gleiche Zeilenzahl, keine Reaktion, bis zum naechsten Neustart. Jetzt gehen url und value_path als CRC-Summe mit ein, ebenso die command_url der Kommandos. Nach dem naechsten Suchlauf faellt damit das Polling fuer vier Geraete weg: 34 der 35 HTTP-Messwerte wandern zum Broker, gefragt wird nur noch der Handtuchtrockner. Co-Authored-By: Claude Opus 5 --- autoActions/autoaction_runner.py | 59 ++++++++++++++++++++++++++------ autoActions/transports.py | 23 +++++++++++++ 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/autoActions/autoaction_runner.py b/autoActions/autoaction_runner.py index 9f73ffd..3656ddf 100644 --- a/autoActions/autoaction_runner.py +++ b/autoActions/autoaction_runner.py @@ -68,7 +68,7 @@ import requests import paho.mqtt.client as mqtt from transports import (HTTPTransport, LogicTransport, MQTTTransport, - TahomaTransport, WLEDTransport) + TahomaTransport, WLEDTransport, ist_topic) logger = logging.getLogger("autoaction") @@ -163,8 +163,13 @@ class Regelwerk: drin: eine geaenderte Zielhoehe einer Jalousie schlug also ebenso wenig durch. - Die Zeilenzahlen der Geraetetabellen bleiben, damit ein - Discovery-Lauf weiterhin ein Neuladen ausloest. + Von den Geraetetabellen zaehlt nicht nur, wie viele Zeilen es gibt, + sondern auch, wohin sie zeigen. Ein Discovery-Lauf legt naemlich nicht + nur an - er schreibt auch bestehende Zeilen um: bei den neueren + Shellys ist aus der HTTP-Adresse ein Topic geworden, bei gleicher + Zeilenzahl. Ohne url und value_path im Fingerabdruck haette der Runner + das erst beim naechsten Neustart bemerkt und bis dahin an einer + Adresse gefragt, an der niemand mehr antwortet. """ with db.cursor() as c: c.execute("""SELECT (SELECT COUNT(*) FROM automations) AS a, @@ -183,7 +188,13 @@ class Regelwerk: action_id, parameter_id, value))), 0) FROM automation_action_params) AS ps, (SELECT COUNT(*) FROM actor_states) AS d, - (SELECT COUNT(*) FROM actor_commands) AS e""") + (SELECT COALESCE(SUM(CRC32(CONCAT_WS(':', + id, url, value_path))), 0) + FROM actor_states) AS ds, + (SELECT COUNT(*) FROM actor_commands) AS e, + (SELECT COALESCE(SUM(CRC32(CONCAT_WS(':', + id, command_url))), 0) + FROM actor_commands) AS es""") return tuple(sorted(c.fetchone().items())) @classmethod @@ -679,6 +690,26 @@ class Runner: return transport return None + def transport_fuer_messwert(self, state): + """ + Wo ein einzelner Messwert gelesen wird. + + Normalerweise sagt das Geraet es an: ein Shelly haengt an HTTP, eine + Jalousie an der Tahoma-Box. Bei den Shellys der zweiten Generation + faellt beides auseinander - sie schicken ihre Messwerte von selbst an + den Broker, geschaltet werden sie weiter ueber HTTP. Ein Messwert mit + einem Topic wird deshalb dort gelesen, wo er ankommt, und nicht dort, + wo sein Geraet sonst zu erreichen ist. + + Fuer die aelteren Shellys aendert sich nichts: in ihren Messwerten + steht ein Feldname, und der fuehrt weiter zum HTTP-Transport. + """ + if ist_topic(state["state_url"]): + for transport in self.transporte: + if isinstance(transport, MQTTTransport): + return transport + return self.transport_fuer(state["actor_url"]) + def regelwerk_laden(self): self.regelwerk = Regelwerk.laden(self.db) @@ -692,13 +723,19 @@ class Runner: transport.kombigeraete_setzen(kombi) # Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist. - for transport in self.transporte: - passende = [{"id": s["id"], "actor_url": s["actor_url"], - "state_url": s["state_url"], "value_path": s["value_path"], - "wertetabelle": s["wertetabelle"]} - for s in self.regelwerk.states.values() - if transport.passt(s["actor_url"])] - transport.zustaende_anmelden(passende) + # Zugeteilt wird je Messwert und nicht je Geraet, weil beides + # auseinanderfallen kann - siehe transport_fuer_messwert(). + listen = [(transport, []) for transport in self.transporte] + for s in self.regelwerk.states.values(): + zustaendig = self.transport_fuer_messwert(s) + for transport, liste in listen: + if transport is zustaendig: + liste.append({"id": s["id"], "actor_url": s["actor_url"], + "state_url": s["state_url"], "value_path": s["value_path"], + "wertetabelle": s["wertetabelle"]}) + break + for transport, liste in listen: + transport.zustaende_anmelden(liste) # Der zuletzt bekannte Wert aus der Datenbank ist besser als gar # keiner: nach einem Neustart steht sonst jede Bedingung auf "unklar", # bis das Geraet zufaellig etwas schickt. diff --git a/autoActions/transports.py b/autoActions/transports.py index 7372cb9..bc5b45d 100644 --- a/autoActions/transports.py +++ b/autoActions/transports.py @@ -14,6 +14,12 @@ URL des Aktors in der Tabelle `actors`: entschieden, sondern an der Box-Kennung in der URL Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne) +Eine Ausnahme gibt es beim Lesen: meldet ein Geraet seine Messwerte an den +Broker, obwohl es ueber HTTP geschaltet wird, so steht in `actor_states.url` +ein Topic. Solche Werte liest der MQTT-Transport - siehe ist_topic() und +Runner.transport_fuer_messwert(). Das betrifft die Shellys der zweiten +Generation; die aelteren koennen kein MQTT und bleiben ganz bei HTTP. + Alle liegen in einer Datei statt in einem Paket wie bei deviceDiscovery: es sind fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht hier auf einen Blick, was dafuer zu tun ist. @@ -46,6 +52,23 @@ logger = logging.getLogger("autoaction.transport") urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +def ist_topic(state_url): + """ + Sieht diese Zustands-URL nach einem MQTT-Topic aus? + + Gebraucht fuer die neueren Shellys: sie melden ihre Messwerte von selbst + an den Broker, geschaltet werden sie aber weiter ueber HTTP. Ihr Geraet + steht deshalb mit einer http://-Adresse in `actors`, waehrend in ihren + Messwerten ein Topic steht ("Power_EG/status/em:0"). Wer nur auf das + Geraet schaut, wuerde solche Werte im Poll-Takt suchen - und nie finden. + + Ein Topic hat Schraegstriche und kein Schema davor. Feldnamen der anderen + Transporte haben beides nicht: "a_voltage", "core:ClosureState", + "seg[0].col[0]". + """ + return bool(state_url) and "/" in state_url and "://" not in state_url + + def wert_aus_pfad(daten, pfad): """ Einen Teilwert aus einer Nutzlast holen: "[3]", "ssid", "seg[0].col[0]".