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 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 00:07:04 +02:00
co-authored by Claude Opus 5
parent 1e7b1d8b21
commit cc96e990fa
2 changed files with 71 additions and 11 deletions
+48 -11
View File
@@ -68,7 +68,7 @@ import requests
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from transports import (HTTPTransport, LogicTransport, MQTTTransport, from transports import (HTTPTransport, LogicTransport, MQTTTransport,
TahomaTransport, WLEDTransport) TahomaTransport, WLEDTransport, ist_topic)
logger = logging.getLogger("autoaction") logger = logging.getLogger("autoaction")
@@ -163,8 +163,13 @@ class Regelwerk:
drin: eine geaenderte Zielhoehe einer Jalousie schlug also ebenso drin: eine geaenderte Zielhoehe einer Jalousie schlug also ebenso
wenig durch. wenig durch.
Die Zeilenzahlen der Geraetetabellen bleiben, damit ein Von den Geraetetabellen zaehlt nicht nur, wie viele Zeilen es gibt,
Discovery-Lauf weiterhin ein Neuladen ausloest. 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: with db.cursor() as c:
c.execute("""SELECT (SELECT COUNT(*) FROM automations) AS a, c.execute("""SELECT (SELECT COUNT(*) FROM automations) AS a,
@@ -183,7 +188,13 @@ class Regelwerk:
action_id, parameter_id, value))), 0) action_id, parameter_id, value))), 0)
FROM automation_action_params) AS ps, FROM automation_action_params) AS ps,
(SELECT COUNT(*) FROM actor_states) AS d, (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())) return tuple(sorted(c.fetchone().items()))
@classmethod @classmethod
@@ -679,6 +690,26 @@ class Runner:
return transport return transport
return None 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): def regelwerk_laden(self):
self.regelwerk = Regelwerk.laden(self.db) self.regelwerk = Regelwerk.laden(self.db)
@@ -692,13 +723,19 @@ class Runner:
transport.kombigeraete_setzen(kombi) transport.kombigeraete_setzen(kombi)
# Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist. # Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist.
for transport in self.transporte: # Zugeteilt wird je Messwert und nicht je Geraet, weil beides
passende = [{"id": s["id"], "actor_url": s["actor_url"], # auseinanderfallen kann - siehe transport_fuer_messwert().
"state_url": s["state_url"], "value_path": s["value_path"], listen = [(transport, []) for transport in self.transporte]
"wertetabelle": s["wertetabelle"]} for s in self.regelwerk.states.values():
for s in self.regelwerk.states.values() zustaendig = self.transport_fuer_messwert(s)
if transport.passt(s["actor_url"])] for transport, liste in listen:
transport.zustaende_anmelden(passende) 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 # Der zuletzt bekannte Wert aus der Datenbank ist besser als gar
# keiner: nach einem Neustart steht sonst jede Bedingung auf "unklar", # keiner: nach einem Neustart steht sonst jede Bedingung auf "unklar",
# bis das Geraet zufaellig etwas schickt. # bis das Geraet zufaellig etwas schickt.
+23
View File
@@ -14,6 +14,12 @@ URL des Aktors in der Tabelle `actors`:
entschieden, sondern an der Box-Kennung in der URL entschieden, sondern an der Box-Kennung in der URL
Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne) 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 Alle liegen in einer Datei statt in einem Paket wie bei deviceDiscovery: es
sind fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht sind fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht
hier auf einen Blick, was dafuer zu tun ist. hier auf einen Blick, was dafuer zu tun ist.
@@ -46,6 +52,23 @@ logger = logging.getLogger("autoaction.transport")
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 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): def wert_aus_pfad(daten, pfad):
""" """
Einen Teilwert aus einer Nutzlast holen: "[3]", "ssid", "seg[0].col[0]". Einen Teilwert aus einer Nutzlast holen: "[3]", "ssid", "seg[0].col[0]".