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.
| 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) |
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".
Kurz begründet, damit die Werte in CFG nicht versehentlich wegoptimiert
werden:
CONFIRM_TICKS(3) –apowerschwankt 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.SetConfigschreibt 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".
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.
- Web-Oberfläche des Plug S öffnen (IP im Browser).
- Nichts an den Input-/Button-Einstellungen ändern – der Knopf bleibt
momentary. Scripts→Create script, Inhalt vonshelly/battery-charge-pilot.jseinfügen,Save, dannStart.Run on startupaktivieren, damit das Script einen Stromausfall übersteht.- Empfohlen:
Settings→Power on default stateaufOffsetzen. Sonst startet nach einem Stromausfall ein voller Ladevorgang, ohne dass du es wolltest.
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).
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(Standard0.80) – niedriger = später abschalten, also höherer Ladestand.0.85schaltet früher.MIN_POWER(Standard15) – 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).
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=88pct 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.
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.
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.
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.
- 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_FACTORsaisonal 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.