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>
275 lines
10 KiB
Python
Executable File
275 lines
10 KiB
Python
Executable File
#!/usr/local/bin/python3.10
|
|
"""Startet die wattpilotshell, aber mit einer Reparatur an der MQTT-Bruecke.
|
|
|
|
Das Problem
|
|
-----------
|
|
Die Wallbox schickt in ihren Statusnachrichten Felder, die die
|
|
API-Beschreibung der Bibliothek (wattpilot 0.2, Stand 2022) nicht kennt -
|
|
gemessen 34 Stueck, darunter 'clea', 'cle', 'opad' und die ganze
|
|
'ocpp*'-Familie. Die Bibliothek schlaegt jedes Feld ungeprueft in ihrem
|
|
Verzeichnis nach:
|
|
|
|
for prop_name, value in msg_dict["status"].items():
|
|
pd = wpdef["properties"][prop_name] # KeyError bei 'clea'
|
|
mqtt_publish_property(wp, mqtt_client, pd, value)
|
|
|
|
Der KeyError reisst die restliche Schleife mit. Alles, was in derselben
|
|
Nachricht *hinter* dem unbekannten Feld steht, wird nie veroeffentlicht -
|
|
beim Verbindungsaufbau trifft das unter anderem 'amp', 'car', 'ust', 'err',
|
|
'cbl', 'pha' und 'wh'. Sichtbar wurde das nur als Fehlerpaar im Log, etwa
|
|
alle 16 Sekunden:
|
|
|
|
ERROR:wattpilot:Error received from WebSocketApp: 'clea'
|
|
ERROR:websocket:error from callback ... Wattpilot.__on_message ...: 'clea'
|
|
|
|
Der zweite Fehler
|
|
-----------------
|
|
Dieselbe Ursache aus der anderen Richtung: 'lot' steht in der
|
|
API-Beschreibung als jsonType 'integer', die Firmware schickt aber ein
|
|
Objekt. Die Bibliothek reicht es dann unveraendert an paho weiter, und das
|
|
lehnt ab:
|
|
|
|
ERROR:wattpilot:Error received from WebSocketApp:
|
|
payload must be a string, bytearray, int, float or None
|
|
|
|
Auch dieser Fehler riss die Schleife mit. Im alten Log machte er gut vier von
|
|
zehn Zeilen aus.
|
|
|
|
Die Reparatur
|
|
-------------
|
|
Unbekannte Felder werden uebersprungen statt zu werfen, jedes einmal mit
|
|
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/<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
|
|
--------------------------------------
|
|
Die Bibliothek liegt unter ~/.local/lib/python3.10/site-packages und damit
|
|
ausserhalb dieses Repositorys; eine Neuinstallation wuerde eine Aenderung
|
|
dort spurlos zurueckdrehen. wattpilot 0.2 ist seit Mai 2022 die letzte
|
|
Veroeffentlichung, ein Update behebt es also nicht. Deshalb steht die
|
|
Reparatur hier, unter Versionskontrolle, und ersetzt die Funktion zur
|
|
Laufzeit. mqtt_setup() schlaegt den Namen erst beim Registrieren nach, so
|
|
dass unsere Fassung greift.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
import sys
|
|
|
|
from wattpilot import wattpilotshell as shell
|
|
|
|
_LOG = logging.getLogger("wattpilot.bruecke")
|
|
|
|
# Jedes unbekannte Feld soll genau einmal auffallen, nicht bei jeder Nachricht.
|
|
_gemeldet = set()
|
|
|
|
|
|
_original_kodieren = shell.mqtt_get_encoded_property
|
|
|
|
|
|
def mqtt_get_encoded_property(pd, value):
|
|
"""Ersatz fuer wattpilotshell.mqtt_get_encoded_property.
|
|
|
|
Die Bibliothek verpackt nur dann in JSON, wenn die API-Beschreibung den
|
|
Wert als Objekt, Liste oder Wahrheitswert fuehrt. Stimmt die Beschreibung
|
|
nicht mit dem ueberein, was die Firmware wirklich schickt, kommt ein
|
|
dict oder eine list bei paho an - und paho nimmt nur Skalare.
|
|
"""
|
|
kodiert = _original_kodieren(pd, value)
|
|
if kodiert is None or isinstance(kodiert, (str, bytes, bytearray, int, float)):
|
|
return kodiert
|
|
|
|
name = pd.get("key", "?")
|
|
if name not in _gemeldet:
|
|
_gemeldet.add(name)
|
|
_LOG.warning(
|
|
"Feld '%s' ist als jsonType %r beschrieben, kommt aber als %s an "
|
|
"- wird als JSON veroeffentlicht", name, pd.get("jsonType"),
|
|
type(kodiert).__name__)
|
|
return json.dumps(kodiert, cls=shell.JSONNamespaceEncoder)
|
|
|
|
|
|
def mqtt_publish_message(event, message):
|
|
"""Ersatz fuer wattpilotshell.mqtt_publish_message - siehe Modulkopf."""
|
|
wp = event["wp"]
|
|
if shell.mqtt_client is None:
|
|
return
|
|
|
|
nachricht = json.loads(message)
|
|
typ = nachricht["type"]
|
|
|
|
if shell.MQTT_PUBLISH_MESSAGES == "true" and (
|
|
shell.MQTT_MESSAGES in ([], [""]) or typ in shell.MQTT_MESSAGES):
|
|
thema = shell.mqtt_subst_topic(shell.MQTT_TOPIC_MESSAGES, {
|
|
"baseTopic": shell.MQTT_TOPIC_BASE,
|
|
"serialNumber": wp.serial,
|
|
"messageType": typ,
|
|
})
|
|
shell.mqtt_client.publish(thema, message)
|
|
|
|
if shell.MQTT_PUBLISH_PROPERTIES != "true" or typ not in ("fullStatus", "deltaStatus"):
|
|
return
|
|
|
|
eigenschaften = shell.wpdef["properties"]
|
|
for name, wert in nachricht["status"].items():
|
|
beschreibung = eigenschaften.get(name)
|
|
if beschreibung is None:
|
|
if name not in _gemeldet:
|
|
_gemeldet.add(name)
|
|
_LOG.warning(
|
|
"Feld '%s' steht nicht in der API-Beschreibung der "
|
|
"Bibliothek - wird uebersprungen (Wert: %.80s)", name, repr(wert))
|
|
continue
|
|
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_decoded_property = mqtt_get_decoded_property
|
|
shell.mqtt_publish_message = mqtt_publish_message
|
|
shell.mqtt_set_value = mqtt_set_value
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(shell.main())
|
|
except KeyboardInterrupt:
|
|
sys.exit(0)
|