- Python 79.1%
- JavaScript 15.4%
- CSS 2.9%
- Shell 2.2%
- HTML 0.2%
- Other 0.2%
| .forgejo/workflows | ||
| deploy/truenas | ||
| docs | ||
| migrations | ||
| scripts | ||
| snlo | ||
| static | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .python-version | ||
| alembic.ini | ||
| CHANGELOG.md | ||
| compose.yaml | ||
| Dockerfile | ||
| LICENSE | ||
| MANIFEST.sha256 | ||
| pyproject.toml | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements-docs.txt | ||
| requirements.txt | ||
| snlo-test.sql | ||
| zensical.toml | ||
SNLO – Spittank.net Ladeorganisation 0.5.12
Laden organisiert.
SNLO – Spittank.net Ladeorganisation ist eine lokale Webapp/PWA für nutzerbezogene Ladeübersichten und Kostenaufstellungen aus beliebig vielen EVCC-Instanzen. EVCC ist verpflichtend und bleibt zuständig für Wallboxanbindung, PV- und Batteriemessung, Ladezustand, Fahrzeugerkennung, Lademodi und reale Sitzungskosten.
Optional kann je EVCC-Ladepunkt eine KEBA P30 über deren UDP-Schnittstelle ergänzt werden. Diese Zusatzanbindung verwendet für Freigabe und Sperren dieselbe RFID-Syntax wie die Home-Assistant-KEBA-Integration; nach erfolgreicher Aktion kann die Wallbox zusätzlich eine kurze Begrüßungs- bzw. Abschiedsmeldung anzeigen. Messwerte und abgeschlossene Sitzungen stammen weiterhin ausschließlich aus EVCC.
Dokumentation
Die vollständige Dokumentation wird mit Zensical aus den Markdown-Dateien unter docs/ gebaut:
pip install -r requirements-docs.txt
zensical build --config-file zensical.toml
Die Ausgabe liegt unter site/. Forgejo Actions validiert die Anwendung und Dokumentation, veröffentlicht main unter daniel.snii.de/snlo und baut ausschließlich bei v*-Tags das gemeinsame Multi-Arch-Image für API, Migrationen und Worker. Details: docs/CI-CD.md.
Funktionsumfang
Nutzer
- Anmeldung mit lokalem Konto oder gruppengesteuertem OpenID Connect
- Dashboard für Heute, Monat, Jahr und Gesamt
- Statistik zu Energie, Stromkosten, Aufschlägen, Solaranteil, Batterienutzung und Gesamtkosten
- laufende sichtbare Sitzung mit Fahrzeug, Tarif, Leistung, Energie und Kosten
- vollständig anonymisierte Anzeige
Belegt, wenn die aktive Sitzung keinem zugewiesenen Fahrzeug des Nutzers gehört - Sitzungsübersicht mit Zeitraumfilter
- Filter
Nur selbst bezahlte Fahrzeuge - Druckansicht der eigenen Kostenübersicht
- kein CSV-Export für Nutzer
- EVCC-Lademodus und zugewiesenes EVCC-Fahrzeug steuern; im Dashboard aktiven Lademodus direkt umschalten
- optional Wallbox mit hinterlegter KEBA-RFID freigeben und sperren; Sperrstatus live aus KEBA
report 2/Authreq
Administration
- beliebig viele EVCC-Instanzen anlegen, bearbeiten, testen, synchronisieren und löschen
- Nutzerkonten verwalten
- interne Fahrzeuge mit EVCC-Fahrzeugen aus mehreren Instanzen verknüpfen
- ein Fahrzeug mehreren Nutzern beziehungsweise Familienmitgliedern zuordnen
- je Fahrzeug und Zeitraum genau einen zahlenden Nutzer festlegen
- Fahrzeugzuweisungen bearbeiten und löschen, einschließlich rückwirkender Neuzuordnung
- benannte Tarife anlegen und bearbeiten
- globale Tarifzeiträume sowie Nutzer/Zeitraum/Tarif-Zuordnungen verwalten
- historische Tarifänderung nur nach ausdrücklicher Warnung und anschließender Neuberechnung
- Druckansicht eines beliebigen zahlenden Nutzers öffnen
- CSV-Export für administrative Auswertung
- KEBA-Verbindungen und RFID-Keys je Nutzer verwalten
- Audit-Protokoll
Fahrzeug- und Zahlerlogik
Die technische Erkennung und die Abrechnung sind getrennt:
EVCC-Fahrzeug
-> internes Fahrzeug
-> mehrere berechtigte Nutzer/Familienmitglieder
-> genau ein zahlender Nutzer je Zeitraum
Dasselbe reale Fahrzeug kann mit EVCC-Fahrzeugen aus mehreren Instanzen verknüpft werden. EVCC liefert in Sitzungen je nach Version oder Konfiguration teilweise den internen Namen und teilweise den sichtbaren Fahrzeugtitel. SNLO gleicht deshalb beide Werte ohne Beachtung der Groß-/Kleinschreibung ab.
Für eine Sitzung gilt:
- Eine explizite KEBA-App-Freigabe ordnet die Sitzung dem freigebenden Nutzer zu.
- Andernfalls wird der zum Sitzungsbeginn gültige zahlende Nutzer des internen Fahrzeugs verwendet.
- Fehlt eine eindeutige Zuordnung, bleibt die Sitzung administrativ prüfbar und zunächst unzugeordnet.
- Eine administrative Korrektur bleibt erhalten und wird nicht durch automatische Abgleiche überschrieben.
Normale Nutzer können entweder alle Sitzungen ihrer zugeordneten Fahrzeuge oder nur die von ihnen selbst bezahlten Sitzungen anzeigen. Administratorübersichten nennen ausschließlich den zahlenden Nutzer.
Tarifmodell
Tarife sind eigenständige, benannte Objekte, zum Beispiel:
Privat
Privat Plus
Gast
Dienstwagen
Ein Tarif enthält keine direkte Nutzer- oder Zeitraumzuordnung. Stattdessen gibt es:
- globale Tarifzuordnungen für einen Zeitraum,
- nutzerbezogene Tarifzuordnungen für einen Zeitraum.
Beispiel:
Alfred bis einschließlich 31.03.2026 -> Privat
Alfred ab 01.04.2026 -> Privat Plus
Eine nutzerbezogene Zuordnung hat Vorrang vor dem globalen Standard. Zeiträume dürfen innerhalb desselben Geltungsbereichs nicht überlappen.
Aufschlagskomponenten
| Komponente | Berechnungsbasis |
|---|---|
| Allgemein | gesamte geladene Energie |
| Solar | EVCC-Solaranteil der Sitzung |
| Netzbezug | Restenergie nach Abzug des EVCC-Solaranteils |
| Batterie | während des tatsächlichen Ladens gemessene positive Batterieentladung, höchstens die geladene Sitzungsenergie |
Es gibt keinen Monatsbeitrag.
Jede Komponente kann dargestellt werden als:
ITEMIZED: unterAufschlägeseparat ausgewiesenINCLUDED: in die sichtbarenStromkosteneingerechnet
Nutzer erhalten niemals den unveränderten EVCC-Rohpreis als eigene Kennzahl. Stromkosten bestehen aus den EVCC-Kosten zuzüglich aller als INCLUDED konfigurierten Komponenten. Intern bleiben EVCC-Basiswert und sämtliche Komponenten getrennt gespeichert.
Historische Änderungen
Abgeschlossene Sitzungen speichern Tarif-ID, Tarifname, Revision, Sätze, Darstellungsarten und berechnete Beträge. Wird ein bereits verwendeter Tarif bearbeitet, warnt die Administration ausdrücklich. Erst nach Bestätigung werden alle Sitzungen dieses Tarifs aus ihren unveränderten Messdaten neu berechnet und der Vorgang auditiert.
Batterieregel
Während ein EVCC-Ladepunkt tatsächlich lädt und die Ladeleistung oberhalb der konfigurierten Schwelle liegt, integriert der Collector die positive EVCC-Batterieleistung:
gemessene Entladung = Summe(max(0, Batterieleistung) × Zeit)
abrechenbare Batterieenergie = min(gemessene Entladung, Sitzungsenergie)
Batterieaufschlag = abrechenbare Batterieenergie × Batteriesatz
Beispiele:
10 kWh geladen, 5 kWh Batterieentladung -> 5 kWh Batteriebasis
10 kWh geladen, 15 kWh Batterieentladung -> 10 kWh Batteriebasis
Laden mehrere Fahrzeuge derselben EVCC-Instanz gleichzeitig, wird die gemessene Entladung proportional zu deren Ladeleistungen verteilt. Messlücken werden nicht erfunden oder rückwirkend extrapoliert. In Listen wird die konkret gemessene Batterienutzung gesamt angezeigt; der Aufschlag bleibt intern auf die Sitzungsenergie begrenzt.
KEBA-RFID-Freigabe
Der optionale Adapter unterstützt aktuell P30_UDP:
- Verbindung je EVCC-Ladepunkt mit Host, UDP-Port, Timeout und optionalem geheimen Zusatzfeld
- RFID-UID je Nutzer und KEBA-Verbindung; die 8-stellige UID aus der KEBA-Kartenliste wird unverändert verwendet
- verschlüsselte Speicherung; der Klartext wird weder an Browser noch in Listen zurückgegeben
- App-Freigabe mit
start <tag> 01010400000000000000; der RFID-Class-Wert wird intern gesetzt - Beenden mit
stop <tag> - best-effort Displaymeldung
Hallo <Benutzername>nach erfolgreicher Freigabe undAuf Wiedersehennach dem Sperren - Sperren zuerst über den dokumentierten
stop <echte RFID>-Weg mit anschließenderAuthreq-Verifikation; nur bei einer noch nicht ladenden, weiterhin entsperrten Station optionaler Fallback über eine absichtlich unbekannte Karte (KEBA_LOCK_FALLBACK_RFID, Standard44556677) - Zuordnung einer so gestarteten Sitzung zum freigebenden Nutzer
Die P30-UDP-Schnittstelle selbst verwendet keinen API-Key. Das optionale geheime Feld wird für diesen Treiber gespeichert, aber nicht gesendet; es hält die Konfiguration für weitere Connector-Typen offen. Die RFID muss von der Wallbox akzeptiert werden. SNLO ersetzt keine RFID-Whitelist-Verwaltung der Wallbox.
KEBA P30 verwendet für die UDP-Schnittstelle den Controller-Port 7090/UDP. SNLO startet deshalb den schlanken Dienst keba-gateway mit einem dauerhaft offenen UDP-Listener auf 7090. Jede Anfrage und jede eingehende KEBA-Antwort wird im Gateway protokolliert; RFID-Werte werden dabei nicht geloggt. network_mode: host vermeidet bei einem normalen rootful Docker Engine unter Linux die Docker-Bridge-NAT-Stufe. Port 7090 muss auf dem Docker-Host frei sein. Läuft dort bereits ein anderer KEBA-UDP-Controller (z. B. eine andere KEBA-Integration), können beide nicht gleichzeitig denselben Port verwenden.
Wichtig: Host-Networking kann nur die Docker-eigene NAT-Stufe beseitigen. Rootless-Docker-Versionen, zusätzliche Netzwerk-Namespaces oder ein Router/Firewall mit Source-NAT können den auf dem Leitungsweg sichtbaren Quellport weiterhin verändern. SNLO meldet deshalb im Gateway-Log den gebundenen Socket (0.0.0.0:7090) und die vom Kernel gewählte lokale Quell-IP. Siehst du die Wallbox-Antwort im Paketmitschnitt, aber keine Zeile KEBA UDP RX im Gateway-Log, erreicht das Paket den Netzwerk-Namespace des Gateways nicht.
Der interne Gateway-Kanal verwendet standardmäßig http://host.docker.internal:17090 und wird mit KEBA_GATEWAY_TOKEN authentifiziert. Der Token wird bei Neuinstallationen automatisch erzeugt. Bei einem Upgrade von 0.5.5 oder älter einmalig ergänzen:
printf 'KEBA_GATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" >> .env
Das Gateway ist bewusst auf report 1, report 2, start ..., stop ... und display ... beschränkt. Der TCP-Port 17090 sollte zusätzlich durch die Host-Firewall nicht aus fremden Netzen erreichbar sein. Für die Fehlersuche zeigt docker compose logs -f keba-gateway HTTP-Anfragen, den UDP-TX-Socket und jede tatsächlich auf UDP 7090 empfangene Antwort.
Architektur
Browser / PWA ---- OIDC Identity Provider
|
v
FastAPI API -------- PostgreSQL
| ^
| |
+---- EVCC-Steuerung
+---- optionale KEBA-RFID-Befehle
|
Collector ---------------+
| | |
EVCC A EVCC B EVCC n
Compose-Dienste:
db: PostgreSQLmigrate: einmalige Alembic-Migrationapi: FastAPI und PWAworker: dynamischer EVCC-Collector und Sitzungsimportkeba-gateway: Host-Netz-UDP-Gateway für KEBA 7090
Schnellstart
python3 scripts/init-env.py
docker compose up -d --build
Standardadresse:
http://<docker-host>:8080
Danach:
- Administratorpasswort ändern.
- EVCC-Instanzen anlegen und synchronisieren.
- Interne Fahrzeuge anlegen und EVCC-Fahrzeuge verknüpfen.
- Familienmitglieder und den zahlenden Nutzer mit Gültigkeitszeiträumen zuweisen.
- Benannte Tarife anlegen.
- Globalen Standard und nutzerbezogene Tarifzeiträume zuordnen.
- Optional KEBA-Verbindungen und Nutzer-RFID-Keys hinterlegen.
Ausführlicher: docs/QUICKSTART.md
Konfiguration und Veröffentlichung: docs/CONFIGURATION.md und docs/CI-CD.md
TrueNAS / Reverse Proxy
Für TrueNAS SCALE liegt eine fertige Produktionsvorlage unter deploy/truenas/compose.yaml. Die interne Docker-IP des API-Containers darf bei einem Recreate wechseln und wird deshalb nicht gepinnt. Der Reverse Proxy zeigt stattdessen auf die stabile LAN-IP des TrueNAS-Hosts und den veröffentlichten Port:
http://<TRUENAS-IP>:8080
Die dazugehörige snlo.env-Vorlage liegt unter deploy/truenas/snlo.env.example.
OpenID Connect
OIDC kann zusätzlich zum lokalen Login oder als alleiniger Anmeldeweg aktiviert werden. SNLO verwendet den Authorization-Code-Flow mit PKCE, prüft ID-Token-Signatur, Issuer, Audience, Ablauf, Nonce und Authorized Party und übernimmt die Rolle aus konfigurierten Nutzer- beziehungsweise Administratorgruppen.
Minimalkonfiguration hinter HTTPS:
APP_ORIGIN=https://snlo.de
COOKIE_SECURE=true
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://sso.example.org/realms/home
OIDC_CLIENT_ID=snlo
OIDC_CLIENT_SECRET=...
OIDC_GROUPS_CLAIM=groups
OIDC_USER_GROUPS=snlo-users
OIDC_ADMIN_GROUPS=snlo-admins
OIDC_AUTO_REDIRECT=true
OIDC_LOCAL_LOGIN_ENABLED=true
Die beim Identity Provider einzutragende Callback-URL lautet exakt:
https://snlo.de/api/auth/oidc/callback
Bei aktivem Auto-Redirect bleibt der lokale Login – sofern nicht deaktiviert – unter /?local=1 erreichbar. OIDC-Konten können automatisch angelegt oder über eine verifizierte identische E-Mail-Adresse mit vorhandenen lokalen Konten verknüpft werden. Rolle, Gruppen, E-Mail und Anzeigename werden bei jeder OIDC-Anmeldung neu synchronisiert.
Normale Browser-Sitzungen verwenden SESSION_TTL_HOURS (Standard 24 Stunden). Eine Anmeldung aus der installierten PWA verwendet eine getrennte langlebige Anwendungssitzung (PWA_SESSION_TTL_DAYS, Standard 3650 Tage), die beim PWA-Start erneuert wird und damit auf persönlichen Geräten praktisch bis zum aktiven Abmelden bestehen bleibt. Bei Browser und installierter PWA derselben Origin wird der HTTP-Cookie-Speicher je nach Plattform geteilt; nach einer PWA-Anmeldung kann die langlebige Sitzung deshalb auch im normalen Browser derselben Geräte-/Browserumgebung gelten.
Vollständige Anleitung: docs/OIDC.md
Update von 0.1.1 oder 0.3.0
Vor dem Update Datenbank und .env sichern. Bei einer bestehenden Compose-Installation muss zusätzlich der bisherige PostgreSQL-Volume-Name in .env übernommen werden:
docker volume ls --format '{{.Name}}' | grep 'postgres-data'
POSTGRES_VOLUME_NAME=<bestehender-volume-name>
Damit verwendet der neue Compose-Projektname weiterhin das vorhandene Datenvolume. Bestehende Werte für POSTGRES_DB, POSTGRES_USER, OIDC-Client und OIDC-Gruppen bleiben unverändert gültig. Bereits verschlüsselte EVCC-/KEBA-Geheimnisse können weiterhin gelesen werden; bestehende Browser-Sitzungen müssen nach der Umstellung neu angemeldet werden. Anschließend genügt:
docker compose up -d --build
Die Migration:
- ergänzt Familienmitgliedschaften und den Zahlerstatus,
- ergänzt OIDC-Identitäten, gruppengesteuerte Rollen und einmalige Login-States,
- übernimmt bestehende Fahrzeugzuweisungen als zahlend,
- erzeugt einen globalen Tarif
Standard, - überführt unterschiedliche historische Tarif-Snapshots in benannte, inaktive Tarife,
- verknüpft vorhandene Sitzungen mit dem jeweils übernommenen Tarif,
- übernimmt aktuell wirksame alte Nutzerüberschreibungen als benannte Nutzertarife,
- entfernt vorhandene Reservierungszeilen.
Alte Tarifregelzeilen bleiben aus Gründen der Nachvollziehbarkeit in der Datenbank, werden aber von der neuen Tarifauflösung nicht mehr verwendet.
EVCC-Zugriff
Als Instanz-URL wird nur die Basis-URL eingetragen:
http://192.168.10.20:7070
Kein /api anhängen. Der EVCC-API-Key ist für die aktuell verwendeten Status-, Sitzungs- und Ladepunktaktionen meist nicht erforderlich, kann aber je Instanz hinterlegt werden. Gespeicherte EVCC- und KEBA-Geheimnisse sowie RFID-Werte werden mit CREDENTIAL_ENCRYPTION_KEY verschlüsselt.
Backup
Zu sichern sind:
- PostgreSQL-Datenbank beziehungsweise das über
POSTGRES_VOLUME_NAMEbenannte Volume .env, insbesondereCREDENTIAL_ENCRYPTION_KEY- der verwendete Quellstand oder das Container-Image
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > snlo.sql
Entwicklung und Prüfung
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
./scripts/validate-release.sh
API-Dokumentation: /api/docs
Grenzen
- SNLO steuert keine zentrale KEBA-RFID-Whitelist.
- Direkte Bedienung an Wallbox oder EVCC kann außerhalb der App-Berechtigungen erfolgen; fremde aktive Sitzungen werden dann lediglich datensparsam als
Belegtangezeigt. - Batterieaufschläge sind nur für live vom Collector beobachtete Intervalle verfügbar.
- Die Anwendung erzeugt Verbrauchs- und Kostenaufstellungen. Rechtliche und messrechtliche Eignung für einen konkreten Abrechnungsfall muss separat geprüft werden.
- Der lokale Logout beendet nicht die Sitzung beim OIDC-Provider; ein providerseitiger Single Logout ist nicht implementiert.
- Vor Produktivbetrieb sind reale OIDC-, EVCC- und KEBA-Tests, Backup/Restore und Reverse-Proxy-Konfiguration erforderlich.
Lizenz
Copyright © 2026 Daniel Spittank
SNLO – Spittank.net Ladeorganisation steht unter der GNU Affero General Public License, Version 3 (AGPL-3.0-only). Der vollständige Lizenztext steht in LICENSE. Wer eine veränderte Version über ein Netzwerk bereitstellt, muss die Anforderungen der AGPL, insbesondere Abschnitt 13, beachten.