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:
@@ -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:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user