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]".