Bruecke nimmt jetzt auch Kommandos entgegen

Der Schreibweg wattpilot/properties/<schluessel>/set war unbenutzbar. Die
Bibliothek reichte Zahlen als Zeichenkette an die Wallbox weiter
("value must be uint8_t"), griff bei unbekannten Namen und bei
Eigenschaften ohne rw-Angabe ungeprueft zu - ftt und fte haben keine, ein
Klick auf die Ladeplanung haette den paho-Callback getoetet - und meldete
nichts zurueck.

Jetzt bekommt der Wert den Typ aus der API-Beschreibung, so dass die
Wallbox 'lmo = 4' genauso nimmt wie 'lmo = Awattar'. Unbekanntes und
Schreibgeschuetztes wird abgelehnt statt zu werfen, und das Ergebnis geht
auf <schluessel>/result - genau wie es der go-e tut. Damit laesst sich das
Dashboard auf einen Weg fuer beide Wallboxen umstellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 08:36:00 +02:00
co-authored by Claude Opus 5
parent ce2a86fa2b
commit 5990a8feb2
2 changed files with 151 additions and 0 deletions
+11
View File
@@ -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 Feld einmal mit einer Warnung im Log. `startWattpilotMQTT.sh` startet seither
die Brücke statt der `wattpilotshell` direkt. 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/<schlüssel>/set` entgegen und meldet auf
`<schlüssel>/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: `fetch_calendar.py` gehört einmal jährlich in den Cron:
``` ```
+140
View File
@@ -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 JSON hinaus statt als Python-Objekt. Alle uebrigen Felder derselben Nachricht
kommen dadurch durch. kommen dadurch durch.
Die Schreibrichtung
-------------------
Dieselbe Bruecke nimmt Kommandos auf wattpilot/properties/<schluessel>/set
entgegen - das Gegenstueck zu api/set?<schluessel>=... 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
<schluessel>/result veroeffentlicht - genau wie es der go-e tut.
Warum hier und nicht in der Bibliothek Warum hier und nicht in der Bibliothek
-------------------------------------- --------------------------------------
Die Bibliothek liegt unter ~/.local/lib/python3.10/site-packages und damit Die Bibliothek liegt unter ~/.local/lib/python3.10/site-packages und damit
@@ -55,6 +66,7 @@ dass unsere Fassung greift.
import json import json
import logging import logging
import re
import sys import sys
from wattpilot import wattpilotshell as shell 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) 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 <praefix><schluessel>/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_encoded_property = mqtt_get_encoded_property
shell.mqtt_get_decoded_property = mqtt_get_decoded_property
shell.mqtt_publish_message = mqtt_publish_message shell.mqtt_publish_message = mqtt_publish_message
shell.mqtt_set_value = mqtt_set_value
if __name__ == "__main__": if __name__ == "__main__":
try: try: