diff --git a/autoActions/README.md b/autoActions/README.md index a7123db..4ba52c1 100644 --- a/autoActions/README.md +++ b/autoActions/README.md @@ -280,6 +280,17 @@ von paho abgelehnt — gut vier von zehn Zeilen im alten Log. Feld einmal mit einer Warnung im Log. `startWattpilotMQTT.sh` startet seither die Brücke statt der `wattpilotshell` direkt. +Seit dem 04.09.2026 trägt dieselbe Brücke auch die Gegenrichtung. Sie nimmt +Kommandos auf `wattpilot/properties//set` entgegen und meldet auf +`/result`, ob die Wallbox sie genommen hat — dasselbe Muster, das +der go-e von Haus aus spricht. Das Dashboard steuert damit beide Wallboxen +über einen Weg (`restricted/wallboxen.php`) und startet für die Wattpilot +keinen eigenen Python-Prozess mehr. Nötig waren dafür drei Reparaturen an der +Bibliothek: Zahlen kamen als Zeichenkette bei der Wallbox an +(`value must be uint8_t`), ein unbekannter Name und jede Eigenschaft ohne +`rw`-Angabe — `ftt` und `fte` zum Beispiel — töteten den paho-Callback, und +eine Rückmeldung gab es überhaupt nicht. + `fetch_calendar.py` gehört einmal jährlich in den Cron: ``` diff --git a/wattpilot_bruecke.py b/wattpilot_bruecke.py index 9d78084..a1f4ae7 100755 --- a/wattpilot_bruecke.py +++ b/wattpilot_bruecke.py @@ -42,6 +42,17 @@ einer Warnung im Log. Werte, die trotz Kodierung kein Skalar sind, gehen als JSON hinaus statt als Python-Objekt. Alle uebrigen Felder derselben Nachricht kommen dadurch durch. +Die Schreibrichtung +------------------- +Dieselbe Bruecke nimmt Kommandos auf wattpilot/properties//set +entgegen - das Gegenstueck zu api/set?=... beim go-e. Auch dieser +Weg war unbenutzbar: die Bibliothek reicht Zahlen als Zeichenkette weiter +("value must be uint8_t"), greift bei unbekannten Namen und bei +Eigenschaften ohne rw-Angabe ungeprueft zu, und meldet nichts zurueck. Hier +wird der Wert in den Typ gebracht, den die API-Beschreibung nennt, unsauberes +verworfen statt den paho-Callback zu toeten, und das Ergebnis auf +/result veroeffentlicht - genau wie es der go-e tut. + Warum hier und nicht in der Bibliothek -------------------------------------- Die Bibliothek liegt unter ~/.local/lib/python3.10/site-packages und damit @@ -55,6 +66,7 @@ dass unsere Fassung greift. import json import logging +import re import sys from wattpilot import wattpilotshell as shell @@ -124,8 +136,136 @@ def mqtt_publish_message(event, message): shell.mqtt_publish_property(wp, shell.mqtt_client, beschreibung, wert) +# --- Schreibrichtung: was aus MQTT hereinkommt ------------------------------ + +_original_dekodieren = shell.mqtt_get_decoded_property + +# requestId -> Eigenschaftsname, damit die Antwort der Wallbox ihr Topic +# wiederfindet. Bleibt eine Antwort aus, verfaellt der Eintrag beim Aufraeumen. +_offene_anfragen = {} +_antwort_haengt = [False] + + +_KEIN_WERT = object() + + +def _als_typ(text, typ): + """Bringt eine Zeichenkette in den Typ, den die API-Beschreibung nennt. + + Gibt _KEIN_WERT zurueck, wenn das nicht geht - dann ist der Wert keine + Zahlenform, sondern vermutlich ein Name aus der valueMap ("Awattar"), um + den sich die Bibliothek selbst kuemmert. + """ + if text.lower() in ("null", "none"): + return None + try: + if typ == "integer": + return int(float(text)) + if typ == "number": + return float(text) + if typ == "boolean": + if text.lower() in ("true", "1"): + return True + if text.lower() in ("false", "0"): + return False + except ValueError: + pass + return _KEIN_WERT + + +def mqtt_get_decoded_property(pd, value): + """Ersatz fuer wattpilotshell.mqtt_get_decoded_property. + + Die Bibliothek holt nur Listen und Objekte aus dem JSON; alles andere + reicht sie als Zeichenkette an die Wallbox weiter, und die lehnt ab: + + ERROR:wattpilot:Error Sending Request 1. Message: value must be uint8_t + + Hier bekommt der Wert zuerst den Typ, den die API-Beschreibung nennt. + Damit nimmt die Wallbox 'lmo = 4' genauso wie 'lmo = Awattar' - der go-e + kennt nur die Zahlenform, und beide sollen gleich bedienbar sein. Nur was + sich nicht als Zahl lesen laesst, geht den alten Weg ueber die valueMap. + """ + if isinstance(value, str): + gewandelt = _als_typ(value.strip(), pd.get("jsonType")) + if gewandelt is not _KEIN_WERT: + return gewandelt + return _original_dekodieren(pd, value) + + +def _antwort_veroeffentlichen(event, nachricht): + """Meldet zurueck, ob die Wallbox den Wert genommen hat. + + Der go-e antwortet auf /result mit 'success' oder + einer Fehlermeldung. Die Bibliothek kennt das nicht, also bauen wir es + nach - sonst bleibt ein abgelehnter Wert fuer die Weboberflaeche + unsichtbar. + """ + if shell.mqtt_client is None: + return + # Die Wallbox schickt die Kennung als Zeichenkette zurueck ("1"), + # gezaehlt wird sie als Zahl. Beide Seiten als Text vergleichen. + name = _offene_anfragen.pop(str(getattr(nachricht, "requestId", "")), None) + if name is None: + return + erfolg = bool(getattr(nachricht, "success", False)) + antwort = "success" if erfolg else str(getattr(nachricht, "message", "error")) + thema = shell.mqtt_subst_topic("~/result", { + "baseTopic": shell.MQTT_TOPIC_BASE, + "serialNumber": event["wp"].serial, + "propName": name, + }) + shell.mqtt_client.publish(thema, antwort) + + +def mqtt_set_value(client, userdata, message): + """Ersatz fuer wattpilotshell.mqtt_set_value. + + Das Original greift zweimal ungeprueft zu: wpdef["properties"][name] wirft + bei einem unbekannten Namen, pd['rw'] bei jeder Eigenschaft ohne + Schreibangabe - ftt und fte zum Beispiel haben keine. Beides toetet den + paho-Callback, und danach nimmt die Bruecke keine Kommandos mehr an. + """ + muster = shell.mqtt_subst_topic(shell.MQTT_TOPIC_PROPERTY_SET, + {"propName": "([^/]+)"}) + treffer = re.fullmatch(muster, message.topic) + if treffer is None: + _LOG.warning("Unerwartetes Set-Topic %s - ignoriert", message.topic) + return + name = treffer.group(1) + + beschreibung = shell.wpdef["properties"].get(name) + if beschreibung is None: + _LOG.warning("Schreibversuch auf unbekannte Eigenschaft '%s' - " + "ignoriert (Topic %s)", name, message.topic) + return + if beschreibung.get("rw") == "R": + _LOG.warning("Eigenschaft '%s' ist schreibgeschuetzt - ignoriert", name) + return + + wert = mqtt_get_decoded_property(beschreibung, message.payload.decode("utf-8")) + + if not _antwort_haengt[0]: + shell.wp.add_event_handler("wp_response", _antwort_veroeffentlichen) + _antwort_haengt[0] = True + if len(_offene_anfragen) > 50: + _offene_anfragen.clear() + + # Erst vormerken, dann senden. Die Wallbox antwortet binnen Millisekunden + # und aus einem anderen Thread; wer die Zuordnung hinterher eintraegt, + # findet die Antwort nicht mehr vor. send_update() zaehlt die Kennung + # hoch und benutzt sie sofort, die naechste ist also vorhersagbar. + naechste = getattr(shell.wp, "_Wattpilot__requestid", 0) + 1 + _offene_anfragen[str(naechste)] = name + + shell.wp.send_update(name, wert) + _LOG.info("Gesetzt: %s = %r (%s)", name, wert, type(wert).__name__) + + shell.mqtt_get_encoded_property = mqtt_get_encoded_property +shell.mqtt_get_decoded_property = mqtt_get_decoded_property shell.mqtt_publish_message = mqtt_publish_message +shell.mqtt_set_value = mqtt_set_value if __name__ == "__main__": try: