Skip to content

About

Script to supervise charging of batteries using a Shelly PlusPlugS

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

batteryChargePilot

Akkuschonendes Laden eines E-Bike-Akkus (Cube Supreme Hybrid ONE 400, Bosch Compact Charger 2 A) über einen Shelly Plus Plug S – komplett autark auf dem Plug, ohne Cloud, App-Zwang oder Smart-Home-Zentrale.

Der Akku hat keine Datenschnittstelle. Der Ladestand wird deshalb nicht gemessen, sondern aus der Form der Leistungskurve abgeleitet: Li-Ion-Lader arbeiten im CC/CV-Verfahren, und der Übergang von konstantem Strom (CC) zu konstanter Spannung (CV) liegt bei ca. 80–85 % SoC. Genau dieser Knick ist an der Steckdose als Leistungsabfall messbar.

Leistung
  85 W ─────────────────╮              ← CC-Phase, Spitze wird gemerkt
                        ╰──╮           ← CV beginnt: Abfall = Cutoff
  68 W ── – – – – – – – – –╰──╮        ← 80 % der Spitze → AUTO-Stopp
                              ╰────╮
   0 W                              ╰──── Lader fertig (NORMAL-Modus)

Weil auf die relative Spitze statt auf feste Wattwerte geregelt wird, funktioniert das mit dem 2-A-Lader, dem 4-A-Lader und mit Fremdladegeräten ohne Umkonfiguration.

Dateien

Datei Zweck
shelly/battery-charge-pilot.js Das eigentliche Script, läuft auf dem Plug
shelly/button-probe.js Einmalige Diagnose: was meldet der Knopf?
test/simulate.js Simulator: stubbt die Shelly-API, prüft die Logik gegen synthetische Ladekurven (node test/simulate.js)

Warum kein Detached-Modus

Naheliegend wäre, den Knopf auf Detached zu stellen und single_push / double_push auszuwerten. Auf diesem Gerät funktioniert das nicht: der Plus Plug S hat laut offizieller Doku keine input:0-Komponente – die Komponentenliste umfasst nur System, WiFi, BLE, Cloud, MQTT, Websocket, switch:0, Script und PLUGS_UI. Ohne Input-Komponente gibt es keine Push-Events; das Gerät feuert ausschließlich switch:0-Toggles (Berichte dazu). Ein Menüpunkt „Input 0 → Type" existiert am Plug S ebenfalls nicht.

Nach einer Umstellung auf Detached wäre der Plug am Gerät gar nicht mehr bedienbar: der Knopf schaltet das Relais nicht mehr, und das Script bekommt nichts. Deshalb bleibt der Knopf im Auslieferungsmodus (momentary), und der Moduswechsel läuft über den Zyklus auf Relais-Ebene – siehe „Bedienung".

Schutzmechanismen

Kurz begründet, damit die Werte in CFG nicht versehentlich wegoptimiert werden:

  • CONFIRM_TICKS (3) – apower schwankt durch Netzspannung und Messrauschen. Ein einzelner Ausreißer unter dem Cutoff darf nicht abschalten, sonst endet das Laden bei beliebigem Füllstand.
  • SETTLE_TICKS (6) – ohne Anlaufphase würde die Einschaltspitze des Netzteils als Spitzenleistung gemerkt. Ein zu hoher Peak hebt den Cutoff mit an und schaltet dann systematisch zu früh ab.
  • PEAK_MIN (30 W) + RUN_TICKS (6) – mit fast vollem Akku ist die Kurve schon in der CV-Phase, „80 % der Spitze" wäre dann Fiktion. Es muss erst eine glaubwürdige CC-Phase gesehen worden sein.
  • DONE_TICKS (18) – trennt auch im Normal-Modus nach Ladeende, damit das Ladegerät nicht dauerhaft am Netz hängt (Standby → 0).
  • TIMEOUT_TICKS (2880) – harte Notabschaltung nach 8 h. Hängt an derselben 10-s-Schleife wie alles andere und greift damit auch, wenn das Script bei bereits eingeschalteter Dose startet. Verbrauch: 1 der maximal 5 Timer pro Script, plus kurzzeitig ein zweiter für den LED-Write. Nicht unter 7 h setzen: Bosch nennt für Compact Charger 2 A + PowerPack 400 6,5 h für eine volle Ladung, ein Händler sogar 7–8 h. Der frühere Wert von 6 h hätte genau die monatliche Balancing-Ladung abgeschnitten – rote LED bei ~98 % statt weiß.
  • setLed() cached – PLUGS_UI.SetConfig schreibt persistent ins Flash, daher nur bei echter Farbänderung (3 Writes pro Ladevorgang).
  • setLed() schickt per Timer ab – nicht kosmetisch, sondern zwingend: direkt aus dem Status-Handler heraus kommt die Payload verstümmelt an. Siehe „Fallstricke der Shelly-Plattform".

Verifikation

Die Zustandsmaschine ist gegen einen Simulator geprüft, der die Shelly-API (Shelly.*, Timer.*) mit virtueller Zeit stubbt und synthetische Ladekurven durchschiebt – inklusive Anlaufspitze, Messrauschen und groben Ausreißern. Damit lassen sich Fälle testen, die auf echter Hardware jeweils Stunden dauern würden.

node test/simulate.js     # 21/21 bestanden

Abgedeckt: Auto-Stopp landet in der CV-Phase (nicht in der CC-Phase), die 140-W-Anlaufspitze wird verworfen (erkannter Peak: 88 W statt 140 W), Einzelausreißer auf 55 W lösen keinen Frühstopp aus, Normal-Modus trennt nach Ladeende, der dritte Tastendruck schaltet endgültig aus, nach einem Auto-Stopp startet der nächste Lauf wieder in NORMAL, Timeout greift bei 8 h und schneidet eine volle Ladung nicht ab, Leerlauf wird erkannt, es bleiben 3 LED-Schreibzugriffe pro Ladevorgang, und die Kalibriermessung setzt genau einen Messpunkt, ohne den Ladevorgang zu verkürzen.

Nicht simulierbar und daher offen: das echte Verhalten des Bosch-Laders und ob apower auf deinem Gerät so rauscharm ist wie angenommen. Der erste Ladevorgang ist der eigentliche Test – die Konsole protokolliert alles Nötige.

Installation

  1. Web-Oberfläche des Plug S öffnen (IP im Browser).
  2. Nichts an den Input-/Button-Einstellungen ändern – der Knopf bleibt momentary.
  3. Scripts → Create script, Inhalt von shelly/battery-charge-pilot.js einfügen, Save, dann Start.
  4. Run on startup aktivieren, damit das Script einen Stromausfall übersteht.
  5. Empfohlen: Settings → Power on default state auf Off setzen. Sonst startet nach einem Stromausfall ein voller Ladevorgang, ohne dass du es wolltest.

Bedienung

Ein Moduszyklus, ein Tastendruck pro Schritt:

Zustand Tastendruck Ergebnis LED
aus 1. ein, NORMAL – lädt voll durch grün
ein / NORMAL 2. ein, AUTO – Stopp bei ~80 % blau
ein / AUTO 3. aus siehe unten

Der Sprung von NORMAL auf AUTO ist am Klicken des Relais hörbar: das Script schaltet die Dose nach ~100 ms selbst wieder ein. Wer die Dose im Normal-Modus wirklich ausschalten will, drückt also zweimal.

Die LED im Aus-Zustand zeigt, wie der letzte Vorgang endete:

Farbe Bedeutung
türkis Auto-Stopp bei ~80 %, abfahrbereit
weiß Ladegerät war von selbst fertig (100 %)
rot Sicherheits-Timeout – bitte nachsehen
dunkel manuell beendet, oder es wurde nie geladen

Alltag: Stecker rein, zweimal drücken → blau. Das Rad steht später mit türkiser LED bei ~80 % und ist abfahrbereit.

Einmal im Monat oder vor einer langen Tour: einmal drücken → grün → volles Laden inklusive Zellen-Balancing durch das Bosch-BMS. Danach trennt das Script von selbst (weiße LED).

Kalibrierung

Die Voreinstellungen sollten für den Bosch Compact Charger passen. Nach dem ersten Ladevorgang lohnt ein Blick in die Script-Konsole – sie protokolliert Spitzenwert, Cutoff und jeden Abfall-Zähler.

Zwei Stellschrauben in CFG:

  • DROP_FACTOR (Standard 0.80) – niedriger = später abschalten, also höherer Ladestand. 0.85 schaltet früher.
  • MIN_POWER (Standard 15) – muss über dem Standby-Verbrauch des Ladegeräts, aber klar unter der Ladeleistung liegen.

Wichtige Einordnung: Der Plug misst die Leistung vor dem Ladegerät, inklusive dessen Eigenverluste von grob 10–15 W. Diese Verluste sind nahezu konstant und flachen den relativen Abfall ab: Halbiert sich der Akkustrom, sinkt die Steckdosenleistung von z. B. 85 W nur auf ~49 W, nicht auf ~43 W. 20 % Abfall an der Steckdose entsprechen deshalb einem deutlich größeren Rückgang des Akkustroms – der reale Stopp liegt eher bei 85–90 % SoC als bei exakt 80 %. Für die Lebensdauer ist das immer noch der ganz überwiegende Teil des Nutzens; wer näher an 80 % will, erhöht DROP_FACTOR auf ~0.88 (höherer Wert = kleinerer nötiger Abfall = früherer Stopp).

Den Cutoff vermessen statt schätzen

Eine NORMAL-Ladung aus dem möglichst leeren Akku vermisst den Cutoff selbst. Das Script wertet die Abfallbedingung in beiden Modi aus, schaltet in NORMAL aber nicht ab, sondern merkt sich nur den Energiezählerstand und lädt durch. Am Ladeende vergleicht es beide Zählerstände und schreibt das Ergebnis in die KVS:

curl http://<plug>/rpc/KVS.GetMany
# -> bcp_cal = pct=83 cutoff_wh=235 full_wh=285 peak_w=88

pct ist der Anteil dieser Ladung, bei dem AUTO gestoppt hätte – bei leerem Startakku also näherungsweise der Ladestand am Cutoff. Der Eigenverbrauch des Laders fällt bei der Differenzbildung weitgehend heraus. Dieselbe Zahl steht auch als Kalibrierung: … in der Konsole.

Ist pct höher als gewünscht, DROP_FACTOR erhöhen; ist er niedriger, senken.

2 A oder 4 A macht einen Unterschied

Der Umschaltpunkt CC→CV hängt am Ladestrom, nicht am Script: die CC-Phase endet, wenn die Klemmenspannung die Ladeschlussspannung erreicht, und die ist OCV + I·Rinnen. Bei halbem Strom ist der Aufschlag halb so groß, der echte Ladestand beim Umschalten also höher. In der Literatur ist die Zelle beim Erreichen der Maximalspannung „usually about 70 to 80 % charged, depending on what the C rate is".

Am PowerPack 400 (11 Ah) bedeutet das:

Lader C-Rate Ladezeit 100 % (Bosch) geschätzter Stopp
Compact 2 A 0,18 C 6,5 h ~85 % SoC
Standard 4 A 0,36 C 3,5 h ~78 % SoC

Die Schätzung stützt sich auf eine gemessene 4A-Kurve (PowerTube 625, konstant 4,14 A bis Minute 183, danach CV bis Minute 290, knapp 600 Wh) – daraus ergibt sich der Knick bei rund 75 % bei 0,23 C. Unsicherheit ±5 Prozentpunkte. Wer es genau wissen will, nimmt die Messung oben.

Praktische Folge: nach einem Ladegerätwechsel neu kalibrieren. Mit einem 4A-Lader stoppt dasselbe DROP_FACTOR deutlich früher.

Diagnose des Knopfes (optional)

Falls dich interessiert, ob deine Firmware inzwischen doch Tastenevents liefert: shelly/button-probe.js als zweites Script anlegen, starten, Knopf drücken, Konsole lesen. Erscheinen dort tatsächlich single_push / double_push, ließe sich ein echter Doppelklick nachrüsten – der Moduszyklus funktioniert aber ohnehin, es besteht kein Handlungsdruck.

Fallstricke der Shelly-Plattform

Alles auf Firmware 1.7.5 (SNPL-00112EU) am Gerät verifiziert. Jeder Punkt hier hat einmal Stunden gekostet.

Shelly.call kürzt Parameter abhängig von der Stacktiefe. Der mJS-Serializer hat ein Tiefenbudget, das von der aktuellen Stacktiefe abhängt. Aus dem Top-Level wird eine verschachtelte params-Struktur vollständig serialisiert; aus addStatusHandler → onOutput() → setLed() heraus fallen die inneren Objekte weg, und die Firmware antwortet mit -103 Missing or bad argument 'config'. Sichtbar machen: dieselbe Struktur durch JSON.stringify schicken – im Fehlerfall stehen dort ... statt der Objekte. Lösung: den Aufruf per Timer.set(20, false, …) aus flachem Stack abschicken, so wie setLed() es tut.

params muss ein Objekt sein. Ein JSON-String wird mit demselben -103 abgelehnt – obwohl genau dieser String per HTTP-RPC einwandfrei durchgeht.

Ohne Callback verschluckt Shelly.call jeden Fehler. Der dreistellige Callback function (res, ec, em) ist Pflicht, sonst scheitern Aufrufe unsichtbar. Genau deshalb blieb der LED-Bug so lange unentdeckt.

Maximal 5 Timer pro Script. Ein sechster Timer.set beendet das Script mit Uncaught Error: Too many running timers.

POST /rpc scheitert bei aktivierter Auth. curl schickt die erste Anfrage unauthentifiziert mitsamt Body, die Firmware antwortet mit 400 statt 401, und curl kommt nie zur Digest-Runde. Mit Auth deshalb GET verwenden – jede RPC-Methode kann das:

curl --digest -u 'admin:PW' -G http://<plug>/rpc/PLUGS_UI.GetConfig
curl --digest -u 'admin:PW' -G http://<plug>/rpc/PLUGS_UI.SetConfig \
  --data-urlencode 'config={"leds":{"mode":"switch"}}'

Nebenbei: --basic 'admin:PW' ist falsch – --basic nimmt kein Argument und curl deutet die Credentials dann als zweite URL („Port number was not a decimal number"). Zugangsdaten gehören hinter -u, und Gen2 spricht Digest, nicht Basic.

Script.PutCode verträgt nur kleine Bodies. Ab wenigen KB kommt „Request body too big". Code in ~600-Byte-Häppchen hochladen, das erste mit append: false, alle weiteren mit append: true.

Teil-Configs sind erlaubt. PLUGS_UI.SetConfig mergt: leds.colors ohne power und ohne night_mode wird akzeptiert. mode muss aber "switch" sein, damit die Schaltfarben überhaupt gelten – Werksdefault ist "power" (Verbrauchs-Farbskala), und dort werden die On/Off-Farben ignoriert. Die Werksfarbe für „ein" ist grün, was einen fehlgeschlagenen Write leicht als Erfolg missdeutet.

Debugging ohne Konsole: Script-Logs werden nicht persistiert. Wer Ergebnisse über HTTP auslesen will, schreibt sie mit KVS.Set weg und holt sie per curl http://<plug>/rpc/KVS.GetMany. Für einen Live-Log stattdessen Sys.SetConfig → debug.websocket.enable setzen und ws://<plug>/debug/log mitlesen.

Grenzen

  • Der Ladestand wird geschätzt, nicht gemessen. ±5–10 % SoC sind normal.
  • Bei Kälte (Garage im Winter) verläuft die Kurve flacher und unruhiger. Der Stopp kommt dann tendenziell später. Notfalls DROP_FACTOR saisonal anpassen.
  • Läuft der Akku ganz leer los, ist die CC-Phase lang und die Erkennung sauber. Bei nur teilentladenem Akku ist die Spitze kürzer – das Plausibilitäts-Gate (PEAK_MIN/RUN_TICKS) verhindert Fehlauslösungen, kann aber dazu führen, dass bei sehr vollem Akku gar nicht abgeschaltet wird und der Lader normal bis 100 % durchläuft. Das ist beabsichtigt.
  • Das Script ersetzt keine Ladeüberwachung. Li-Ion-Akkus nicht unbeaufsichtigt auf brennbarem Untergrund laden.

About

Script to supervise charging of batteries using a Shelly PlusPlugS

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages