Geräteprofil · Wallboxen
Easee Wallbox
Die Wallbox hat keine Schnittstelle im eigenen Netz — sie spricht ausschließlich mit dem Rechenzentrum des Herstellers. nomos liest sie deshalb über einen Node-RED-Flow, der sich dort mit den Kontodaten anmeldet, die Werte abholt und auf MQTT veröffentlicht. Laden pausieren, fortsetzen und die Stromgrenze gehen denselben Weg zurück.
Ein Flow bedient alle Wallboxen des Kontos. In nomos wird je Wallbox ein Gerät angelegt.
Voraussetzungen
- Ein MQTT-Broker, in nomos unter Skills → MQTT eingetragen.
- Node-RED mit Zugriff auf denselben Broker. Der Flow braucht nur Standardknoten.
- Die Zugangsdaten des Easee-Kontos und die Seriennummer jeder Wallbox. Die Seriennummer steht auf dem Gerät und in der App des Herstellers; sie beginnt mit
EH. - Eine Internetverbindung des Controllers — ohne sie bleiben die Werte stehen.
Den Flow einfügen
In Node-RED: Menü rechts oben (☰) → Import → den JSON-Text einfügen → Import → Deploy.
JSON zum Einfügen
[{"id":"nomos_easee","type":"tab","label":"Easee Wallbox","disabled":false,"info":""},{"id":"nomos_easee_broker","type":"mqtt-broker","name":"nomos Broker","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"nomos_easee_tick","type":"inject","z":"nomos_easee","name":"alle 60 Sekunden","x":150,"y":140,"wires":[["nomos_easee_settings"]],"props":[{"p":"payload"}],"repeat":"60","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date"},{"id":"nomos_easee_settings","type":"function","z":"nomos_easee","name":"Zugangsdaten","x":340,"y":140,"wires":[["nomos_easee_auth"]],"func":"// Hier eintragen: Zugangsdaten des Easee-Kontos und die Seriennummern der\n// Wallboxen (sie stehen auf dem Geraet, z. B. EH123456).\nflow.set(\"easeeUser\", \"name@example.com\");\nflow.set(\"easeePassword\", \"KENNWORT-HIER\");\nflow.set(\"easeeChargers\", [\"EH000000\"]);\n// Topic-Wurzel; das Geraet in nomos bekommt spaeter easee/<Seriennummer>\nflow.set(\"easeeRoot\", \"easee\");\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[]},{"id":"nomos_easee_auth","type":"function","z":"nomos_easee","name":"Anmelden oder abfragen","x":560,"y":140,"wires":[["nomos_easee_login"],["nomos_easee_refresh"],["nomos_easee_poll"]],"func":"// Drei Wege: anmelden, Token erneuern oder direkt abfragen.\n// Das Zugriffstoken gilt eine Stunde; eine Minute vorher wird erneuert.\nconst now = Date.now();\nconst token = flow.get(\"easeeToken\");\nconst expires = flow.get(\"easeeExpires\") || 0;\nconst refresh = flow.get(\"easeeRefresh\");\n\nif (token && now < expires - 60000) { return [null, null, msg]; }\n\nif (refresh) {\n msg.url = \"https://api.easee.com/api/accounts/refresh_token\";\n msg.headers = { \"content-type\": \"application/json\" };\n msg.payload = { refreshToken: refresh };\n return [null, msg, null];\n}\n\nmsg.url = \"https://api.easee.com/api/accounts/login\";\nmsg.headers = { \"content-type\": \"application/json\" };\nmsg.payload = { userName: flow.get(\"easeeUser\"), password: flow.get(\"easeePassword\") };\nreturn [msg, null, null];\n","outputs":3,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[]},{"id":"nomos_easee_login","type":"http request","z":"nomos_easee","name":"Anmeldung","x":780,"y":80,"wires":[["nomos_easee_token"]],"method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[]},{"id":"nomos_easee_refresh","type":"http request","z":"nomos_easee","name":"Token erneuern","x":780,"y":140,"wires":[["nomos_easee_token"]],"method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[]},{"id":"nomos_easee_token","type":"function","z":"nomos_easee","name":"Token merken","x":980,"y":110,"wires":[["nomos_easee_poll"]],"func":"// Antwort der Anmeldung bzw. der Erneuerung auswerten\nconst p = msg.payload || {};\nif (!p.accessToken) {\n node.warn(\"keine Anmeldung moeglich - Benutzername und Kennwort pruefen\");\n flow.set(\"easeeToken\", null);\n flow.set(\"easeeRefresh\", null);\n return null;\n}\nflow.set(\"easeeToken\", p.accessToken);\nflow.set(\"easeeRefresh\", p.refreshToken || null);\nflow.set(\"easeeExpires\", Date.now() + (Number(p.expiresIn) || 3600) * 1000);\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[]},{"id":"nomos_easee_poll","type":"function","z":"nomos_easee","name":"Wallboxen abfragen","x":580,"y":240,"wires":[["nomos_easee_observations"]],"func":"// Je Wallbox eine Abfrage. Ein Aufruf liefert alle angeforderten Beobachtungen.\nconst token = flow.get(\"easeeToken\");\nif (!token) { return null; }\nconst chargers = flow.get(\"easeeChargers\") || [];\nif (!chargers.length) { node.warn(\"keine Seriennummer eingetragen\"); return null; }\n\nconst IDS = \"109,120,121,124,114,48,103,96,102,250\";\nconst out = chargers.map((serial) => ({\n serial: String(serial).trim(),\n method: \"GET\",\n url: \"https://api.easee.com/state/\" + encodeURIComponent(String(serial).trim())\n + \"/observations?ids=\" + IDS,\n headers: { \"Authorization\": \"Bearer \" + token },\n payload: \"\"\n}));\nreturn [out];\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[]},{"id":"nomos_easee_observations","type":"http request","z":"nomos_easee","name":"Werte lesen","x":800,"y":240,"wires":[["nomos_easee_publish"]],"method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[]},{"id":"nomos_easee_publish","type":"function","z":"nomos_easee","name":"Werte veroeffentlichen","x":1000,"y":240,"wires":[["nomos_easee_out"]],"func":"// Beobachtungen in Topics uebersetzen. Einheiten und Bedeutungen nach der\n// Dokumentation von Easee: 120 in kW, 121 und 124 in kWh, 114 und 48 in A.\nconst root = flow.get(\"easeeRoot\") || \"easee\";\nconst serial = msg.serial;\nconst body = msg.payload;\n\nif (!body || !Array.isArray(body.observations)) {\n node.warn(\"keine Beobachtungen fuer \" + serial + \" - Anmeldung wird erneuert\");\n flow.set(\"easeeToken\", null);\n return [[{ topic: \"tele/\" + root + \"/\" + serial + \"/LWT\", payload: \"Offline\",\n retain: true }]];\n}\n\nconst seen = {};\nbody.observations.forEach((o) => { seen[Number(o.id)] = o.value; });\n\nconst MODES = {\n 0: \"Offline\", 1: \"Nicht verbunden\", 2: \"Wartet auf Start\", 3: \"Laedt\",\n 4: \"Abgeschlossen\", 5: \"Stoerung\", 6: \"Bereit\", 7: \"Wartet auf Freigabe\",\n 8: \"Freigabe wird aufgehoben\"\n};\nconst REASONS = {\n 0: \"\", 1: \"Lastmanagement: Stromkreis zu klein\",\n 2: \"Lastmanagement: dynamische Grenze des Stromkreises zu klein\",\n 3: \"Lastmanagement: Notgrenze zu klein\", 4: \"Lastmanagement: Absicherung zu klein\",\n 5: \"Lastmanagement: in der Warteschlange\",\n 6: \"Lastmanagement: Warteschlange, Fahrzeug ist voll\",\n 7: \"Netzform konnte nicht erkannt werden\",\n 8: \"Keine Stromanforderung vom Fahrzeug\", 9: \"Verbindung zur Hauptbox verloren\",\n 10: \"Strom vom Equalizer zu gering\", 11: \"Phase nicht angeschlossen\",\n 25: \"Begrenzt durch die Absicherung\", 26: \"Begrenzt durch den Stromkreis\",\n 27: \"Begrenzt durch die dynamische Grenze des Stromkreises\",\n 28: \"Begrenzt durch den Equalizer\", 29: \"Begrenzt durch das Lastmanagement\",\n 30: \"Begrenzt durch die Offline-Einstellungen\",\n 50: \"Kein Fahrzeug angeschlossen\", 51: \"Maximalstrom der Wallbox zu klein\",\n 52: \"Dynamische Grenze der Wallbox zu klein\", 53: \"Wallbox ist deaktiviert\",\n 54: \"Zeitsteuerung aktiv\", 55: \"Wartet auf Freigabe\", 56: \"Wallbox meldet Stoerung\",\n 57: \"Fahrzeug verhaelt sich unregelmaessig\", 75: \"Begrenzt durch das Kabel\",\n 76: \"Begrenzt durch die Zeitsteuerung\", 77: \"Begrenzt durch den Maximalstrom\",\n 78: \"Begrenzt durch die dynamische Grenze\", 79: \"Fahrzeug laedt nicht\",\n 80: \"Grenze wurde gerade angepasst\", 81: \"Begrenzt durch das Fahrzeug\",\n 100: \"Unbekannter Fehler\"\n};\n\nconst mode = seen[109] === undefined ? null : Number(seen[109]);\nconst reasonCode = seen[96] === undefined ? null : Number(seen[96]);\nconst bool = (v) => (v === true || v === 1 || v === \"true\" ? 1 : 0);\nconst numberOf = (v) => {\n if (v === undefined || v === null || v === \"\") return null;\n const n = Number(v);\n return isNaN(n) ? null : n;\n};\nconst round2 = (n) => Math.round(n * 100) / 100;\n\nconst values = {};\nif (mode !== null) {\n values.STATE = mode;\n values.CHARGING = mode === 3 ? 1 : 0;\n values.CONNECTED = [2, 3, 4, 6, 7, 8].indexOf(mode) >= 0 ? 1 : 0;\n values.MALFUNCTION = mode === 5 ? 1 : 0;\n}\nconst power = numberOf(seen[120]);\nif (power !== null) { values.POWER = Math.round(power * 1000); } // kW -> W\nconst session = numberOf(seen[121]);\nif (session !== null) { values.ENERGY_SESSION = round2(session); }\nconst total = numberOf(seen[124]);\nif (total !== null) { values.ENERGY_TOTAL = round2(total); }\nconst current = numberOf(seen[114]);\nif (current !== null) { values.CURRENT = round2(current); }\nconst limit = numberOf(seen[48]);\nif (limit !== null) { values.LIMIT = Math.round(limit); }\nif (seen[103] !== undefined) { values.CABLE = bool(seen[103]); }\nif (seen[102] !== undefined) { values.SMART = bool(seen[102]); }\nif (seen[250] !== undefined) { values.ONLINE = bool(seen[250]); }\nif (reasonCode !== null) {\n values.REASON = REASONS[reasonCode] !== undefined\n ? REASONS[reasonCode] : (\"Code \" + reasonCode);\n}\nif (values.STATE !== undefined && values.REASON === undefined) { values.REASON = \"\"; }\n\nconst topic = root + \"/\" + serial;\nconst out = Object.keys(values).map((key) => ({\n topic: \"stat/\" + topic + \"/\" + key, payload: values[key], retain: true\n}));\n// Das Lebenszeichen beschreibt die Wallbox: Betriebsart 0 heisst offline\nconst online = mode === null ? false : mode !== 0;\nout.push({ topic: \"tele/\" + topic + \"/LWT\", payload: online ? \"Online\" : \"Offline\",\n retain: true });\nif (!out.length) { return null; }\nreturn [out];\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[]},{"id":"nomos_easee_out","type":"mqtt out","z":"nomos_easee","name":"an nomos","x":1230,"y":240,"wires":[],"topic":"","qos":"0","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"nomos_easee_broker"},{"id":"nomos_easee_in","type":"mqtt in","z":"nomos_easee","name":"Befehle aus nomos","x":150,"y":360,"wires":[["nomos_easee_command"]],"topic":"cmnd/easee/+/+","qos":"0","datatype":"auto-detect","broker":"nomos_easee_broker","nl":false,"rap":true,"rh":0,"inputs":0},{"id":"nomos_easee_command","type":"function","z":"nomos_easee","name":"Befehl umsetzen","x":390,"y":360,"wires":[["nomos_easee_send"]],"func":"// Befehl aus nomos in einen Aufruf der Schnittstelle uebersetzen.\n// Topic: cmnd/<Wurzel>/<Seriennummer>/<BEFEHL>\nconst token = flow.get(\"easeeToken\");\nif (!token) { node.warn(\"noch nicht angemeldet - Befehl verworfen\"); return null; }\n\nconst parts = String(msg.topic).split(\"/\");\nconst command = parts[parts.length - 1];\nconst serial = parts[parts.length - 2];\nconst chargers = (flow.get(\"easeeChargers\") || []).map((s) => String(s).trim());\nif (chargers.indexOf(serial) < 0) {\n node.warn(\"unbekannte Seriennummer im Topic: \" + msg.topic);\n return null;\n}\n\nconst value = String(msg.payload).trim();\nconst on = value === \"1\" || value.toLowerCase() === \"on\" || value.toLowerCase() === \"true\";\nconst base = \"https://api.easee.com/api/chargers/\" + encodeURIComponent(serial) + \"/commands/\";\n\nif (command === \"POWER\") {\n // Pause und Fortsetzen lassen die Ladesitzung bestehen\n msg.url = base + (on ? \"resume_charging\" : \"pause_charging\");\n msg.payload = {};\n} else if (command === \"LIMIT\") {\n const amps = Math.round(Number(value));\n if (isNaN(amps) || amps < 0 || amps > 32) {\n node.warn(\"Stromgrenze ausserhalb 0 bis 32 A: \" + value);\n return null;\n }\n // minutes = 0 heisst: die Grenze gilt, bis eine andere gesetzt wird\n msg.url = base + \"set_dynamic_charger_current\";\n msg.payload = { amps: amps, minutes: 0 };\n} else {\n node.warn(\"unbekannter Befehl: \" + command);\n return null;\n}\n\nmsg.method = \"POST\";\nmsg.headers = { \"Authorization\": \"Bearer \" + token, \"content-type\": \"application/json\" };\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[]},{"id":"nomos_easee_send","type":"http request","z":"nomos_easee","name":"Befehl senden","x":620,"y":360,"wires":[["nomos_easee_poll"]],"method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[]}]
Danach zwei Knoten anpassen:
- Zugangsdaten — Benutzername und Kennwort des Kontos sowie die Liste der Seriennummern. Mehrere Wallboxen werden als Liste eingetragen:
["EH123456", "EH999999"]. - an nomos und Befehle aus nomos — den Broker eintragen.
Das Gerät in nomos anlegen
- Skills → Add-ons: das Profil importieren
- Je Wallbox ein Gerät anlegen und als MQTT-Topic
easee/<Seriennummer>eintragen:
easee/EH123456
Was der Flow veröffentlicht
| Topic | Inhalt |
|---|---|
stat/<Topic>/STATE |
Betriebsart als Zahl: 0 offline, 1 nicht verbunden, 2 wartet auf Start, 3 lädt, 4 abgeschlossen, 5 Störung, 6 bereit, 7 wartet auf Freigabe, 8 Freigabe wird aufgehoben |
stat/<Topic>/CHARGING |
1, solange geladen wird |
stat/<Topic>/CONNECTED |
1, solange ein Fahrzeug angeschlossen ist |
stat/<Topic>/POWER |
Leistung in W |
stat/<Topic>/CURRENT |
Ladestrom in A |
stat/<Topic>/ENERGY_SESSION |
Energie der laufenden Ladung in kWh |
stat/<Topic>/ENERGY_TOTAL |
Energie seit Inbetriebnahme in kWh |
stat/<Topic>/LIMIT |
geltende Stromgrenze in A |
stat/<Topic>/REASON |
im Klartext, warum gerade nicht geladen wird |
stat/<Topic>/MALFUNCTION |
1, wenn die Wallbox eine Störung meldet |
stat/<Topic>/CABLE, SMART, ONLINE |
ausgeblendete Zusatzwerte |
tele/<Topic>/LWT |
Online, solange die Wallbox erreichbar ist |
In Gegenrichtung hört der Flow auf cmnd/easee/<Seriennummer>/POWER und /LIMIT.
Datenbereich
- Status, Lädt, Fahrzeug angeschlossen
- Leistung, Ladestrom, Energie der Ladung und Energie gesamt
- Stromgrenze und Warum es nicht lädt
- Störung (
malfunction) - Kabelverriegelung, intelligentes Laden und die Verbindung zum Rechenzentrum — ausgeblendet
- Erreichbar (
reachable)
Schaltbar sind Ein/Aus und die Stromgrenze (0 bis 32 A).
Hinweise
- Aus pausiert, es meldet nicht ab. Die Ladesitzung bleibt bestehen und wird mit Ein fortgesetzt. Eine Freigabe per RFID oder App wird dadurch nicht aufgehoben.
- Die Stromgrenze ist eine vorübergehende Grenze. Sie gilt, bis eine andere gesetzt wird, und fällt weg, sobald die Wallbox neu startet oder das Kabel gezogen wird. Danach gilt wieder, was in der Wallbox dauerhaft eingestellt ist.
- 0 A hält das Laden an. Wer nur drosseln will, bleibt bei den üblichen 6 A als Untergrenze — darunter laden viele Fahrzeuge nicht mehr an.
- Jede Änderung geht über das Internet. Zwischen dem Schalten in nomos und der Reaktion der Wallbox liegen in der Regel ein paar Sekunden; ohne Internetverbindung geht gar nichts.
- Der Flow fragt im Minutentakt ab. Häufiger lohnt nicht und belastet das Konto unnötig; ein Aufruf holt alle Werte auf einmal.
- Die Anmeldung gilt eine Stunde. Der Flow erneuert sie vorher selbst, ohne sich erneut mit Kennwort anzumelden.
- Warum es nicht lädt ist der nützlichste Wert, wenn ein Fahrzeug hängt: er unterscheidet Lastmanagement, fehlende Freigabe, Zeitsteuerung und eine Begrenzung durch das Fahrzeug.
- Ein Konto, mehrere Wallboxen. Jede bekommt ihr eigenes Gerät in nomos; der Flow fragt sie nacheinander ab.
- Nicht im Profil enthalten: Ladehistorie und Abrechnung, Zeitpläne, Benutzerverwaltung, die Einstellungen des Stromkreises sowie der Equalizer.