Dieses Projekt ist meiner Tochter Elisa gewidmet.
Gleich ausprobieren? Mit Docker läuft ELMA in wenigen Minuten lokal, ganz ohne Energiemanager: siehe Lokal testen.
Wenn die Sonne scheint, produziert eine PV-Anlage oft mehr Strom, als der Haushalt gerade braucht. Dieser Überschuss wird meist für wenige Cent ins Netz eingespeist. Gleich nebenan zahlen Nachbarn, Familie oder Freunde für ihren Strom den vollen Preis. Sie wissen aber nicht, wann es sich lohnen würde, die Waschmaschine einzuschalten.
Auslöser für ELMA ist das Peer-to-Peer-Teilen von Strom. Haushalte sind dabei nicht mehr nur Kunden eines Energieversorgers. Sie geben ihren Strom direkt untereinander weiter, von Nachbar zu Nachbar. Der Strom fließt weiter über das öffentliche Netz, verbraucht und verrechnet wird er aber zwischen den Teilnehmern selbst.
In Österreich gibt es dafür seit 1. Oktober 2026 die Peer-to-Peer-Verträge (P2P) nach dem neuen Elektrizitätswirtschaftsgesetz (ElWG).
- Zwei oder mehr Personen schließen sich zusammen, um ihren selbst erzeugten Strom gemeinsam zu nutzen.
- Dafür reicht ein Vertrag. Anders als bei einer Energiegemeinschaft muss kein Verein und keine Genossenschaft gegründet werden.
- Es gibt Mustervorlagen für einen Erzeuger mit einem oder mehreren Abnehmern (1:1 / 1:n). Das ist genau der Fall, für den ELMA gebaut ist.
Laut der Seite ist die Umsetzung vorerst nur zwischen Zählpunkten beim selben Netzbetreiber möglich. Vergünstigte Netzentgelte sollen ab 2027 gelten. Der aktuelle Stand steht auf energiegemeinschaften.gv.at. Dasselbe Prinzip gilt auch für Energiegemeinschaften (EEG/BEG).
Dabei fehlt eine Kleinigkeit: Das Teilen funktioniert nur, wenn der Empfänger den Strom genau dann verbraucht, wenn er erzeugt wird. Strom lässt sich im Netz nicht „aufheben“. Der Überschuss der PV-Anlage am Mittag ist nur dann geteilter Strom, wenn nebenan in diesem Moment die Waschmaschine läuft. Ohne Information darüber wird dem Zufall überlassen, was man sich teilt.
ELMA schließt diese Lücke:
- Der Erzeuger teilt seinen aktuellen Stromüberschuss.
- Die Empfänger im Peer-to-Peer-Netz sehen ihn live am Handy und legen ihren Verbrauch gezielt in diese Zeit.
- So wird aus dem Teilen auf dem Papier tatsächlich geteilter Sonnenstrom. Mehr Strom bleibt lokal, das Netz wird entlastet, und beide Seiten profitieren.
ELMA übernimmt dabei nicht die Verrechnung. Die läuft wie bisher über die Energiegemeinschaft bzw. den Netzbetreiber. ELMA liefert die Information, wann sich das Teilen lohnt.
- Live-Anzeige des Überschusses: eine große Zahl mit Ampel (viel / etwas / kein Überschuss) und dem Verlauf der letzten 24 Stunden.
Die Zahl ist das Mittel der letzten 5 Minuten (
SMOOTH_WINDOW_MS). Darunter steht, ob der Überschuss in dieser Zeit stabil war oder stark schwankt: Stabil heißt, er lag mindestens 80 % der Zeit bei wenigstens 70 % des Mittels und liegt auch jetzt noch dort. Die Ampel erscheint erst, wenn Werte für die vollen 5 Minuten da sind. - Akku des Erzeugers: „Lädt 1,2 kW · 92 %“ (ab 200 W) oder „Akku voll“ (ab 98 %). So sieht der Empfänger, warum gerade
nichts frei ist oder dass bald mehr kommt. Steht oder entlädt der Akku, erscheint nichts. Die Bridge liest dafür
BATTERY_TOPIC(siehe Payload-Format). - Geräte statt Watt: Viele können mit „1,4 kW“ wenig anfangen. Deshalb zeigt ELMA mit Symbolen, was gerade möglich ist, z. B. „✓ 🧺 Waschmaschine“ oder „noch 650 W bis 🫖 Wasserkocher“. Jeder wählt seine eigenen Geräte aus und kann auch eigene anlegen, z. B. eine Poolpumpe mit 800 W.
- Push-Nachricht: z. B. „Jetzt reicht's für 🧺 Waschmaschine“, sobald eines der eigenen Geräte mit dem Überschuss laufen kann. Das passiert erst nach 2 Minuten stabilem Überschuss, damit kurze Wolken keinen Fehlalarm auslösen, und nie, solange er stark schwankt.
- Strom reservieren (ELISA-Modus): Ein Empfänger, dem der Erzeuger das Recht gibt, reserviert den Überschuss für bis zu 60 min am Tag. Der Erzeuger bekommt eine Nachricht, und seine Hausautomation (Node-RED) lässt Wallbox und Wärmepumpe so lange nicht anspringen. Reservieren geht sofort oder geplant für später am selben Tag. Dann wird die Wärmepumpe schon eine Stunde vorher zurückgehalten. Konzept und Entscheidungen: docs/elisa.md.
- Einladung per Link: Der Erzeuger lädt Empfänger mit einem Link ein, z. B. über WhatsApp. Ohne Einladung kann sich niemand registrieren, die Daten bleiben also im kleinen Kreis.
- Anmelden mit Passkey: per Fingerabdruck, Gesicht oder Geräte-PIN. Ein Passwort gibt es nicht.
- Wie eine App am Handy: Die App lässt sich auf Android und iPhone über „Zum Startbildschirm hinzufügen“ installieren. Kein App Store ist nötig.
- Läuft zuhause: Die Daten kommen direkt vom eigenen Energiemanager, z. B. Smartfox über MQTT. ELMA läuft auf einem Raspberry Pi im Heimnetz und ist über einen Cloudflare Tunnel erreichbar. Es fallen keine Cloud-Kosten an, und es müssen keine Ports geöffnet werden.
Der Überschuss wird aus einem MQTT-Topic des lokalen Brokers mysmarthome gelesen.
[Heimnetz, ein Docker-Gerät z. B. Raspberry Pi]
mysmarthome (MQTT) ──▶ bridge ──WebSocket──▶ backend (API + App) ◀── cloudflared
│ (ausgehender Tunnel)
[Internet] https://elma.deine-domain.at ◀┘ ◀── Handy (PWA)
- bridge (
apps/bridge): abonniert das Topic, rechnet den Wert auf Watt um und schickt ihn ans Backend. Es wird höchstens alle 5 s gesendet, bei Sprüngen ab 50 W sofort. - backend (
apps/backend): Fastify + SQLite (node:sqlite). Es verwaltet Login, Einladungen und Freigaben, verteilt Live-Werte per WebSocket, speichert Minutenmittel (30 Tage) und liefert die App aus. - web (
apps/web): React-PWA. Am Handy lässt sie sich über „Zum Startbildschirm hinzufügen“ wie eine App installieren. - shared (
packages/shared): gemeinsame Typen und Schemas.
Im Heimnetz müssen keine Ports geöffnet werden: Der Cloudflare Tunnel baut die Verbindung von innen nach außen auf.
Abgeschottetes Backend: Das Backend hängt nur in einem internen Docker-Netz. Es erreicht weder das Heimnetz (z. B. andere Dienste auf dem Pi) noch das Internet, und am Gerät ist kein Port für ELMA offen. Falls jemand über die Anmeldung eindringt, kommt er so nicht weiter ins Heimnetz.
- cloudflared (Tunnel) und die Bridge (MQTT) hängen zusätzlich im normalen Netz.
- push-proxy (
apps/push-proxy): ein kleiner Ausgangs-Proxy für Push-Nachrichten. Er lässt nur verschlüsselte Verbindungen zu den Push-Diensten von Google, Apple, Mozilla und Microsoft durch. Push-Abos mit anderen Adressen nimmt das Backend gar nicht erst an. - Die Verwaltung per CLI geht weiterhin mit
docker compose exec backend ….
Voraussetzung ist nur Docker.
docker compose -f dev/docker-compose.dev.yml up -d --build mosquitto backenddocker compose -f dev/docker-compose.dev.yml exec backend node src/cli.ts create-user [email protected]Das gibt einen Einrichtungslink aus. Darüber legst du im Browser deinen Passkey an.
Passkeys funktionieren auf localhost auch ohne HTTPS.
docker compose -f dev/docker-compose.dev.yml exec backend node src/cli.ts create-producer "PV Dach" [email protected]Das ausgegebene Token starten wir mit der Bridge:
DEVICE_TOKEN=<token> docker compose -f dev/docker-compose.dev.yml up -d bridgeEinen Testwert von 2300 W schicken:
docker compose -f dev/docker-compose.dev.yml exec mosquitto mosquitto_pub -t elma/test/surplus -m 2300Dann den Einrichtungslink öffnen (http://localhost:3000/?setup=…) und den Passkey anlegen.
-
Das Repo auf das Gerät im LAN kopieren und
cp .env.example .envausführen. DanachMQTT_TOPIC, das Payload-Format (siehe unten) undJWT_SECRETsetzen, z. B. mitopenssl rand -hex 32. -
Das Backend starten und die Zugänge anlegen:
docker compose up -d --build backend
docker compose exec backend node src/cli.ts create-user [email protected]
Das gibt einen Einrichtungslink aus. Damit legst du am Handy deinen Passkey an. Das geht erst über die öffentliche HTTPS-Adresse, also nach Schritt 4.
docker compose exec backend node src/cli.ts create-producer "PV Dach" [email protected]
Das ausgegebene
DEVICE_TOKENin die.enveintragen. -
Die Bridge starten und prüfen, ob Werte ankommen:
docker compose --profile bridge up -d bridge
docker compose logs -f bridge
-
Cloudflare Tunnel einrichten (kostenlos, braucht eine Domain bei Cloudflare). Ausführliche Anleitung mit Stolperfallen: docs/cloudflare.md.
- Im Cloudflare-Dashboard unter Zero Trust → Networks → Tunnels einen Tunnel anlegen (Typ Cloudflared).
Das Token kommt als
CLOUDFLARE_TUNNEL_TOKENin die.env. - Einen Public Hostname anlegen, z. B.
elma.deine-domain.at→ Servicehttp://backend:3000. PUBLIC_URL=https://elma.deine-domain.atin der.envsetzen und dann starten:docker compose --profile bridge --profile tunnel up -d
Ohne eigene Domain reicht zum Ausprobieren ein Quick-Tunnel mit einer zufälligen
*.trycloudflare.com-Adresse. Er läuft über den Dienstcloudflared, denn nur der hängt im internen Netz beim Backend und zugleich im Netz nach außen:docker compose run --rm cloudflared tunnel --no-autoupdate --url http://backend:3000
- Im Cloudflare-Dashboard unter Zero Trust → Networks → Tunnels einen Tunnel anlegen (Typ Cloudflared).
Das Token kommt als
Die Version steht in der Root-package.json (SemVer). Zu jedem Release gehört ein Git-Tag.
- In
package.json(und den Paketen unterapps/*,packages/*) die Version erhöhen. - Committen und taggen, mit
-aund Beschreibung, denn nur solche Tags überträgtgit push --follow-tags:git tag -a vX.Y.Z -m "ELMA vX.Y.Z" - Pushen:
Der Tag startet
git push --follow-tags
release.yml: Images für die ELMA-Box und die GitHub Release mit Release-Notes aus den Commits (siehe docs/hosting.md, „Images veröffentlichen“). Die Notizen bis 0.11.1 stehen in CHANGELOG.md. - Auf den Pi ausrollen:
scripts/deploy.sh
Das Skript bricht ab, wenn es nicht committete oder nicht gepushte Änderungen gibt.
Es vergibt die Version aus git describe (z. B. vX.Y.Z, oder vX.Y.Z-2-gabc1234 für Zwischenstände) und ersetzt den Code auf dem Pi.
Die .env und die Daten bleiben dabei erhalten.
Danach baut es die Container neu und prüft über <PUBLIC_URL>/api/health, dass die neue Version läuft.
Die Adresse kommt aus PUBLIC_URL in der .env auf dem Ziel; mit DEPLOY_URL=… lässt sie sich überschreiben.
Die laufende Version steht in der App ganz unten.
| Payload im Topic | .env |
|---|---|
1234 (Watt) |
PAYLOAD_UNIT=W |
1.23 (Kilowatt) |
PAYLOAD_UNIT=kW |
{"data":{"surplus": 1234}} |
PAYLOAD_PATH=data.surplus |
-1234 (Einspeisung negativ) |
PAYLOAD_INVERT=true |
Findet die Bridge keine Zahl, schreibt sie eine Warnung mit der Payload ins Log. So lässt sich das Format schnell herausfinden.
Für den Akku-Hinweis liest die Bridge zusätzlich BATTERY_TOPIC (z. B. myhome/ecoflow/state). Das Format ist fest, das von ecoflowd:
{"battery":515,"soc":63,"timestamp":"2026-09-22T09:13:16Z"}, battery in W und positiv beim Laden, soc in %, timestamp als Messzeit.
Ein Akkuwert, der älter als 5 Minuten ist, wird nicht mehr angezeigt. Leer lassen, wenn es keinen Akku gibt.
Neues Backend zuerst ausrollen, dann die Bridge: Ein altes Backend antwortet auf Akkuwerte mit „Ungültige Nachricht“.
Melde dich als Erzeuger in der App an und tippe neben dem Namen des Erzeugers auf das Personen-Symbol mit Plus. Im Blatt „Einladen“ trägst du optional den Namen der Person ein (z. B. „Margit“) und tippst auf „Einladungslink erstellen“. Danach teilst du den Link mit „Teilen“ (WhatsApp, Signal, Mail usw.) oder kopierst ihn. Der Link ist 7 Tage gültig und nur einmal verwendbar. Der Empfänger legt damit sein Konto an; ohne Einladung ist keine Registrierung möglich. Der Name steht dann gleich im neuen Konto und lässt sich später selbst ändern. Alternativ geht das auch per Kommandozeile, der Name ist optional:
docker compose exec backend node src/cli.ts invite <producerId> MargitDas Recht vergibt der Erzeuger im Blatt „Einladen“ (Personen-Symbol am Erzeuger) mit dem Schalter „ELISA-Modus“ neben dem Empfänger. Ohne Hausautomation bekommt der Erzeuger nur die Nachricht. Damit Node-RED Wallbox und Wärmepumpe zurückhält, braucht es ein Steuer-Token:
docker compose exec backend node src/cli.ts control-token <producerId>Node-RED schickt damit alle 15 s POST /api/control/status (Details in docs/elisa.md und KONZEPT-ELISA.md im Repo myhome).
Die Antwort { active, until, by, heatpumpUntil } trennt Wallbox und Wärmepumpe. active/until gelten nur für die laufende
Reservierung. heatpumpUntil ist schon 60 min vor einer geplanten gesetzt.
Das Token nur über einen sicheren Kanal weitergeben. Ein neues macht das alte ungültig.
Läuft die Bridge, veröffentlicht sie die Reservierung zusätzlich retained auf elma/reservation (anderes Topic per RESERVATION_TOPIC),
z. B. für die Strom-Ampel MAGGIE: {"active":true,"until":<Unix-ms>,"by":"Elisa"} bzw. {"active":false}.
Das Topic dient nur der Anzeige. Node-RED liest es bewusst nicht, denn der Broker ist offen und eine gefälschte Nachricht soll nichts schalten.
ELMA kennt keine Passwörter. Angemeldet wird mit einem Passkey: Das Handy speichert einen Schlüssel für ELMA, und man bestätigt per Fingerabdruck, Gesicht oder Geräte-PIN. Auf dem Server liegt nur der öffentliche Schlüssel.
- Neues Konto: über einen Einladungslink. Man gibt die E-Mail ein und tippt auf „Passkey anlegen“.
- Erster Zugang für den Betreiber:
create-user <email>gibt einen Einrichtungslink aus. - Neues Handy, Passkey verloren oder Konto aus der Passwort-Zeit: Einen neuen Einrichtungslink erzeugen.
Er ist 7 Tage gültig und nur einmal verwendbar.
Wer den Link hat, kann sich als dieser User anmelden, deshalb nur über einen sicheren Kanal weitergeben.
docker compose exec backend node src/cli.ts setup-link <email>
- Wer hat schon einen Passkey?
docker compose exec backend node src/cli.ts list-users - Name: Ebenfalls dort setzt jeder seinen Anzeigenamen, z. B. „Elisa“. Ohne Namen gilt der Teil der E-Mail vor dem @.
Als Betreiber geht es auch im Terminal:
docker compose exec backend node src/cli.ts set-name <email> Elisa. Der Name ist für Anzeigen bei anderen gedacht (zuerst für den ELISA-Modus, siehe docs/elisa.md), damit dort nie die E-Mail-Adresse steht. - Weitere Geräte: Über den Kreis mit dem Anfangsbuchstaben oben rechts öffnet sich das Konto-Blatt. Dort lassen sich Passkeys ansehen, hinzufügen und löschen, und dort steht auch „Abmelden“. Über das Google- bzw. Apple-Konto sind sie meist ohnehin auf allen eigenen Geräten. Am PC kann man sich auch anmelden, indem man den angezeigten QR-Code mit dem Handy scannt.
- Passkeys sind an die Domain gebunden (z. B.
elma.example.com). Bei einem Umzug auf eine andere Domain müssen alle neu angelegt werden. Über die LAN-Adresse (http://192.168.…) funktionieren sie nicht, nur über HTTPS oderlocalhost.
scripts/deploy.sh sichert vor jedem Ausrollen die Datenbank nach ~/elma-backups/ auf dem Pi.
Die letzten 10 Sicherungen bleiben erhalten, der Dateiname enthält Datum und bisherige Version.
Wiederherstellen, z. B. nach einem Zurücksteigen auf eine ältere Version:
cd ~/elma && docker compose stop backenddocker compose cp ~/elma-backups/<datei>.db backend:/data/elma.dbdocker compose run --rm --no-deps --entrypoint rm backend -f /data/elma.db-wal /data/elma.db-shmdocker compose start backendAktiviert werden sie in der App unter Geräte → ⚙️ Meine Geräte → 🔔 Benachrichtigungen. Dort markiert man die Geräte, für die man benachrichtigt werden will, z. B. „Jetzt reicht's für 🧺 Waschmaschine“.
- Eine Nachricht kommt erst, wenn das Gerät 2 Minuten durchgehend laufen könnte. Kurze Wolken lösen also nichts aus.
- Zeigt die Ampel „Schwankt stark“, gilt das Gerät als „passt nicht“, und die 2 Minuten beginnen neu.
- Pro Überschuss-Phase kommt höchstens eine Nachricht, und frühestens nach 1 Stunde wieder. Eine neue Phase beginnt erst, wenn der Überschuss unter 80 % der Geräteleistung gefallen ist.
- Die Schlüssel (VAPID) erzeugt das Backend beim ersten Start selbst und speichert sie in der Datenbank.
In der
.envgehört nurVAPID_SUBJECT=mailto:[email protected]gesetzt. - Web-Push braucht HTTPS, also den Cloudflare Tunnel (oder
localhostzum Testen). - Android: funktioniert in Chrome direkt.
- iPhone: funktioniert erst, nachdem ELMA auf dem Home-Bildschirm installiert ist (iOS 16.4 oder neuer).
- Android: Den Link in Chrome öffnen, dann Menü ⋮ → „App installieren“ bzw. „Zum Startbildschirm hinzufügen“ wählen.
- iPhone: Den Link in Safari öffnen, dann Teilen → „Zum Home-Bildschirm“ wählen.
Node ≥ 24 und pnpm werden gebraucht; alternativ läuft alles im Container, siehe unten.
pnpm installpnpm testpnpm typecheckOhne lokales Node laufen die Tests so im Container:
docker run --rm -v "$PWD":/app -w /app node:24-alpine sh -c "corepack enable && pnpm install && pnpm test"Auch andere Haushalte können als Erzeuger mitmachen: mit einem Shelly Pro 3EM, zentral auf deiner Instanz oder mit einer eigenen ELMA-Box. Konzept, Diagramme und Checkliste stehen in docs/hosting.md.
- Konto löschen und Freigabe entfernen (offen): Heute kann man im Terminal Konten anlegen, aber keines löschen und
auch keine Freigabe zurücknehmen. Dafür kommen zwei CLI-Befehle wie bei der übrigen Verwaltung, in der App ändert sich
nichts (Wolfgang, 2026-10-22):
delete-user <email>(Konto mit Passkeys, Push-Abos, Geräteauswahl, Freigaben und Reservierungen) undremove-recipient <producerId> <email>(der Empfänger sieht den Erzeuger nicht mehr, sein Konto bleibt). Dazu gehört:- Eine laufende Reservierung des Users endet über
ReservationService, mit Push und Ende-Grund, wie beim Entzug des Rechts (Lücke in docs/elisa.md, „Umgesetzt in 0.8.0“). - Eine offene Live-Verbindung bekommt danach keine Werte mehr. Heute ermittelt
/livedie erlaubten Erzeuger nur beim Verbinden. - Ein noch gültiges JWT nützt nichts mehr.
GET /api/meliefert dann schon 401, die anderen Routen prüfen nur das JWT. invites.used_byhat keinON DELETE CASCADE. Ein Konto, das eine Einladung eingelöst hat, lässt sich deshalb nicht löschen, ohne die Spalte vorher zu leeren.- Ist der User Erzeuger, löscht die Kaskade auch seinen Erzeuger mit dem ganzen Verlauf. Das braucht eine Rückfrage oder eine Weigerung, solange er noch Erzeuger hat.
- Eine laufende Reservierung des Users endet über
- Offen aus dem Review vom 26.10.2026:
- Das Abmelden der App, wenn
/livedas Token ablehnt (4401,apps/web/src/useLive.ts), hat keinen Test. Die Web-App hat keine Testumgebung für Hooks und Komponenten; ein Test bräuchte jsdom o. Ä. als neue Abhängigkeit. - Der Befehl für den Quick-Tunnel (Abschnitt „Produktiv im Heimnetz“) ist nicht ausprobiert, weil er das Backend
öffentlich macht. Offen ist auch, ob das leere
TUNNEL_TOKENausdocker-compose.ymlden Quick-Tunnel stört.
- Das Abmelden der App, wenn
- Android-App im Play Store über Capacitor (gleicher Code)
- Mehrere Erzeuger/Empfänger (Energiegemeinschaft); das Datenmodell ist dafür schon vorbereitet
Ideen, Fehlerberichte und Fragen sind als Issue willkommen. Wie Beiträge ablaufen, steht in CONTRIBUTING.md. Sicherheitslücken bitte nicht öffentlich melden, sondern wie in SECURITY.md beschrieben.
Copyright © 2026 Wolfgang Mathe
ELMA steht unter der PolyForm Noncommercial License 1.0.0, siehe LICENSE.md. Der Quellcode ist einsehbar, ELMA ist aber keine Open-Source-Software im engeren Sinn: Kommerzielle Nutzung ist nicht erlaubt.
Erlaubt, jeweils nicht-kommerziell:
- private Nutzung, z. B. im eigenen Haushalt, mit Nachbarn oder Freunden
- Nutzung durch gemeinnützige Organisationen, Schulen und Hochschulen, öffentliche Forschung, Umweltschutzorganisationen und Behörden
- ändern und weitergeben, jeweils mit dieser Lizenz und dem Copyright-Hinweis
Nicht erlaubt ohne Zustimmung des Urhebers:
- kommerzielle Nutzung, z. B. durch Unternehmen, als bezahlter Dienst oder in einem verkauften Produkt
Für eine kommerzielle Nutzung bitte Kontakt aufnehmen; eine separate Lizenz ist möglich.
Das Programm wird ohne jede Gewährleistung bereitgestellt.


