Compare commits
140
Commits
b6253f9e55
..
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
372b997876 | ||
|
|
5687596799 | ||
|
|
faa905ab09 | ||
|
|
9fbc52ee47 | ||
|
|
5437f3f058 | ||
|
|
ebec355c96 | ||
|
|
58f91e4319 | ||
|
|
ec7182b833 | ||
|
|
37addf4242 | ||
|
|
ce525a1ca0 | ||
|
|
e6aa50bb44 | ||
|
|
b6e7353086 | ||
|
|
f4a3c5fbe7 | ||
|
|
13e49ff60c | ||
|
|
30f7652803 | ||
|
|
913a4cd5f3 | ||
|
|
9878197aea | ||
|
|
8eb9688942 | ||
|
|
6c91d6bcc6 | ||
|
|
5890093b80 | ||
|
|
6d9aba74b0 | ||
|
|
fe3f46ba0e | ||
|
|
fc024fcfba | ||
|
|
44362e4bf5 | ||
|
|
76f8b067cc | ||
|
|
e0e52d2454 | ||
|
|
9394fe3eb1 | ||
|
|
b5c1fa6be2 | ||
|
|
fc4c4632ce | ||
|
|
12174a59b4 | ||
|
|
640fe53720 | ||
|
|
8d83b083bf | ||
|
|
6cd7bf19f5 | ||
|
|
3ce9af4244 | ||
|
|
8357e8dfab | ||
|
|
38353aed93 | ||
|
|
cf92e7623b | ||
|
|
add1077674 | ||
|
|
bfdcf4536b | ||
|
|
faa5dfb33f | ||
|
|
53fca5e2bb | ||
|
|
f611da975d | ||
|
|
393969594e | ||
|
|
cae14cf399 | ||
|
|
3aaa744a8a | ||
|
|
fdc2007bb1 | ||
|
|
37400878b6 | ||
|
|
ace1821213 | ||
|
|
b3b868cd2a | ||
|
|
a74f14aa19 | ||
|
|
fe966516b5 | ||
|
|
b3bbae1a72 | ||
|
|
ae2cd8ecff | ||
|
|
995b704e36 | ||
|
|
5ff0d405d4 | ||
|
|
ccecc437e9 | ||
|
|
156591a91e | ||
|
|
e0814a0c14 | ||
|
|
3cabf597ac | ||
|
|
b86810d30f | ||
|
|
d1fc62d43c | ||
|
|
21577c0125 | ||
|
|
aefe46352f | ||
|
|
6f76e6cd86 | ||
|
|
18c34b32e7 | ||
|
|
b111a5c501 | ||
|
|
ef708564d8 | ||
|
|
cf2ce3aff4 | ||
|
|
708a8ecea2 | ||
|
|
40f41a2fb8 | ||
|
|
2d24ce0900 | ||
|
|
f47c908798 | ||
|
|
72d96aeb81 | ||
|
|
dcf51bd8be | ||
|
|
efa8fe698c | ||
|
|
f250dea59a | ||
|
|
1e6481330a | ||
|
|
dbd728dd03 | ||
|
|
a67018e68f | ||
|
|
0f1e0c9fbb | ||
|
|
693beb2263 | ||
|
|
b1cb289922 | ||
|
|
98ee569178 | ||
|
|
2392a9c209 | ||
|
|
7845c66fd8 | ||
|
|
d7308663da | ||
|
|
d03a7fd8fb | ||
|
|
d804ba4bc1 | ||
|
|
e7c546ff2d | ||
|
|
e6516f5d7a | ||
|
|
8ae1f1ea8a | ||
|
|
3c71626b5f | ||
|
|
7b6d1b2f07 | ||
|
|
cf1ae5e348 | ||
|
|
3edf724df7 | ||
|
|
7cf67d4392 | ||
|
|
abd8c7edeb | ||
|
|
312b76aee5 | ||
|
|
4a01e9df0f | ||
|
|
66966875e7 | ||
|
|
51ce9b339e | ||
|
|
37822f0ed0 | ||
|
|
cda579ab86 | ||
|
|
d874ec0a3b | ||
|
|
5c5ef73fb8 | ||
|
|
49fe18c2eb | ||
|
|
052685401f | ||
|
|
ecbd0a3baf | ||
|
|
b404de4095 | ||
|
|
9598a287f8 | ||
|
|
6c2d6ddcde | ||
|
|
d4828f1ee5 | ||
|
|
29cfd8ceaa | ||
|
|
92e8ec59df | ||
|
|
355e7163fb | ||
|
|
1b4a788f22 | ||
|
|
9e6a675b25 | ||
|
|
4514ccb557 | ||
|
|
9869d99641 | ||
|
|
98fb926fbd | ||
|
|
7a3c436039 | ||
|
|
0a3ece3a81 | ||
|
|
21a6466ca5 | ||
|
|
37304691eb | ||
|
|
8af7776f33 | ||
|
|
0c9d4d920a | ||
|
|
dcd310498d | ||
|
|
c1fdd37b2b | ||
|
|
8439213234 | ||
|
|
efda77ccad | ||
|
|
69da93270e | ||
|
|
b2779148ee | ||
|
|
f10d454eb7 | ||
|
|
9426b0f041 | ||
|
|
1541482bf6 | ||
|
|
9acbff610d | ||
|
|
9aab4abdb3 | ||
|
|
0f9860356d | ||
|
|
bd47faa593 | ||
|
|
578285b0d6 |
@@ -21,6 +21,12 @@ jobs:
|
||||
- name: Abhaengigkeiten installieren
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
|
||||
- name: Pruefungen ausfuehren
|
||||
- name: Composer-Metadaten pruefen
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Abhaengigkeiten auf Sicherheitsmeldungen pruefen
|
||||
run: composer audit --no-interaction
|
||||
|
||||
- name: Unit-Tests und Syntaxpruefung ausfuehren
|
||||
run: composer check
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/vendor/
|
||||
/.phpunit.cache/
|
||||
/.phpunit.result.cache
|
||||
/build/
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Agenten-Anweisungen für Enelix-EMS
|
||||
|
||||
## Aktueller V4-Fortsetzungseinstieg: 06.10.2026
|
||||
|
||||
Neuester Quellstand: Bedienintegration und Zusammenfuehrung sind in
|
||||
docs/prognose-bedienintegration.md und im obersten Abschnitt der folgenden
|
||||
Uebergabe beschrieben. Daniel hat Commit/Push auf develop und beta ausdruecklich
|
||||
freigegeben; dies ist kein Anlagenrollout und keine neue Stellfreigabe.
|
||||
|
||||
Die vollstaendige Uebergabe liegt auf **enelix-services / Server 89** unter
|
||||
`/srv/agent/repos/Enelix-EMS/docs/NETPLAN_V4_AGENT_HANDOFF.md`.
|
||||
Vor Fortsetzung zuerst den neuesten datierten Abschnitt lesen. Die Datei ist
|
||||
im integrierten Repository versioniert; alte Serverkopien koennen abweichen.
|
||||
|
||||
Letzter hier belegter Anlagenzustand: **06.10.2026, 12:01:11 UTC /
|
||||
14:01:11 Europe/Zurich**, aus `/srv/agent/v4-recovery-20261006/finished.json`
|
||||
auf **iot-symcon01**. Dies ist keine neue Live-Abnahme:
|
||||
V4 und alter Netzfahrplan AUS, Batterie 44234 gestoppt, EV-Auftrag 19651 = 0 W.
|
||||
Der unabhaengige SDL-Auftrag blieb unangetastet.
|
||||
Rueckmelde-/Cache-/Empfangskorrekturen sind installiert; **offen ist die
|
||||
instanzbezogene Batterietimerblockade (LastRun=0)**. Kein nachgewiesenes
|
||||
physisches Tracking, kein >90-s-Dauerbetrieb und kein automatischer
|
||||
Batterie-Watchdog-Lauf. Alte Installer nicht erneut ausfuehren und keine
|
||||
externen Tick-Hooks als Umgehung einsetzen. Zuerst Timerursache beheben und
|
||||
automatische Ausfuehrung belegen, danach nur innerhalb gueltiger Freigabe testen.
|
||||
|
||||
EV bleibt **161.44 kWh / 39 kW**. Die bestehende befristete Testfreigabe endet
|
||||
**07.10.2026, 17:38:07 Europe/Zurich**; diese Notiz erneuert oder verlaengert
|
||||
sie nicht. Wegfall des zusaetzlichen +/-5-kW-Limits hebt Geraete-, SOC-,
|
||||
Netzgrenzen und die dokumentierten Grenzen der Testanlagen-Ausnahme nicht auf.
|
||||
Kein Aktivstart oder Dienstneustart aus der Bitte um Kontext-Sicherung ableiten.
|
||||
|
||||
Aeltere V4-Statusangaben und damalige "naechste Schritte" weiter unten sind
|
||||
historisch, soweit die neuere Uebergabe sie ersetzt. Sonstige Projekt- und
|
||||
Sicherheitsregeln sowie separate Manager-Leistungsverteilungsnotizen bleiben
|
||||
unveraendert. Aktuellen Zustand und Freigaben vor operativen Arbeiten erneut
|
||||
pruefen. Ein neuer Agent muss diese Quellen lesen; kein automatischer Chattransfer.
|
||||
|
||||
## Netzfahrplan V4: zuerst den Gesprächskontext übernehmen
|
||||
|
||||
Bei Aufgaben zu Netzfahrplan, Prognosen, V4, Batterieoptimierung oder Lihrenmoos **vor Änderungen** die vollständige Übergabe lesen:
|
||||
|
||||
**[`docs/NETPLAN_V4_AGENT_HANDOFF.md`](docs/NETPLAN_V4_AGENT_HANDOFF.md)**
|
||||
|
||||
Sie enthält Daniels Anforderungen und Korrekturen, installierte versus nur entwickelte Komponenten, Quellen-/Instanzzuordnung, bekannte behobene Fehler, Grenzen der Testfreigabe und den nächsten konkreten Arbeitsschritt. Die Runtime-Berichte stammen zuletzt vom 03.10.2026; Zeitpunkt und aktuellen Zustand prüfen. Alte Entwicklungsdokumente können noch „nicht installiert“ sagen, obwohl ein jüngerer Installationsbericht den Abschluss belegt.
|
||||
|
||||
Kurzstand bei Erstellung dieser Anweisung: Unified RC1 und korrigierter Rückmelde-/begrenzter Testpfad installiert; Versandproblem behoben. Nächste offene Umsetzung sind bestätigte Geräte-Lesezeitpunkte statt pauschaler Verwendung von `VariableUpdated` (`feedback_source_skew`). EV **161.44 kWh / 39 kW**, nicht die zurückgenommenen 160/30. Dauerproduktion ist noch nicht fertig; keine automatische Stellfreigabe.
|
||||
|
||||
## Arbeits- und Git-Regeln
|
||||
|
||||
- Vor Arbeit Host-`/srv/agent/AGENT_CONTEXT.md`, Branch und Arbeitsverzeichnis prüfen. Servercheckout ist nicht gleich laufendes Symcon-Modulverzeichnis.
|
||||
- Neue Entwicklung auf **`develop`**, keine Feature-Branches. Nur eigene/zugehörige Änderungen committen. `develop` darf im vereinbarten Rahmen gepusht werden; `beta` nur gemäss ausdrücklicher Freigabe und nach Tests, `main` nach ausdrücklichem Auftrag/Feldabnahme. Keine Schutzregeln oder fehlende Authentifizierung umgehen.
|
||||
- Daniel möchte zusammenhängende produktionsgeeignete Umsetzung, keine weitere Schleife aus Diagnosekategorien, identischen Rückfragen und veralteten Installern. Vorhandene Quellen und Werkzeuge benutzen, keine bekannten Anlagenangaben erneut verlangen.
|
||||
- Code-, Host-/Container-, Kernel- und Feldtests sowie Installation, Veröffentlichung und Stellfreigabe getrennt belegen. „Optimal“/„ACK“/grüne Tests sind keine gemessene Einsparung und keine Produktionsfreigabe.
|
||||
- Keine Stellbefehle, Testsitzungen, Freigaben oder Neustarts aus einer reinen Kontext-/Statusanfrage ableiten. Aktive Hardware nur innerhalb des ausdrücklich freigegebenen Umfangs behandeln. Messgrenzen/Watchdog-Nachweise nicht erfinden oder per Label umgehen.
|
||||
- Keine Credentials lesen/ausgeben/einchecken. Root/Docker-Rechte nicht ausweiten. Bestehende GUI-/Fremdänderungen erhalten. Vor Runtime-Änderungen passende Sicherung, exakte Quell-/Abhängigkeitsprüfung und Rücksetzweg.
|
||||
- PHP-IPS-Skripte gehören in den Symcon-Kernel; isolierte Tests mit simulierten IPS-Aufrufen niemals dort ausführen. `MC_ReloadModule`/`IPS_ApplyChanges` können Initialisierungsnebenwirkungen haben.
|
||||
- Alte Outbox-/Historienwerte nicht löschen, Kennungen nicht im Original umschreiben, Cursor nur nach passender Serverbestätigung weiterführen. Keine künstlich frischen Messzeitstempel.
|
||||
- Übergabe nach tatsächlichem Fortschritt aktualisieren: Datum, Commit, was wirklich installiert/getestet wurde und nächster konkreter Schritt. Keine Hintergrundarbeit behaupten.
|
||||
|
||||
Diese Datei ergänzt bestehende höherrangige Projekt-/Hostregeln. Sie ist keine neue Betriebsfreigabe und kann keinen anderen Agenten ohne Lesen der Dateien automatisch mit dem gesamten Chat synchronisieren.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Batterie
|
||||
|
||||
IP-Symcon-Modul fuer Batteriespeicher im Enelix EMS. Das Modul uebernimmt die
|
||||
Leistungsangebote und SoC-Logik der bisherigen Enelix-Batterie, arbeitet aber
|
||||
ereignisbasiert und schreibt Stellwerte direkt in ausgewaehlte
|
||||
IP-Symcon-Registervariablen.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- positive Leistung bedeutet Laden, negative Leistung bedeutet Entladen
|
||||
- stufenlose Leistungsbereiche in ganzen Watt innerhalb der dynamischen Grenzen
|
||||
- unterschiedliche Leistungsangebote fuer PV- und Peakbetrieb
|
||||
- Reserve- und Mindestladezustand mit konfigurierbarer Hysterese
|
||||
- direkte Registeransteuerung fuer herstellerunabhaengige Batterien, GoodWe,
|
||||
SolarEdge und Sigenergy
|
||||
- Umschaltung zwischen Wechselrichter- und Enelix-Steuerung
|
||||
- ereignisbasierte Reaktion auf alle ausgewaehlten Messwerte
|
||||
- Managerkommunikation ueber den Enelix-Vertrag 4.0
|
||||
- Sollwert-Timeout, zeitbasierte Aenderungssperre und periodische Vollmeldung
|
||||
- optionale Diagnosevariablen und Debug-Logging
|
||||
|
||||
Es gibt keinen zyklischen Regel- oder Berechnungstimer. Timer werden nur fuer
|
||||
Vollmeldungen, den Ablauf einer Manager-Vorgabe und das Ende der
|
||||
Aenderungssperre verwendet.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- IP-Symcon ab Version 8.0
|
||||
- ein eingerichteter Enelix Manager
|
||||
- numerische Variablen fuer Ladezustand, Netzleistung, Batterieleistung sowie
|
||||
maximale Lade- und Entladeleistung
|
||||
- numerische Registervariablen mit Standard- oder benutzerdefinierter Aktion
|
||||
|
||||
## Vorzeichen und Einheiten
|
||||
|
||||
| Wert | Positiv | Negativ | Einheit |
|
||||
| --- | --- | --- | --- |
|
||||
| Manager-Sollleistung | Laden | Entladen | W |
|
||||
| Batterieleistung | Laden | Entladen | W |
|
||||
| Netzleistung | Netzbezug | Einspeisung | W |
|
||||
| Ladezustand | - | - | % |
|
||||
|
||||
Sigenergy-Leistungsregister werden in kW beschrieben. Alle anderen
|
||||
Leistungsregister werden in W beschrieben.
|
||||
|
||||
## Batterietypen und Register
|
||||
|
||||
| Batterietyp | Management | Modus | Leistungsregister |
|
||||
| --- | --- | --- | --- |
|
||||
| Herstellerunabhaengig | WR 0, Enelix 1 | Laden 0, Entladen 1 | getrennt Laden/Entladen in W |
|
||||
| GoodWe | WR 1, Enelix 2 | Laden 11, Entladen 12 | gemeinsam, absoluter Wert in W |
|
||||
| SolarEdge | WR 1, Enelix 4 | Laden 3, Entladen 4 | getrennt Laden/Entladen in W |
|
||||
| Sigenergy | WR 0, Enelix 1 | Laden 3, Entladen 6 | getrennt Laden/Entladen in kW |
|
||||
|
||||
Bei Wechselrichtersteuerung bietet die Batterie dem Manager nur [0] an. Das
|
||||
Modul setzt die Leistungsregister auf 0 und schreibt den passenden
|
||||
Automatikcode in das Managementregister.
|
||||
|
||||
Die Konfigurationsmaske zeigt nach Wahl des Batterietyps nur die benoetigten
|
||||
Registerauswahlen:
|
||||
|
||||
- GoodWe: Management, Modus und gemeinsames Leistungsregister
|
||||
- alle anderen Typen: Management, Modus, Ladeleistung und Entladeleistung
|
||||
|
||||
## Messwerte
|
||||
|
||||
| Property | Beschreibung |
|
||||
| --- | --- |
|
||||
| MaxLadeleistungVariableID | Aktuell zulaessige maximale Ladeleistung in W |
|
||||
| MaxEntladeleistungVariableID | Aktuell zulaessige maximale Entladeleistung in W |
|
||||
| LadezustandVariableID | Ladezustand in % |
|
||||
| NetzleistungVariableID | Netzbezug beziehungsweise Einspeisung in W |
|
||||
| IstleistungVariableID | Aktuelle Batterieleistung in W |
|
||||
| MesswertMaxAlter | Maximales Alter jedes Messwertes in Sekunden |
|
||||
|
||||
Alle Messwerte muessen als Integer- oder Floatvariable vorliegen. Fehlt ein
|
||||
Messwert, ist er veraltet oder liegt der Ladezustand ausserhalb von 0 bis
|
||||
100 %, meldet sich die Batterie als nicht verfuegbar und bietet nur [0] an.
|
||||
|
||||
## Ladezustandslogik
|
||||
|
||||
ReserveLadezustand entspricht der bisherigen Peakshaving-Reserve.
|
||||
MindestLadezustand schuetzt vor weiterer Entladung.
|
||||
LadezustandHysterese ersetzt die bisher fest im Code hinterlegte
|
||||
2-Prozent-Hysterese. NachladenMitMaximalleistung verwendet standardmaessig die
|
||||
dynamische maximale Ladeleistung. Wird der Schalter deaktiviert, begrenzt
|
||||
MaximaleNachladeleistung das schutzbedingte Nachladen in W. Der wirksame Wert
|
||||
wird nie hoeher als die dynamische Ladegrenze der Batterie.
|
||||
|
||||
Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
|
||||
|
||||
- Im PV-Betrieb steht oberhalb der Reserve der durchgaengige Bereich von der
|
||||
maximalen Entlade- bis zur maximalen Ladeleistung zur Verfuegung.
|
||||
- Unterhalb der Reserve wird die wirksame Nachladeleistung als fester Wert
|
||||
angeboten.
|
||||
- Bei vollem Speicher wird der durchgaengige Bereich von der maximalen
|
||||
Entladeleistung bis 0 W angeboten.
|
||||
- Im Peakbetrieb wird oberhalb der Reserve der aus Netz- und aktueller
|
||||
Batterieleistung berechnete Entladewert angeboten.
|
||||
- Innerhalb der Reserve-Hysterese und unterhalb des Mindestladezustands werden
|
||||
Laden und Entladen wie bisher eingeschraenkt; die positive Grenze ist dabei
|
||||
hoechstens die konfigurierte Nachladeleistung.
|
||||
- Oberhalb der Reserve ohne aktive Hysterese bleibt der normale Ladebereich
|
||||
unveraendert und wird nicht durch die Nachladegrenze reduziert.
|
||||
|
||||
## Manager und Zeitverhalten
|
||||
|
||||
Das Modul verwendet den gemeinsamen Enelix-Nachrichtenvertrag 4.0.
|
||||
Betriebsart ist PV oder Peak. Ein Paket mit Sollleistung_W=null
|
||||
kuendigt nur die Betriebsart an und loest die Neuberechnung des Angebots aus.
|
||||
|
||||
Aenderungssperre ist die ereignisbasierte Entsprechung des frueheren
|
||||
Idle-Counters. Nach einer echten Sollwertaenderung meldet das Modul
|
||||
AenderungMoeglich=false. Ein einmaliger Timer hebt die Sperre nach Ablauf
|
||||
der konfigurierten Sekunden wieder auf.
|
||||
|
||||
VorgabeTimeout setzt die Register auf 0, wenn der Manager eine Vorgabe
|
||||
nicht rechtzeitig erneuert. Meldeintervall erzeugt nur eine periodische
|
||||
Vollmeldung und fuehrt keinen unabhaengigen Regelzyklus aus.
|
||||
|
||||
## Sichtbare Variablen
|
||||
|
||||
Immer vorhanden:
|
||||
|
||||
- Aktiv: lokale EMS-Freigabe
|
||||
- Ladestatus: 0 unbekannt, 1 ruhend, 2 laden, 3 entladen
|
||||
|
||||
Bei aktivierten Diagnosevariablen werden zusaetzlich unter anderem
|
||||
Istleistung, Sollleistung, Ladezustand, Messwertgueltigkeit, Hysterese,
|
||||
Leistungsgrenzen, Energie, Leistungsangebot und der letzte Registerbefehl
|
||||
angezeigt.
|
||||
|
||||
LoggingEin aktiviert zusaetzliche Meldungen im IP-Symcon-Debugprotokoll.
|
||||
Die Schaltflaeche **Messwerte und Angebot aktualisieren** loest eine sofortige
|
||||
ereignisartige Aktualisierung aus. **Register auf sicheren Zustand setzen**
|
||||
verwirft die aktuelle Vorgabe und schreibt eine Leistung von 0.
|
||||
|
||||
## Inbetriebnahme
|
||||
|
||||
1. Batterietyp und Steuerung auswaehlen.
|
||||
2. Alle fuenf Messwertvariablen zuordnen.
|
||||
3. Die automatisch eingeblendeten Registervariablen zuordnen.
|
||||
4. Reserve, Mindestladezustand, Hysterese und Nachladeleistung pruefen.
|
||||
5. Diagnosevariablen und Logging fuer die Erstinbetriebnahme aktivieren.
|
||||
6. Die Batterie im Manager zuordnen.
|
||||
7. Aktiv einschalten.
|
||||
8. Unter Aufsicht je einen Lade-, Entlade- und Nullsollwert senden.
|
||||
9. Registercodes, Vorzeichen, Einheiten und reale Wechselrichterreaktion
|
||||
kontrollieren.
|
||||
10. Abschliessend die Wechselrichtersteuerung waehlen und pruefen, dass alle
|
||||
Leistungsregister auf 0 sowie das Managementregister auf Automatik gehen.
|
||||
|
||||
## Fehlerbilder
|
||||
|
||||
- **Konfiguration ungueltig:** Variablentypen, Aktionszuordnung und die fuer
|
||||
den Batterietyp erforderlichen Register pruefen.
|
||||
- **Messwerte fehlen oder sind veraltet:** Variablenaktualisierung und
|
||||
MesswertMaxAlter kontrollieren.
|
||||
- **Register konnten nicht geschrieben werden:** Standard- oder
|
||||
benutzerdefinierte Aktion der Zielvariablen sowie die Geraetekommunikation
|
||||
pruefen.
|
||||
- **Sollwert wird abgewiesen:** Der Wert muss im zuletzt gemeldeten Angebot
|
||||
enthalten und die Aenderungssperre muss abgelaufen sein.
|
||||
|
||||
## Tests
|
||||
|
||||
~~~bash
|
||||
composer test -- --filter 'Batterie(Regler|Modulstruktur)Test'
|
||||
composer symcon:single -- Batterie
|
||||
composer check
|
||||
~~~
|
||||
|
||||
Der Symcon-Test prueft die Modulinstanz, direkte GoodWe-Registerbefehle,
|
||||
Laden und Entladen, Messwertereignisse sowie die Rueckgabe an die
|
||||
Wechselrichtersteuerung.
|
||||
|
||||
## Weiterfuehrende Dokumentation
|
||||
|
||||
- [Ausfuehrliche Modulbeschreibung](../docs/module/Batterie/README.md)
|
||||
- [Manager-Verbraucher-Schnittstelle](../docs/Schnittstelle.md)
|
||||
- [Migration von Enelix 1](../docs/migration/Batterie.md)
|
||||
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Manager und Zeitverhalten",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPV",
|
||||
"caption": "Prioritaet PV",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPeak",
|
||||
"caption": "Prioritaet Peak",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Meldeintervall",
|
||||
"caption": "Meldeintervall",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "VorgabeTimeout",
|
||||
"caption": "Vorgabe-Timeout",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Aenderungssperre",
|
||||
"caption": "Sperrzeit nach Leistungsaenderung",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Batterie und Messwerte",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Batterietyp",
|
||||
"caption": "Batterietyp",
|
||||
"onChange": "IPS_RequestAction($id, 'FormBatterietyp', $Batterietyp);",
|
||||
"options": [
|
||||
{"caption": "Unkonfiguriert", "value": 0},
|
||||
{"caption": "Herstellerunabhaengig", "value": 1},
|
||||
{"caption": "GoodWe", "value": 2},
|
||||
{"caption": "SolarEdge", "value": 3},
|
||||
{"caption": "Sigenergy", "value": 4}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Batteriemanagement",
|
||||
"caption": "Steuerung der Batterie",
|
||||
"options": [
|
||||
{"caption": "Wechselrichter", "value": 1},
|
||||
{"caption": "Enelix Manager", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "MaxLadeleistungVariableID",
|
||||
"caption": "Maximale Ladeleistung",
|
||||
"validVariableTypes": [1, 2]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "MaxEntladeleistungVariableID",
|
||||
"caption": "Maximale Entladeleistung",
|
||||
"validVariableTypes": [1, 2]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "LadezustandVariableID",
|
||||
"caption": "Ladezustand",
|
||||
"validVariableTypes": [1, 2]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "NetzleistungVariableID",
|
||||
"caption": "Netzleistung",
|
||||
"validVariableTypes": [1, 2]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "IstleistungVariableID",
|
||||
"caption": "Aktuelle Batterieleistung",
|
||||
"validVariableTypes": [1, 2]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MesswertMaxAlter",
|
||||
"caption": "Maximales Messwertalter",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Ladezustandsgrenzen",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "ReserveLadezustand",
|
||||
"caption": "Reserve fuer Peakshaving",
|
||||
"suffix": " %",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MindestLadezustand",
|
||||
"caption": "Minimaler Ladezustand",
|
||||
"suffix": " %",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "LadezustandHysterese",
|
||||
"caption": "Reserve-Hysterese",
|
||||
"suffix": " %",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "NachladenMitMaximalleistung",
|
||||
"caption": "Beim Nachladen maximale Ladeleistung verwenden",
|
||||
"onChange": "IPS_RequestAction($id, 'FormNachladenMitMaximalleistung', $NachladenMitMaximalleistung);"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaximaleNachladeleistung",
|
||||
"caption": "Maximale Nachladeleistung",
|
||||
"suffix": " W",
|
||||
"minimum": 1,
|
||||
"visible": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Batterieregister",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Es werden nur die zum gewaehlten Batterietyp passenden Register angezeigt."
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "ManagementRegisterVariableID",
|
||||
"caption": "Managementregister",
|
||||
"validVariableTypes": [1, 2],
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "ModusRegisterVariableID",
|
||||
"caption": "Lade-/Entlademodus",
|
||||
"validVariableTypes": [1, 2],
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "LeistungsRegisterVariableID",
|
||||
"caption": "Leistungsregister",
|
||||
"validVariableTypes": [1, 2],
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "LadeleistungRegisterVariableID",
|
||||
"caption": "Ladeleistungsregister",
|
||||
"validVariableTypes": [1, 2],
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "EntladeleistungRegisterVariableID",
|
||||
"caption": "Entladeleistungsregister",
|
||||
"validVariableTypes": [1, 2],
|
||||
"visible": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Darstellung und Diagnose",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EinstellungenInVisu",
|
||||
"caption": "Einstellungen in der Visualisierung anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "DiagnosevariablenAnzeigen",
|
||||
"caption": "Diagnosevariablen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LoggingEin",
|
||||
"caption": "Diagnoseprotokoll aktivieren"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Button",
|
||||
"caption": "Messwerte und Angebot aktualisieren",
|
||||
"onClick": "IPS_RequestAction($id, 'Aktualisieren', true);"
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"caption": "Register auf sicheren Zustand setzen",
|
||||
"onClick": "IPS_RequestAction($id, 'SichererZustand', true);"
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{"code": 102, "icon": "active", "caption": "Aktiv"},
|
||||
{"code": 201, "icon": "error", "caption": "Konfiguration ungueltig"},
|
||||
{"code": 202, "icon": "inactive", "caption": "Messwerte fehlen oder sind veraltet"},
|
||||
{"code": 203, "icon": "error", "caption": "Batterieregister konnten nicht geschrieben werden"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}",
|
||||
"name": "Batterie",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Batteriespeicher"
|
||||
],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Batterie"
|
||||
}
|
||||
+1282
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Manager: V4-Planempfang und Prognosebedienung im vorhandenen Bereich
|
||||
Prognose / Forecast zusammengefasst. Explizite Start-/Stoppaktionen pruefen
|
||||
Lizenz und bestehende Testfreigaben. Kein automatischer Start, keine neue
|
||||
Dauerbetriebsfreigabe und keine Loeschung bestehender Laufzeitobjekte.
|
||||
- Integration des bisherigen V4-Entwicklungsstands mit den aktuellen Manager-,
|
||||
SDL- und Sollwertkorrekturen; V4-Stopp und Sollwertverwerfen bleiben erhalten.
|
||||
- Optionaler nativer V4-Betriebsdatensender fuer den Schattenversuch (standardmaessig aus), separate Diagnose und sichere Batterie-/Messquellenzuordnung. Kein neuer Fahrplan wird lokal ausgefuehrt; vollstaendige Monatspeak-/Viertelstunden-Messnachweise und deren Historienadapter bleiben Voraussetzung.
|
||||
- Verbraucher: Beim Verwerfen einer Manager-Vorgabe wird auch die gespeicherte
|
||||
und sichtbare Sollleistung auf 0 W gesetzt. Gilt fuer Batterie, beide
|
||||
Ladestationen, Pufferspeicher, Verbraucher 1-Stufig, Waermepumpe und
|
||||
Warmwassererwaermer. Bereits ungueltige Altwerte werden beim Anwenden der
|
||||
Instanzkonfiguration bereinigt. Gueltige Vorgaben, lokale Schutzprogramme,
|
||||
Mindestlaufzeiten und der Nachrichtenvertrag bleiben erhalten.
|
||||
|
||||
- Manager: SDL-/Regelenergie als separate Istleistungsquelle mit optionalem SOC,
|
||||
Leistungsfaktor und getrennten Anzeigeoptionen fuer Energiefluss und Diagramme.
|
||||
SDL wird unabhaengig von der Anzeige aus Hauslast und neuer Hausenergie
|
||||
herausgerechnet; die Prognosetelemetrie verwendet die korrigierte Hauslast.
|
||||
Bestehende Visualisierungen werden ausschliesslich um eigene SDL-Eintraege
|
||||
ergaenzt. Standardmaessig deaktiviert, keine automatische Quellenmigration,
|
||||
keine rueckwirkende Datenkorrektur und keine Aenderung externer SDL-Steuerungen.
|
||||
- Manager: Positives Budget wird bei gleicher Prioritaet nach der kleinsten
|
||||
naechsten erreichbaren Sollleistung verteilt. Nur bei gleicher Leistungsstufe
|
||||
entscheiden exakter Energiebezug und anschliessend die Instanz-ID. Einzelstufen
|
||||
und ganzzahlige Bereiche teilen sich das Budget; reservierte Lasten und die
|
||||
negative Defizitregelung bleiben unveraendert. Keine Konfigurationsmigration.
|
||||
|
||||
## 0.1 Build 1 - Erste Beta - 2026-09-29
|
||||
|
||||
### Enthalten
|
||||
|
||||
- Enelix Manager mit PV- und Peak-Betrieb, Priorisierung und Diagnose.
|
||||
- Anlagenweite Einspeisebegrenzung, Netzfahrplan und Prognosetopologie.
|
||||
- Batterie, Warmwassererwaermer, Pufferspeicher und Waermepumpe.
|
||||
- Verbraucher 1-Stufig sowie Easee Gateway und beide Ladestationsvarianten.
|
||||
- Lizenzpruefung fuer Manager und einzeln freischaltbare Verbrauchermodule.
|
||||
- Idempotente Demoanlage fuer Installation und Integrationstests.
|
||||
|
||||
### Beta-Aenderungen
|
||||
|
||||
- Ladestationen halten beim Aktivieren und Umschalten von Solarladen den bisherigen Strom bis zur neuen Manager-Vorgabe; die Stand-alone-Phasenprobe verwendet nur noch 6 A.
|
||||
- Fahrzeugstatus ist bei beiden Ladestationsvarianten eine Diagnosevariable, Ist- und Sollleistung gehoeren zu den optionalen Visualisierungseinstellungen.
|
||||
- Stufenlose Leistungsbereiche fuer Batteriespeicher.
|
||||
- Schutzbedingtes Nachladen der Batterie kann wahlweise auf die dynamische Maximalleistung oder eine benutzerdefinierte Watt-Grenze begrenzt werden.
|
||||
- Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert.
|
||||
- Stand-alone-Ladestationen erkennen nach einer einstellbaren Beobachtungszeit den niedrigeren Fahrzeug-Maximalstrom und begrenzen ihr Leistungsangebot ohne Ladeunterbruch.
|
||||
- Easee-Ladestationen laden einen fehlenden Initialstatus ueber die Observations-API nach und starten bei noch unbekannter Ausgangsphase konservativ einphasig.
|
||||
- Wartende Easee-Ladestationen erhalten bei positiver Stromvorgabe einmal pro Fahrzeugverbindung einen Startbefehl; Freigabe- und Blockiergruende werden diagnostiziert.
|
||||
- Fehlende oder veraltete Verbraucher blockieren die Verteilung an aktuelle Verbraucher nicht mehr.
|
||||
- Verbraucher gleicher Prioritaet werden wie in Enelix 1 anhand ihrer bezogenen Energie in 2-kWh-Gruppen ausgeglichen.
|
||||
- Die Stoerungsueberwachung fordert einen fehlenden Geraetezugang innerhalb ihres Retry-Zyklus automatisch neu an.
|
||||
- Der Warmwassererwaermer verwendet konfigurierbare Temperatur-Stoergrenzen von standardmaessig 0 bis 100 Grad C.
|
||||
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
|
||||
|
||||
### Hinweise
|
||||
|
||||
- Diese Version ist fuer kontrollierte Anlagen- und Feldtests bestimmt.
|
||||
- Vor produktivem Einsatz sind Sicherung und anlagenspezifische Abnahme erforderlich.
|
||||
- Korrekturen werden vorwaerts ueber `develop` und anschliessend `beta` verteilt.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Easee Gateway
|
||||
|
||||
Gemeinsames IP-Symcon-Splittermodul fuer ein Easee-Nutzerkonto. Es verwaltet
|
||||
Anmeldung, Token, Ereignisverbindung und REST-Stromvorgaben fuer mehrere
|
||||
Ladestationen.
|
||||
|
||||
Konfiguration, Betrieb und Fehlerbehandlung:
|
||||
[Moduldokumentation](../docs/module/Easee-Gateway/README.md)
|
||||
|
||||
Kind-Gateway-Vertrag:
|
||||
[Schnittstellenbeschreibung](../docs/Schnittstelle-Easee-Gateway.md)
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "Active",
|
||||
"caption": "Gateway aktiv"
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "Username",
|
||||
"caption": "Easee Benutzername"
|
||||
},
|
||||
{
|
||||
"type": "PasswordTextBox",
|
||||
"name": "Password",
|
||||
"caption": "Easee Passwort"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "VerifyCertificate",
|
||||
"caption": "TLS-Zertifikat pruefen"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Ein Gateway wird von allen Easee-Ladestationen desselben Kontos gemeinsam verwendet."
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Button",
|
||||
"caption": "Verbindung neu aufbauen",
|
||||
"onClick": "IPS_RequestAction($id, \"Reconnect\", false);"
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{
|
||||
"code": 201,
|
||||
"icon": "error",
|
||||
"caption": "Easee-Zugangsdaten fehlen"
|
||||
},
|
||||
{
|
||||
"code": 202,
|
||||
"icon": "error",
|
||||
"caption": "Easee-Anmeldung fehlgeschlagen"
|
||||
},
|
||||
{
|
||||
"code": 203,
|
||||
"icon": "error",
|
||||
"caption": "Easee-Ereignisverbindung fehlgeschlagen"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "{B7552FC9-87BC-49D0-8CCB-076BBB045BBE}",
|
||||
"name": "EaseeGateway",
|
||||
"type": 2,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Easee Gateway"
|
||||
],
|
||||
"parentRequirements": [
|
||||
"{79827379-F36E-4ADA-8A95-5F8D1DC92FA9}"
|
||||
],
|
||||
"childRequirements": [
|
||||
"{107D5CFA-8F3D-4E38-8643-90DDFE6A3B4D}"
|
||||
],
|
||||
"implemented": [
|
||||
"{018EF6B5-AB94-40C6-AA53-46943E824ACF}",
|
||||
"{7AEF3DF7-DA5B-47C5-BCDC-0110D06DDC04}"
|
||||
],
|
||||
"prefix": "ENELIXEASEE",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/EaseeGateway"
|
||||
}
|
||||
@@ -0,0 +1,964 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../libs/EaseeGatewayProtokoll.php';
|
||||
|
||||
use Belevo\EnelixEMS\EaseeGatewayProtokoll;
|
||||
|
||||
class EaseeGateway extends IPSModule
|
||||
{
|
||||
private const WEBSOCKET_MODULE_ID = '{D68FD31F-0E90-7019-F16C-1949BD3079EF}';
|
||||
private const SIMPLE_RX_DATA_ID = '{018EF6B5-AB94-40C6-AA53-46943E824ACF}';
|
||||
private const SIMPLE_TX_DATA_ID = '{79827379-F36E-4ADA-8A95-5F8D1DC92FA9}';
|
||||
private const CHILD_REQUEST_DATA_ID = '{7AEF3DF7-DA5B-47C5-BCDC-0110D06DDC04}';
|
||||
private const CHILD_EVENT_DATA_ID = '{107D5CFA-8F3D-4E38-8643-90DDFE6A3B4D}';
|
||||
private const SIGNALR_BASE_URL = 'https://streams.easee.com/hubs/chargers';
|
||||
private const API_BASE_URL = 'https://api.easee.com';
|
||||
private const RECORD_SEPARATOR = "\x1e";
|
||||
private const CACHE_MAXIMALALTER_SEKUNDEN = 60;
|
||||
|
||||
public function Create(): void
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->RegisterPropertyBoolean('Active', true);
|
||||
$this->RegisterPropertyString('Username', '');
|
||||
$this->RegisterPropertyString('Password', '');
|
||||
$this->RegisterPropertyBoolean('VerifyCertificate', true);
|
||||
|
||||
// Nur fuer den automatisierten IP-Symcon-Funktionstest.
|
||||
$this->RegisterPropertyBoolean('Testmodus', false);
|
||||
|
||||
$this->RegisterAttributeString('ObservationCache', '{}');
|
||||
|
||||
$this->RegisterVariableBoolean('Connected', 'Easee verbunden', '~Switch', 10);
|
||||
$this->RegisterVariableInteger(
|
||||
'SubscriptionCount',
|
||||
'Angemeldete Ladestationen',
|
||||
'',
|
||||
20
|
||||
);
|
||||
$this->RegisterVariableString('LastError', 'Letzter Fehler', '', 30);
|
||||
|
||||
$this->RegisterTimer(
|
||||
'MaintainConnectionTimer',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'MaintainConnection', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'TokenRefreshTimer',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'RefreshToken', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'ReconnectTimer',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'ReconnectAfterLoss', false);"
|
||||
);
|
||||
|
||||
$this->RequireParent(self::WEBSOCKET_MODULE_ID);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
|
||||
$this->SetTimerInterval('MaintainConnectionTimer', 0);
|
||||
$this->SetTimerInterval('TokenRefreshTimer', 0);
|
||||
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||
$this->SetBuffer('SignalRReady', '0');
|
||||
$this->SetBuffer('ReceiveBuffer', '');
|
||||
$this->SetBuffer('Subscriptions', '{}');
|
||||
$this->SetBuffer('SubscribedThisConnection', '{}');
|
||||
$this->SetBuffer('PendingInvocations', '{}');
|
||||
$this->SetValue('SubscriptionCount', 0);
|
||||
$this->setzeVerbunden(false);
|
||||
|
||||
if (!$this->ReadPropertyBoolean('Active')) {
|
||||
$this->SetStatus(104);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
$this->SetStatus(102);
|
||||
$this->setzeLetztenFehler('');
|
||||
$this->setzeVerbunden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
trim($this->ReadPropertyString('Username')) === ''
|
||||
|| $this->ReadPropertyString('Password') === ''
|
||||
) {
|
||||
$this->SetStatus(201);
|
||||
$this->setzeLetztenFehler('Easee-Benutzername oder Passwort fehlt.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->stelleWebSocketParentSicher()) {
|
||||
$this->SetStatus(203);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetTimerInterval('MaintainConnectionTimer', 10000);
|
||||
$this->SetTimerInterval('TokenRefreshTimer', 1800000);
|
||||
if (!$this->erneuereZugangsdaten(false)) {
|
||||
$this->SetStatus(202);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->verbindeSignalR();
|
||||
}
|
||||
|
||||
public function RequestAction($ident, $wert): void
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'MaintainConnection':
|
||||
$this->pflegeVerbindung();
|
||||
return;
|
||||
|
||||
case 'ReconnectAfterLoss':
|
||||
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||
if (
|
||||
$this->ReadPropertyBoolean('Active')
|
||||
&& !$this->ReadPropertyBoolean('Testmodus')
|
||||
&& $this->GetBuffer('SignalRReady') !== '1'
|
||||
) {
|
||||
$this->verbindeSignalR();
|
||||
}
|
||||
return;
|
||||
|
||||
case 'RefreshToken':
|
||||
if (
|
||||
$this->erneuereZugangsdaten(false)
|
||||
&& $this->GetBuffer('SignalRReady') !== '1'
|
||||
) {
|
||||
$this->verbindeSignalR();
|
||||
}
|
||||
return;
|
||||
|
||||
case 'Reconnect':
|
||||
$this->SetBuffer('AccessToken', '');
|
||||
$this->SetBuffer('RefreshToken', '');
|
||||
if (
|
||||
$this->ReadPropertyBoolean('Testmodus')
|
||||
|| $this->erneuereZugangsdaten(false)
|
||||
) {
|
||||
$this->verbindeSignalR();
|
||||
}
|
||||
return;
|
||||
|
||||
case 'TestObservation':
|
||||
if (!$this->ReadPropertyBoolean('Testmodus') || !is_string($wert)) {
|
||||
throw new InvalidArgumentException('TestObservation ist nur im Testmodus zulaessig.');
|
||||
}
|
||||
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($daten)) {
|
||||
throw new InvalidArgumentException('TestObservation erwartet ein JSON-Objekt.');
|
||||
}
|
||||
$this->veroeffentlicheBeobachtung(
|
||||
EaseeGatewayProtokoll::seriennummer((string) ($daten['serialNumber'] ?? '')),
|
||||
(int) ($daten['id'] ?? 0),
|
||||
$daten['value'] ?? null
|
||||
);
|
||||
return;
|
||||
|
||||
case 'TestSignalRFrame':
|
||||
if (!$this->ReadPropertyBoolean('Testmodus') || !is_string($wert)) {
|
||||
throw new InvalidArgumentException(
|
||||
'TestSignalRFrame ist nur im Testmodus zulaessig.'
|
||||
);
|
||||
}
|
||||
$this->verarbeiteSignalRRahmen($wert);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||
}
|
||||
|
||||
public function GetConfigurationForParent(): string
|
||||
{
|
||||
return json_encode([
|
||||
'Active' => $this->ReadPropertyBoolean('Active')
|
||||
&& !$this->ReadPropertyBoolean('Testmodus'),
|
||||
'URL' => $this->GetBuffer('WebSocketURL'),
|
||||
'VerifyCertificate' => $this->ReadPropertyBoolean('VerifyCertificate'),
|
||||
'Headers' => '[]',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function ReceiveData($jsonString): void
|
||||
{
|
||||
$paket = json_decode((string) $jsonString, true);
|
||||
if (!is_array($paket) || !isset($paket['Buffer'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetBuffer('LastReceive', (string) time());
|
||||
$puffer = $this->GetBuffer('ReceiveBuffer') . (string) $paket['Buffer'];
|
||||
$rahmen = explode(self::RECORD_SEPARATOR, $puffer);
|
||||
$this->SetBuffer('ReceiveBuffer', (string) array_pop($rahmen));
|
||||
|
||||
foreach ($rahmen as $eintrag) {
|
||||
if ($eintrag !== '') {
|
||||
$this->verarbeiteSignalRRahmen($eintrag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function ForwardData($jsonString): string
|
||||
{
|
||||
$paket = json_decode((string) $jsonString, true);
|
||||
if (!is_array($paket) || !isset($paket['Buffer'])) {
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Ungueltiges Datenpaket.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
return $this->ProcessStationRequest((string) $paket['Buffer']);
|
||||
}
|
||||
|
||||
public function ProcessStationRequest($jsonString): string
|
||||
{
|
||||
$anfrage = json_decode((string) $jsonString, true);
|
||||
if (!is_array($anfrage) || !isset($anfrage['action'])) {
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Ungueltige Gateway-Anfrage.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
$seriennummer = EaseeGatewayProtokoll::seriennummer(
|
||||
(string) ($anfrage['serialNumber'] ?? '')
|
||||
);
|
||||
|
||||
switch ($anfrage['action']) {
|
||||
case 'Subscribe':
|
||||
case 'GetState':
|
||||
if ($seriennummer === '') {
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Seriennummer fehlt.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
$this->registriereAbonnement($seriennummer);
|
||||
return $this->erstelleStatusantwort($seriennummer);
|
||||
|
||||
case 'SetDynamicChargerCurrent':
|
||||
return json_encode($this->setzeDynamischenLadestrom(
|
||||
$seriennummer,
|
||||
(float) ($anfrage['amps'] ?? -1)
|
||||
), JSON_THROW_ON_ERROR);
|
||||
|
||||
case 'StartCharging':
|
||||
return json_encode(
|
||||
$this->sendeLadestart($seriennummer),
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Unbekannte Gateway-Aktion.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
private function stelleWebSocketParentSicher(): bool
|
||||
{
|
||||
$instanz = IPS_GetInstance($this->InstanceID);
|
||||
$parentID = (int) $instanz['ConnectionID'];
|
||||
|
||||
if ($parentID <= 0 && !$this->RequireParent(self::WEBSOCKET_MODULE_ID)) {
|
||||
$this->setzeLetztenFehler('WebSocket-Client konnte nicht erstellt werden.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$instanz = IPS_GetInstance($this->InstanceID);
|
||||
$parentID = (int) $instanz['ConnectionID'];
|
||||
if ($parentID <= 0 || !IPS_InstanceExists($parentID)) {
|
||||
$this->setzeLetztenFehler('WebSocket-Client ist nicht verbunden.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$parent = IPS_GetInstance($parentID);
|
||||
if ($parent['ModuleInfo']['ModuleID'] !== self::WEBSOCKET_MODULE_ID) {
|
||||
$this->setzeLetztenFehler('Ungueltige WebSocket-Schnittstelle.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function pflegeVerbindung(): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('Active') || $this->ReadPropertyBoolean('Testmodus')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$jetzt = time();
|
||||
$letzteAushandlung = (int) $this->GetBuffer('LastNegotiation');
|
||||
$letzterEmpfang = (int) $this->GetBuffer('LastReceive');
|
||||
$bereit = $this->GetBuffer('SignalRReady') === '1';
|
||||
|
||||
if ($bereit) {
|
||||
$this->sendeSignalRRahmen(['type' => 6]);
|
||||
if ($letzterEmpfang > 0 && ($jetzt - $letzterEmpfang) <= 90) {
|
||||
return;
|
||||
}
|
||||
$this->planeWiederverbindung(
|
||||
'Seit mehr als 90 Sekunden keine Easee-Ereignisdaten empfangen.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (($jetzt - $letzteAushandlung) >= 45) {
|
||||
$this->verbindeSignalR();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendeHandshake();
|
||||
}
|
||||
|
||||
private function verbindeSignalR(): bool
|
||||
{
|
||||
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
$this->setzeEreignisverbindung(true);
|
||||
return true;
|
||||
}
|
||||
if (!$this->stelleAccessTokenSicher()) {
|
||||
$this->SetBuffer('SignalRReady', '0');
|
||||
$this->setzeVerbunden(false);
|
||||
$this->SetStatus(202);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setzeEreignisverbindung(
|
||||
false,
|
||||
'Easee-Ereignisverbindung wird aufgebaut.'
|
||||
);
|
||||
$this->SetBuffer('LastNegotiation', (string) time());
|
||||
$token = $this->GetBuffer('AccessToken');
|
||||
$antwort = $this->httpAnfrage(
|
||||
'POST',
|
||||
self::SIGNALR_BASE_URL . '/negotiate?negotiateVersion=1',
|
||||
'',
|
||||
$token
|
||||
);
|
||||
if ($antwort['httpCode'] === 401 && $this->erneuereZugangsdaten(false)) {
|
||||
$token = $this->GetBuffer('AccessToken');
|
||||
$antwort = $this->httpAnfrage(
|
||||
'POST',
|
||||
self::SIGNALR_BASE_URL . '/negotiate?negotiateVersion=1',
|
||||
'',
|
||||
$token
|
||||
);
|
||||
}
|
||||
if (!$antwort['success']) {
|
||||
$this->setzeEreignisverbindung(
|
||||
false,
|
||||
'Easee-Ereignisverbindung fehlgeschlagen: HTTP '
|
||||
. $antwort['httpCode']
|
||||
. ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$daten = json_decode($antwort['body'], true);
|
||||
$verbindungstoken = is_array($daten)
|
||||
? (string) ($daten['connectionToken'] ?? $daten['connectionId'] ?? '')
|
||||
: '';
|
||||
if ($verbindungstoken === '') {
|
||||
$this->setzeEreignisverbindung(
|
||||
false,
|
||||
'Easee liefert kein Verbindungstoken.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = 'wss://streams.easee.com/hubs/chargers?id='
|
||||
. rawurlencode($verbindungstoken)
|
||||
. '&access_token=' . rawurlencode($token);
|
||||
$this->SetBuffer('WebSocketURL', $url);
|
||||
$this->SetBuffer('SignalRReady', '0');
|
||||
$this->SetBuffer('ReceiveBuffer', '');
|
||||
$this->SetBuffer('SubscribedThisConnection', '{}');
|
||||
$this->SetBuffer('PendingInvocations', '{}');
|
||||
$this->markiereBeobachtungscacheVeraltet();
|
||||
$this->setzeVerbunden(false);
|
||||
|
||||
if (!$this->konfiguriereWebSocketParent($url)) {
|
||||
$this->SetStatus(203);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->sendeHandshake();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function konfiguriereWebSocketParent(string $url): bool
|
||||
{
|
||||
$parentID = (int) IPS_GetInstance($this->InstanceID)['ConnectionID'];
|
||||
if ($parentID <= 0) {
|
||||
$this->setzeLetztenFehler('WebSocket-Client ist nicht verbunden.');
|
||||
return false;
|
||||
}
|
||||
|
||||
IPS_SetProperty($parentID, 'Active', true);
|
||||
IPS_SetProperty($parentID, 'URL', $url);
|
||||
IPS_SetProperty(
|
||||
$parentID,
|
||||
'VerifyCertificate',
|
||||
$this->ReadPropertyBoolean('VerifyCertificate')
|
||||
);
|
||||
IPS_SetProperty($parentID, 'Headers', '[]');
|
||||
IPS_ApplyChanges($parentID);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function sendeHandshake(): void
|
||||
{
|
||||
$this->sendeRohdaten(
|
||||
json_encode(['protocol' => 'json', 'version' => 1], JSON_THROW_ON_ERROR)
|
||||
. self::RECORD_SEPARATOR
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $rahmen */
|
||||
private function sendeSignalRRahmen(array $rahmen): void
|
||||
{
|
||||
$this->sendeRohdaten(
|
||||
json_encode($rahmen, JSON_THROW_ON_ERROR) . self::RECORD_SEPARATOR
|
||||
);
|
||||
}
|
||||
|
||||
private function sendeRohdaten(string $nutzdaten): void
|
||||
{
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
return;
|
||||
}
|
||||
$this->SendDataToParent(json_encode([
|
||||
'DataID' => self::SIMPLE_TX_DATA_ID,
|
||||
'Buffer' => $nutzdaten,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
private function verarbeiteSignalRRahmen(string $rahmen): void
|
||||
{
|
||||
if ($rahmen === '{}') {
|
||||
$this->setzeEreignisverbindung(true);
|
||||
$this->sendeAlleAbonnements();
|
||||
return;
|
||||
}
|
||||
|
||||
$nachricht = json_decode($rahmen, true);
|
||||
if (!is_array($nachricht)) {
|
||||
$this->setzeLetztenFehler('Ungueltige Easee-Ereignisnachricht.');
|
||||
return;
|
||||
}
|
||||
if (isset($nachricht['error'])) {
|
||||
$this->planeWiederverbindung(
|
||||
'Easee-Ereignisfehler: ' . $nachricht['error']
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$typ = (int) ($nachricht['type'] ?? 0);
|
||||
if ($typ === 6) {
|
||||
return;
|
||||
}
|
||||
if ($typ === 7) {
|
||||
$this->planeWiederverbindung(
|
||||
'Easee-Ereignisverbindung wurde beendet.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$seriennummer = '';
|
||||
if ($typ === 3 && isset($nachricht['invocationId'])) {
|
||||
$offen = $this->lesePufferArray('PendingInvocations');
|
||||
$aufrufID = (string) $nachricht['invocationId'];
|
||||
$seriennummer = (string) ($offen[$aufrufID] ?? '');
|
||||
unset($offen[$aufrufID]);
|
||||
$this->SetBuffer(
|
||||
'PendingInvocations',
|
||||
json_encode($offen, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
foreach (['arguments', 'result'] as $feld) {
|
||||
if (!isset($nachricht[$feld])) {
|
||||
continue;
|
||||
}
|
||||
$abonnements = array_keys($this->lesePufferArray('Subscriptions'));
|
||||
foreach (EaseeGatewayProtokoll::extrahiereBeobachtungen(
|
||||
$nachricht[$feld],
|
||||
$abonnements,
|
||||
$seriennummer
|
||||
) as $beobachtung) {
|
||||
$this->veroeffentlicheBeobachtung(
|
||||
$beobachtung['Seriennummer'],
|
||||
$beobachtung['ID'],
|
||||
$beobachtung['Wert']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function registriereAbonnement(string $seriennummer): void
|
||||
{
|
||||
$abonnements = $this->lesePufferArray('Subscriptions');
|
||||
if (!isset($abonnements[$seriennummer])) {
|
||||
$abonnements[$seriennummer] = true;
|
||||
$this->SetBuffer(
|
||||
'Subscriptions',
|
||||
json_encode($abonnements, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$this->SetValue('SubscriptionCount', count($abonnements));
|
||||
}
|
||||
|
||||
if ($this->GetBuffer('SignalRReady') === '1') {
|
||||
$this->sendeAbonnement($seriennummer);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendeAlleAbonnements(): void
|
||||
{
|
||||
foreach (array_keys($this->lesePufferArray('Subscriptions')) as $seriennummer) {
|
||||
$this->sendeAbonnement((string) $seriennummer);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendeAbonnement(string $seriennummer): void
|
||||
{
|
||||
$gesendet = $this->lesePufferArray('SubscribedThisConnection');
|
||||
if (isset($gesendet[$seriennummer])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aufrufID = (string) (((int) $this->GetBuffer('InvocationID')) + 1);
|
||||
$this->SetBuffer('InvocationID', $aufrufID);
|
||||
$offen = $this->lesePufferArray('PendingInvocations');
|
||||
$offen[$aufrufID] = $seriennummer;
|
||||
$this->SetBuffer(
|
||||
'PendingInvocations',
|
||||
json_encode($offen, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$this->sendeSignalRRahmen([
|
||||
'type' => 1,
|
||||
'invocationId' => $aufrufID,
|
||||
'target' => 'SubscribeWithCurrentState',
|
||||
'arguments' => [$seriennummer, true],
|
||||
]);
|
||||
|
||||
$gesendet[$seriennummer] = true;
|
||||
$this->SetBuffer(
|
||||
'SubscribedThisConnection',
|
||||
json_encode($gesendet, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
private function veroeffentlicheBeobachtung(
|
||||
string $seriennummer,
|
||||
int $id,
|
||||
$wert
|
||||
): void {
|
||||
if (
|
||||
$seriennummer === ''
|
||||
|| !in_array($id, EaseeGatewayProtokoll::BEOBACHTUNGEN, true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->speichereBeobachtungImCache($seriennummer, $id, $wert);
|
||||
|
||||
$this->SendDataToChildren(json_encode([
|
||||
'DataID' => self::CHILD_EVENT_DATA_ID,
|
||||
'Buffer' => json_encode([
|
||||
'type' => 'Observation',
|
||||
'serialNumber' => $seriennummer,
|
||||
'id' => $id,
|
||||
'value' => $wert,
|
||||
'timestamp' => time(),
|
||||
], JSON_THROW_ON_ERROR),
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
private function speichereBeobachtungImCache(
|
||||
string $seriennummer,
|
||||
int $id,
|
||||
$wert
|
||||
): void {
|
||||
$cache = $this->leseAttributArray('ObservationCache');
|
||||
if (!isset($cache[$seriennummer]) || !is_array($cache[$seriennummer])) {
|
||||
$cache[$seriennummer] = [];
|
||||
}
|
||||
if ($id === 109 && (int) $wert === 1) {
|
||||
foreach ([110, 120, 182, 183, 184, 185] as $sessionID) {
|
||||
$cache[$seriennummer][(string) $sessionID] = 0;
|
||||
}
|
||||
}
|
||||
$cache[$seriennummer][(string) $id] = $wert;
|
||||
$cache[$seriennummer]['updated'] = time();
|
||||
$this->WriteAttributeString(
|
||||
'ObservationCache',
|
||||
json_encode($cache, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
private function markiereBeobachtungscacheVeraltet(): void
|
||||
{
|
||||
$cache = $this->leseAttributArray('ObservationCache');
|
||||
foreach ($cache as &$zustand) {
|
||||
if (is_array($zustand)) {
|
||||
$zustand['updated'] = 0;
|
||||
}
|
||||
}
|
||||
unset($zustand);
|
||||
$this->WriteAttributeString(
|
||||
'ObservationCache',
|
||||
json_encode($cache, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
private function stelleAktuellenStatusSicher(string $seriennummer): void
|
||||
{
|
||||
$cache = $this->leseAttributArray('ObservationCache');
|
||||
$zustand = $cache[$seriennummer] ?? [];
|
||||
$cacheIstAktuell = is_array($zustand)
|
||||
&& array_key_exists('109', $zustand)
|
||||
&& (int) ($zustand['updated'] ?? 0) >= time() - self::CACHE_MAXIMALALTER_SEKUNDEN;
|
||||
if (
|
||||
$cacheIstAktuell
|
||||
|| $this->ReadPropertyBoolean('Testmodus')
|
||||
|| $this->GetBuffer('SignalRReady') !== '1'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$antwort = $this->autorisierteApiAnfrage(
|
||||
'GET',
|
||||
'/state/' . rawurlencode($seriennummer) . '/observations?ids='
|
||||
. rawurlencode(implode(',', EaseeGatewayProtokoll::BEOBACHTUNGEN)),
|
||||
''
|
||||
);
|
||||
if (!($antwort['success'] ?? false)) {
|
||||
$this->setzeLetztenFehler(
|
||||
'Easee-Status konnte nicht geladen werden: '
|
||||
. (string) ($antwort['error'] ?? 'Unbekannter API-Fehler.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$daten = json_decode((string) ($antwort['body'] ?? ''), true);
|
||||
if (!is_array($daten)) {
|
||||
$this->setzeLetztenFehler('Easee-Statusantwort ist ungueltig.');
|
||||
return;
|
||||
}
|
||||
$beobachtungen = EaseeGatewayProtokoll::extrahiereBeobachtungen(
|
||||
$daten,
|
||||
[$seriennummer],
|
||||
$seriennummer
|
||||
);
|
||||
$betriebsstatusGeladen = false;
|
||||
foreach ($beobachtungen as $beobachtung) {
|
||||
$this->speichereBeobachtungImCache(
|
||||
$beobachtung['Seriennummer'],
|
||||
$beobachtung['ID'],
|
||||
$beobachtung['Wert']
|
||||
);
|
||||
$betriebsstatusGeladen = $betriebsstatusGeladen
|
||||
|| $beobachtung['ID'] === 109;
|
||||
}
|
||||
if (!$betriebsstatusGeladen) {
|
||||
$this->setzeLetztenFehler(
|
||||
'Easee-Statusantwort enthaelt keinen Betriebsstatus (109).'
|
||||
);
|
||||
return;
|
||||
}
|
||||
$this->setzeLetztenFehler('');
|
||||
}
|
||||
|
||||
private function erstelleStatusantwort(string $seriennummer): string
|
||||
{
|
||||
$this->stelleAktuellenStatusSicher($seriennummer);
|
||||
$cache = $this->leseAttributArray('ObservationCache');
|
||||
return json_encode([
|
||||
'success' => true,
|
||||
'connected' => $this->GetBuffer('SignalRReady') === '1'
|
||||
|| $this->ReadPropertyBoolean('Testmodus'),
|
||||
'state' => $cache[$seriennummer] ?? [],
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function setzeDynamischenLadestrom(string $seriennummer, float $ampere): array
|
||||
{
|
||||
if ($seriennummer === '') {
|
||||
return ['success' => false, 'error' => 'Seriennummer fehlt.'];
|
||||
}
|
||||
if (
|
||||
!is_finite($ampere)
|
||||
|| floor($ampere) !== $ampere
|
||||
|| $ampere < 0
|
||||
|| $ampere > 32
|
||||
|| ($ampere > 0 && $ampere < 6)
|
||||
) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Strom muss 0 A oder eine ganze Zahl zwischen 6 und 32 A sein.',
|
||||
];
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
$this->SetBuffer('LetzterTestbefehl', json_encode([
|
||||
'serialNumber' => $seriennummer,
|
||||
'amps' => $ampere,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
return ['success' => true, 'httpCode' => 200, 'body' => '{}'];
|
||||
}
|
||||
|
||||
return $this->autorisierteApiAnfrage(
|
||||
'POST',
|
||||
'/api/chargers/' . rawurlencode($seriennummer)
|
||||
. '/commands/set_dynamic_charger_current',
|
||||
json_encode([
|
||||
'amps' => (int) round($ampere),
|
||||
'minutes' => 0,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function sendeLadestart(string $seriennummer): array
|
||||
{
|
||||
if ($seriennummer === '') {
|
||||
return ['success' => false, 'error' => 'Seriennummer fehlt.'];
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
return ['success' => true, 'httpCode' => 200, 'body' => '{}'];
|
||||
}
|
||||
|
||||
return $this->autorisierteApiAnfrage(
|
||||
'POST',
|
||||
'/api/chargers/' . rawurlencode($seriennummer)
|
||||
. '/commands/start_charging',
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function autorisierteApiAnfrage(
|
||||
string $methode,
|
||||
string $pfad,
|
||||
string $inhalt
|
||||
): array {
|
||||
if (!$this->stelleAccessTokenSicher()) {
|
||||
return ['success' => false, 'error' => 'Kein Easee-Access-Token.'];
|
||||
}
|
||||
|
||||
$antwort = $this->httpAnfrage(
|
||||
$methode,
|
||||
self::API_BASE_URL . $pfad,
|
||||
$inhalt,
|
||||
$this->GetBuffer('AccessToken')
|
||||
);
|
||||
if ($antwort['httpCode'] === 401 && $this->erneuereZugangsdaten(false)) {
|
||||
$antwort = $this->httpAnfrage(
|
||||
$methode,
|
||||
self::API_BASE_URL . $pfad,
|
||||
$inhalt,
|
||||
$this->GetBuffer('AccessToken')
|
||||
);
|
||||
}
|
||||
if (!$antwort['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $antwort['error'] !== ''
|
||||
? $antwort['error']
|
||||
: 'Easee-HTTP-Fehler ' . $antwort['httpCode'] . '.',
|
||||
'httpCode' => $antwort['httpCode'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'httpCode' => $antwort['httpCode'],
|
||||
'body' => $antwort['body'],
|
||||
];
|
||||
}
|
||||
|
||||
private function erneuereZugangsdaten(bool $neuVerbinden = true): bool
|
||||
{
|
||||
$tokenpaar = null;
|
||||
$refreshToken = $this->GetBuffer('RefreshToken');
|
||||
if ($refreshToken !== '') {
|
||||
$tokenpaar = $this->fordereTokenpaarAn(
|
||||
self::API_BASE_URL . '/api/accounts/refresh_token',
|
||||
['refreshToken' => $refreshToken]
|
||||
);
|
||||
}
|
||||
if ($tokenpaar === null) {
|
||||
$tokenpaar = $this->fordereTokenpaarAn(
|
||||
self::API_BASE_URL . '/api/accounts/login',
|
||||
[
|
||||
'userName' => $this->ReadPropertyString('Username'),
|
||||
'password' => $this->ReadPropertyString('Password'),
|
||||
]
|
||||
);
|
||||
}
|
||||
if ($tokenpaar === null || !isset($tokenpaar['accessToken'])) {
|
||||
$this->setzeLetztenFehler('Easee-Anmeldung oder Token-Erneuerung fehlgeschlagen.');
|
||||
$this->SetStatus(202);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->SetBuffer('AccessToken', (string) $tokenpaar['accessToken']);
|
||||
if (isset($tokenpaar['refreshToken'])) {
|
||||
$this->SetBuffer('RefreshToken', (string) $tokenpaar['refreshToken']);
|
||||
}
|
||||
if ($neuVerbinden) {
|
||||
return $this->verbindeSignalR();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function stelleAccessTokenSicher(): bool
|
||||
{
|
||||
return $this->GetBuffer('AccessToken') !== ''
|
||||
|| $this->erneuereZugangsdaten(false);
|
||||
}
|
||||
|
||||
/** @param array<string, string> $nutzdaten
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function fordereTokenpaarAn(string $url, array $nutzdaten): ?array
|
||||
{
|
||||
$antwort = $this->httpAnfrage(
|
||||
'POST',
|
||||
$url,
|
||||
json_encode($nutzdaten, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
if (!$antwort['success']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$daten = json_decode($antwort['body'], true);
|
||||
return is_array($daten) ? $daten : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, body: string, error: string, httpCode: int}
|
||||
*/
|
||||
private function httpAnfrage(
|
||||
string $methode,
|
||||
string $url,
|
||||
string $inhalt = '',
|
||||
string $bearerToken = ''
|
||||
): array {
|
||||
$kopf = ['Accept: application/json', 'Content-Type: application/json'];
|
||||
if ($bearerToken !== '') {
|
||||
$kopf[] = 'Authorization: Bearer ' . $bearerToken;
|
||||
}
|
||||
|
||||
$curl = curl_init($url);
|
||||
if ($curl === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'body' => '',
|
||||
'error' => 'HTTP-Anfrage konnte nicht initialisiert werden.',
|
||||
'httpCode' => 0,
|
||||
];
|
||||
}
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => $methode,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_HTTPHEADER => $kopf,
|
||||
CURLOPT_POSTFIELDS => $inhalt,
|
||||
CURLOPT_SSL_VERIFYPEER => $this->ReadPropertyBoolean('VerifyCertificate'),
|
||||
CURLOPT_SSL_VERIFYHOST => $this->ReadPropertyBoolean('VerifyCertificate') ? 2 : 0,
|
||||
]);
|
||||
|
||||
$antwort = curl_exec($curl);
|
||||
$fehler = curl_error($curl);
|
||||
$httpStatus = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
|
||||
return [
|
||||
'success' => $antwort !== false
|
||||
&& $fehler === ''
|
||||
&& $httpStatus >= 200
|
||||
&& $httpStatus < 300,
|
||||
'body' => $antwort === false ? '' : (string) $antwort,
|
||||
'error' => $fehler,
|
||||
'httpCode' => $httpStatus,
|
||||
];
|
||||
}
|
||||
|
||||
private function setzeEreignisverbindung(
|
||||
bool $verbunden,
|
||||
string $fehler = ''
|
||||
): void {
|
||||
$this->SetBuffer('SignalRReady', $verbunden ? '1' : '0');
|
||||
$this->SetStatus($verbunden ? 102 : 203);
|
||||
$this->setzeLetztenFehler($verbunden ? '' : $fehler);
|
||||
if ($verbunden) {
|
||||
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||
}
|
||||
$this->setzeVerbunden($verbunden);
|
||||
}
|
||||
|
||||
private function planeWiederverbindung(string $fehler): void
|
||||
{
|
||||
$this->setzeEreignisverbindung(false, $fehler);
|
||||
$this->SetBuffer('LastNegotiation', '0');
|
||||
if (
|
||||
$this->ReadPropertyBoolean('Active')
|
||||
&& !$this->ReadPropertyBoolean('Testmodus')
|
||||
) {
|
||||
$this->SetTimerInterval('ReconnectTimer', 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private function setzeVerbunden(bool $verbunden): void
|
||||
{
|
||||
$warVerbunden = (bool) $this->GetValue('Connected');
|
||||
$this->SetValue('Connected', $verbunden);
|
||||
if ($warVerbunden === $verbunden) {
|
||||
return;
|
||||
}
|
||||
$this->SendDataToChildren(json_encode([
|
||||
'DataID' => self::CHILD_EVENT_DATA_ID,
|
||||
'Buffer' => json_encode([
|
||||
'type' => 'GatewayStatus',
|
||||
'connected' => $verbunden,
|
||||
], JSON_THROW_ON_ERROR),
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
private function setzeLetztenFehler(string $nachricht): void
|
||||
{
|
||||
$this->SetValue('LastError', $nachricht);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function leseAttributArray(string $name): array
|
||||
{
|
||||
$wert = json_decode($this->ReadAttributeString($name), true);
|
||||
return is_array($wert) ? $wert : [];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function lesePufferArray(string $name): array
|
||||
{
|
||||
$wert = json_decode($this->GetBuffer($name), true);
|
||||
return is_array($wert) ? $wert : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Ladestation Gateway
|
||||
|
||||
Eventbasiertes Enelix-EMS-Verbrauchermodul fuer eine Easee-Ladestation. Das
|
||||
Modul liest Fahrzeugstatus und aktive 1-/3-Phasenladung direkt aus Easee
|
||||
Observations und sendet Stromvorgaben ueber das verbundene Easee Gateway.
|
||||
|
||||
Konfiguration, Betriebsarten und Tests:
|
||||
[Moduldokumentation](../docs/module/Ladestation-Gateway/README.md)
|
||||
|
||||
Gateway-Vertrag:
|
||||
[Schnittstellenbeschreibung](../docs/Schnittstelle-Easee-Gateway.md)
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Betriebsmodus",
|
||||
"caption": "Variante",
|
||||
"options": [
|
||||
{
|
||||
"caption": "Easee",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"caption": "Easee - Nur Solarladen",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "Ladestationskennung",
|
||||
"caption": "Easee Seriennummer"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaximalerLadestrom",
|
||||
"caption": "Maximaler Ladestrom",
|
||||
"minimum": 6,
|
||||
"maximum": 32,
|
||||
"suffix": " A"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "Ladefreigabe",
|
||||
"caption": "Ladefreigabe beim Start"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "Solarladen",
|
||||
"caption": "Solarladen beim Start"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPV",
|
||||
"caption": "Prioritaet PV",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPeak",
|
||||
"caption": "Prioritaet Peak",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Meldeintervall",
|
||||
"caption": "Meldeintervall",
|
||||
"minimum": 1,
|
||||
"suffix": " s"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "VorgabeTimeout",
|
||||
"caption": "Vorgabe-Timeout",
|
||||
"minimum": 1,
|
||||
"suffix": " s"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EinstellungenInVisu",
|
||||
"caption": "Einstellungen in Visualisierung"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "DiagnosevariablenAnzeigen",
|
||||
"caption": "Diagnosevariablen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LoggingEin",
|
||||
"caption": "Diagnoseprotokoll aktivieren"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Easee-Zugangsdaten werden ausschliesslich im verbundenen Easee Gateway gespeichert."
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Button",
|
||||
"caption": "Status neu anfordern",
|
||||
"onClick": "IPS_RequestAction($id, \"StatusAnfordern\", false);"
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{
|
||||
"code": 201,
|
||||
"icon": "error",
|
||||
"caption": "Konfiguration ungueltig"
|
||||
},
|
||||
{
|
||||
"code": 202,
|
||||
"icon": "error",
|
||||
"caption": "Easee Gateway nicht verbunden"
|
||||
},
|
||||
{
|
||||
"code": 203,
|
||||
"icon": "inactive",
|
||||
"caption": "Warte auf Easee-Status"
|
||||
},
|
||||
{
|
||||
"code": 204,
|
||||
"icon": "error",
|
||||
"caption": "Easee Ladestation meldet Fehler"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}",
|
||||
"name": "LadestationGateway",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Ladestation Gateway"
|
||||
],
|
||||
"parentRequirements": [
|
||||
"{7AEF3DF7-DA5B-47C5-BCDC-0110D06DDC04}"
|
||||
],
|
||||
"childRequirements": [],
|
||||
"implemented": [
|
||||
"{107D5CFA-8F3D-4E38-8643-90DDFE6A3B4D}"
|
||||
],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/LadestationGateway"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Manager und Zeitverhalten",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPV",
|
||||
"caption": "Prioritaet PV",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPeak",
|
||||
"caption": "Prioritaet Peak",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Meldeintervall",
|
||||
"caption": "Meldeintervall",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "VorgabeTimeout",
|
||||
"caption": "Vorgabe-Timeout",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Abfrageintervall",
|
||||
"caption": "Geraetestatus abfragen",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Phasenerkennungszeit",
|
||||
"caption": "Phasen erkennen nach",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "FahrzeugstromErkennungszeit",
|
||||
"caption": "Fahrzeug-Maximalstrom erkennen nach",
|
||||
"suffix": " s",
|
||||
"minimum": 30
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "ZeitZwischenZustandswechseln",
|
||||
"caption": "Zeit zwischen Leistungsaenderungen",
|
||||
"suffix": " min",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindesteinschaltdauer",
|
||||
"caption": "Mindesteinschaltdauer",
|
||||
"suffix": " min",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindestausschaltdauer",
|
||||
"caption": "Mindestausschaltdauer",
|
||||
"suffix": " min",
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Ladestation",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Geraetetyp",
|
||||
"caption": "Geraetetyp",
|
||||
"options": [
|
||||
{
|
||||
"caption": "Nicht konfiguriert",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"caption": "go-e Charger (alte API)",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"caption": "go-e Charger Gemini / Gemini flex",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"caption": "smart-me Pico",
|
||||
"value": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "Geraeteadresse",
|
||||
"caption": "IP-Adresse oder Hostname (go-e)"
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "GeraeteID",
|
||||
"caption": "Geraete-ID (Pico)"
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "Seriennummer",
|
||||
"caption": "Seriennummer (Pico)"
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "Benutzername",
|
||||
"caption": "Benutzername (Pico)"
|
||||
},
|
||||
{
|
||||
"type": "PasswordTextBox",
|
||||
"name": "Passwort",
|
||||
"caption": "Passwort (Pico)"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaximalerLadestrom",
|
||||
"caption": "Maximaler Ladestrom",
|
||||
"suffix": " A",
|
||||
"minimum": 6,
|
||||
"maximum": 32
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "Ladefreigabe",
|
||||
"caption": "Ladefreigabe beim Start"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "Solarladen",
|
||||
"caption": "Solarladen beim Start"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Darstellung und Diagnose",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EinstellungenInVisu",
|
||||
"caption": "Ladefreigabe und Solarladen in der Visualisierung anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "DiagnosevariablenAnzeigen",
|
||||
"caption": "Diagnosevariablen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LoggingEin",
|
||||
"caption": "Diagnoseprotokoll aktivieren"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{
|
||||
"code": 102,
|
||||
"icon": "active",
|
||||
"caption": "Aktiv"
|
||||
},
|
||||
{
|
||||
"code": 201,
|
||||
"icon": "error",
|
||||
"caption": "Konfiguration ungueltig"
|
||||
},
|
||||
{
|
||||
"code": 202,
|
||||
"icon": "error",
|
||||
"caption": "Geraetekommunikation fehlgeschlagen"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "{0D94913C-0F31-4C29-A685-6EB4AE58E55D}",
|
||||
"name": "LadestationStandAlone",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Ladestation Stand-Alone"
|
||||
],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/LadestationStandAlone"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,927 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Lizenzierung",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "Lizenzcode",
|
||||
"caption": "Lizenzcode",
|
||||
"placeholder": "ENX-XXXX-XXXX-XXXX-XXXX"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"name": "LizenzInstallationID",
|
||||
"caption": "Installations-ID wird erzeugt"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"name": "LizenzInformation",
|
||||
"caption": "Lizenzcode fehlt."
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "LizenzPruefen",
|
||||
"caption": "Lizenz pruefen und binden",
|
||||
"onClick": "IPS_RequestAction($id, 'FormLizenzPruefen', $Lizenzcode);"
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "LizenzVerwalten",
|
||||
"caption": "Lizenz verwalten",
|
||||
"link": true,
|
||||
"onClick": "echo 'https://license.enelix.ch';"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Grundeinstellungen",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Rolle",
|
||||
"caption": "Rolle",
|
||||
"options": [
|
||||
{"caption": "Alleine", "value": 0},
|
||||
{"caption": "Hauptmanager", "value": 1},
|
||||
{"caption": "Untermanager", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "NetzleistungVariableID",
|
||||
"caption": "Netzleistung"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "SollwertSolarladen",
|
||||
"caption": "Sollwert Solarladen",
|
||||
"suffix": " W",
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Lastspitzenmodus",
|
||||
"caption": "Lastspitzenmodus",
|
||||
"onChange": "IPS_RequestAction($id, 'FormLastspitzenmodus', $Lastspitzenmodus);",
|
||||
"options": [
|
||||
{"caption": "Aus", "value": 0},
|
||||
{"caption": "Konstant", "value": 1},
|
||||
{"caption": "Monatlich", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Lastspitzengrenze",
|
||||
"caption": "Lastspitzengrenze",
|
||||
"suffix": " W",
|
||||
"digits": 1,
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "MonatsgrenzenUmschalten",
|
||||
"caption": "Monatsgrenzen anzeigen",
|
||||
"visible": false,
|
||||
"onClick": "IPS_RequestAction($id, 'FormMonatsgrenzenUmschalten', true);"
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "Monatsgrenzen",
|
||||
"caption": "Monatliche Lastspitzengrenzen",
|
||||
"rowCount": 12,
|
||||
"add": false,
|
||||
"delete": false,
|
||||
"sortable": false,
|
||||
"loadValuesFromConfiguration": false,
|
||||
"visible": false,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "Monat",
|
||||
"name": "Monat",
|
||||
"width": "auto",
|
||||
"save": true
|
||||
},
|
||||
{
|
||||
"caption": "Grenze",
|
||||
"name": "Grenze_W",
|
||||
"width": "180px",
|
||||
"save": true,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " W",
|
||||
"digits": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Monatsnummer",
|
||||
"name": "MonatIndex",
|
||||
"width": "0px",
|
||||
"visible": false,
|
||||
"save": true
|
||||
}
|
||||
],
|
||||
"values": []
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"name": "PeakShavingTitel",
|
||||
"caption": "Peak Shaving am Netzanschlusspunkt"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EinspeisebegrenzungAktiv",
|
||||
"caption": "Anlagenweite Einspeisebegrenzung aktivieren"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Einspeisegrenze",
|
||||
"caption": "Maximale Einspeisung der Gesamtanlage",
|
||||
"suffix": " W",
|
||||
"minimum": 0.0,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "AutomatischeSuche",
|
||||
"caption": "Verbraucher automatisch suchen",
|
||||
"onChange": "IPS_RequestAction($id, 'FormAutomatischeSuche', $AutomatischeSuche);"
|
||||
},
|
||||
{
|
||||
"type": "SelectObject",
|
||||
"name": "SuchbereichID",
|
||||
"caption": "Suchbereich",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "VerbraucherAktualisieren",
|
||||
"caption": "Verbraucher aktualisieren",
|
||||
"visible": true,
|
||||
"onClick": "IPS_RequestAction($id, 'FormVerbraucherAktualisieren', json_encode(['SuchbereichID' => $SuchbereichID, 'Zuordnung' => $AutomatischeVerbraucherZuordnung]));"
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "VerbraucherZuordnung",
|
||||
"caption": "Verbraucher manuell auswaehlen",
|
||||
"rowCount": 6,
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"visible": false,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "Instanz",
|
||||
"name": "InstanzID",
|
||||
"width": "auto",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectInstance"}
|
||||
},
|
||||
{
|
||||
"caption": "Aktiv",
|
||||
"name": "Aktiv",
|
||||
"width": "100px",
|
||||
"add": false,
|
||||
"edit": {"type": "CheckBox"}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "AutomatischeVerbraucherZuordnung",
|
||||
"caption": "Gefundene Verbraucher",
|
||||
"rowCount": 8,
|
||||
"add": false,
|
||||
"delete": false,
|
||||
"loadValuesFromConfiguration": false,
|
||||
"visible": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "Verbraucher",
|
||||
"name": "Name",
|
||||
"width": "auto",
|
||||
"save": true
|
||||
},
|
||||
{
|
||||
"caption": "Instanz-ID",
|
||||
"name": "InstanzID",
|
||||
"width": "100px",
|
||||
"save": true
|
||||
},
|
||||
{
|
||||
"caption": "Verwenden",
|
||||
"name": "Aktiv",
|
||||
"width": "100px",
|
||||
"save": true,
|
||||
"edit": {"type": "CheckBox"}
|
||||
}
|
||||
],
|
||||
"values": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Anlagentopologie",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "RowLayout",
|
||||
"items": [
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "NetzbezugEnergieVariableID",
|
||||
"caption": "Netzbezugszaehler"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "NetzbezugEnergiefaktor",
|
||||
"caption": "Faktor",
|
||||
"digits": 6
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "NetzeinspeisungEnergieVariableID",
|
||||
"caption": "Einspeisezaehler"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "NetzeinspeisungEnergiefaktor",
|
||||
"caption": "Faktor",
|
||||
"digits": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Energiezaehler werden in kWh erwartet; ueber den Faktor koennen beispielsweise Wh (0,001) umgerechnet werden."
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Die Wechselrichter-Tabelle beschreibt die AC-Wandler der Anlage. PV-Flaechen und Batteriespeicher verweisen darauf, damit gemeinsame Leistungsgrenzen und die elektrische Kopplung eindeutig sind. Bei einem Hybridgeraet verwenden PV und Batterie dieselbe Wechselrichter-ID; bei einem AC-gekoppelten Speicher wird dessen separater Batteriewechselrichter erfasst."
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "AnlagenWechselrichter",
|
||||
"caption": "Wechselrichter / AC-Wandler",
|
||||
"rowCount": 4,
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "ID",
|
||||
"name": "ID",
|
||||
"width": "130px",
|
||||
"add": "wr-1",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "Name",
|
||||
"name": "Name",
|
||||
"width": "auto",
|
||||
"add": "Wechselrichter",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "Typ",
|
||||
"name": "Typ",
|
||||
"width": "130px",
|
||||
"add": "pv",
|
||||
"edit": {
|
||||
"type": "Select",
|
||||
"options": [
|
||||
{"caption": "PV", "value": "pv"},
|
||||
{"caption": "Batterie", "value": "battery"},
|
||||
{"caption": "Hybrid", "value": "hybrid"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "AC Nennleistung",
|
||||
"name": "ACNennleistung_kW",
|
||||
"width": "150px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kW",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Istleistung",
|
||||
"name": "IstleistungVariableID",
|
||||
"width": "140px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Messfaktor",
|
||||
"name": "Istleistungsfaktor",
|
||||
"width": "110px",
|
||||
"add": 1.0,
|
||||
"edit": {"type": "NumberSpinner", "digits": 4}
|
||||
},
|
||||
{
|
||||
"caption": "Erzeugungsenergie",
|
||||
"name": "ErzeugungsenergieVariableID",
|
||||
"width": "160px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Energiefaktor",
|
||||
"name": "Erzeugungsenergiefaktor",
|
||||
"width": "110px",
|
||||
"add": 1.0,
|
||||
"edit": {"type": "NumberSpinner", "digits": 6}
|
||||
},
|
||||
{
|
||||
"caption": "PV-Begrenzung",
|
||||
"name": "BegrenzungVariableID",
|
||||
"width": "150px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Stellwert",
|
||||
"name": "Begrenzungsart",
|
||||
"width": "120px",
|
||||
"add": "percent",
|
||||
"edit": {
|
||||
"type": "Select",
|
||||
"options": [
|
||||
{"caption": "Prozent", "value": "percent"},
|
||||
{"caption": "Watt", "value": "watt"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "AnlagenPVFlaechen",
|
||||
"caption": "PV-Flaechen",
|
||||
"rowCount": 6,
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "ID",
|
||||
"name": "ID",
|
||||
"width": "120px",
|
||||
"add": "pv-1",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "Name",
|
||||
"name": "Name",
|
||||
"width": "auto",
|
||||
"add": "PV-Flaeche",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "DC-Leistung",
|
||||
"name": "DCLeistung_kWp",
|
||||
"width": "130px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kWp",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Neigung",
|
||||
"name": "Neigung_Grad",
|
||||
"width": "110px",
|
||||
"add": 30.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " Grad",
|
||||
"minimum": 0.0,
|
||||
"maximum": 90.0,
|
||||
"digits": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Azimut",
|
||||
"name": "Azimut_Grad",
|
||||
"width": "110px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " Grad",
|
||||
"minimum": -180.0,
|
||||
"maximum": 180.0,
|
||||
"digits": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Wechselrichter-ID",
|
||||
"name": "WechselrichterID",
|
||||
"width": "150px",
|
||||
"add": "wr-1",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "MPPT",
|
||||
"name": "MPPT",
|
||||
"width": "90px",
|
||||
"add": "",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "Module",
|
||||
"name": "Modulanzahl",
|
||||
"width": "90px",
|
||||
"add": 0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"minimum": 0,
|
||||
"digits": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Modulleistung",
|
||||
"name": "Modulleistung_Wp",
|
||||
"width": "130px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " Wp",
|
||||
"minimum": 0.0,
|
||||
"digits": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Der Batterieeintrag beschreibt den Speicher selbst: Kapazitaet, Leistung, Ladezustand und Energiezaehler. Die Wechselrichter-ID ist nur die Verknuepfung zum zugehoerigen AC-Wandler und keine doppelte Batteriekonfiguration."
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "AnlagenBatterien",
|
||||
"caption": "Batteriespeicher",
|
||||
"rowCount": 4,
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "ID",
|
||||
"name": "ID",
|
||||
"width": "120px",
|
||||
"add": "bat-1",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "Name",
|
||||
"name": "Name",
|
||||
"width": "auto",
|
||||
"add": "Batteriespeicher",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "Nennkapazitaet",
|
||||
"name": "Nennkapazitaet_kWh",
|
||||
"width": "150px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kWh",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Nutzkapazitaet",
|
||||
"name": "Nutzkapazitaet_kWh",
|
||||
"width": "150px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kWh",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Max. Laden",
|
||||
"name": "MaxLadeleistung_kW",
|
||||
"width": "130px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kW",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Max. Entladen",
|
||||
"name": "MaxEntladeleistung_kW",
|
||||
"width": "140px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kW",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Wechselrichter-ID",
|
||||
"name": "WechselrichterID",
|
||||
"width": "150px",
|
||||
"add": "wr-1",
|
||||
"edit": {"type": "ValidationTextBox"}
|
||||
},
|
||||
{
|
||||
"caption": "Kopplung",
|
||||
"name": "Kopplung",
|
||||
"width": "130px",
|
||||
"add": "ac",
|
||||
"edit": {
|
||||
"type": "Select",
|
||||
"options": [
|
||||
{"caption": "AC", "value": "ac"},
|
||||
{"caption": "DC", "value": "dc"},
|
||||
{"caption": "Hybrid", "value": "hybrid"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Leistung",
|
||||
"name": "LeistungVariableID",
|
||||
"width": "140px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Messfaktor",
|
||||
"name": "Leistungsfaktor",
|
||||
"width": "110px",
|
||||
"add": 1.0,
|
||||
"edit": {"type": "NumberSpinner", "digits": 4}
|
||||
},
|
||||
{
|
||||
"caption": "Ladezustand",
|
||||
"name": "SOCVariableID",
|
||||
"width": "140px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Ladeenergie",
|
||||
"name": "LadeenergieVariableID",
|
||||
"width": "140px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Entladeenergie",
|
||||
"name": "EntladeenergieVariableID",
|
||||
"width": "150px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Energiefaktor",
|
||||
"name": "Energiefaktor",
|
||||
"width": "110px",
|
||||
"add": 1.0,
|
||||
"edit": {"type": "NumberSpinner", "digits": 6}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Erweiterte Einstellungen",
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "KeepAlive",
|
||||
"caption": "Keep alive",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "VerbraucherTimeout",
|
||||
"caption": "Verbraucher-Timeout",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MesswertMaxAlter",
|
||||
"caption": "Maximales Messwertalter",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Umschaltdifferenz",
|
||||
"caption": "Mindestdifferenz vor Umschaltung",
|
||||
"suffix": " %",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Netzleistungsfaktor",
|
||||
"caption": "Netzleistungsfaktor",
|
||||
"digits": 4
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "DiagnosevariablenAnzeigen",
|
||||
"caption": "Diagnosevariablen anlegen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LoggingEin",
|
||||
"caption": "Laufendes Debug-Logging aktivieren"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Energieaufzeichnung",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnergieaufzeichnungAktiv",
|
||||
"caption": "Leistungs- und Energiewerte automatisch aufzeichnen"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "PV und Batterie werden aus der Anlagentopologie summiert. Der Hausverbrauch wird aus PV + Netz - Batterie berechnet. Netz: positiv Bezug; Batterie: positiv Laden."
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "MesswerteAnzeigen",
|
||||
"caption": "Einzelne Leistungs- und Energiewerte unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "RowLayout",
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "LeistungsaufzeichnungMinuten",
|
||||
"caption": "Leistung verdichten",
|
||||
"options": [
|
||||
{"caption": "1 Minute", "value": 1},
|
||||
{"caption": "5 Minuten", "value": 5},
|
||||
{"caption": "1 Stunde", "value": 60}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "LeistungLoeschenMonate",
|
||||
"caption": "Leistung loeschen nach",
|
||||
"minimum": 0,
|
||||
"suffix": " Monaten (0 = nie)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "RowLayout",
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "EnergieaufzeichnungMinuten",
|
||||
"caption": "Energie verdichten",
|
||||
"options": [
|
||||
{"caption": "1 Minute", "value": 1},
|
||||
{"caption": "5 Minuten", "value": 5},
|
||||
{"caption": "1 Stunde", "value": 60}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "EnergieVerdichtenMonate",
|
||||
"caption": "Auf Tageswerte verdichten nach",
|
||||
"minimum": 0,
|
||||
"suffix": " Monaten"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "EnergieLoeschenMonate",
|
||||
"caption": "Energie loeschen nach",
|
||||
"minimum": 0,
|
||||
"suffix": " Monaten (0 = nie)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnergyPieAnzeigen",
|
||||
"caption": "Energy Pie unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnergiediagrammeAnzeigen",
|
||||
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "RowLayout",
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "DiagrammPVModus",
|
||||
"caption": "PV in Diagrammen",
|
||||
"options": [
|
||||
{"caption": "Summe", "value": 0},
|
||||
{"caption": "Einzelne Anlagen", "value": 1},
|
||||
{"caption": "Summe und einzelne Anlagen", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "DiagrammBatterieModus",
|
||||
"caption": "Batterien in Diagrammen",
|
||||
"options": [
|
||||
{"caption": "Summe", "value": 0},
|
||||
{"caption": "Einzelne Speicher", "value": 1},
|
||||
{"caption": "Summe und einzelne Speicher", "value": 2}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LadestationenSeparatAnzeigen",
|
||||
"caption": "Ladestationen separat in Energiefluss und Diagrammen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "VerbraucherSeparatAnzeigen",
|
||||
"caption": "Uebrige Verbraucher separat in Energiefluss und Diagrammen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Bestehende Energy Pies, Diagramme und Energiefluesse bleiben bei Apply unveraendert. Fuer eine Neuerzeugung den jeweiligen Schalter ausschalten, Apply ausfuehren, wieder einschalten und erneut Apply ausfuehren."
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "FunFactsAnzeigen",
|
||||
"caption": "Energy Facts unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnergieflussAnzeigen",
|
||||
"caption": "Energiefluss unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "EnergieflussLeistungseinheit",
|
||||
"caption": "Leistungseinheit im Energiefluss",
|
||||
"options": [
|
||||
{"caption": "Watt (W)", "value": 0},
|
||||
{"caption": "Kilowatt (kW)", "value": 1}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"name": "PrognoseForecast",
|
||||
"caption": "Prognose / Forecast",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "PrognoseAktiv",
|
||||
"caption": "Prognosen aktivieren",
|
||||
"onChange": "IPS_RequestAction($id, 'FormPrognoseAktiv', $PrognoseAktiv);"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"name": "PrognoseLizenzInformation",
|
||||
"caption": "Prognoselizenz wird geprueft."
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Forecast verwendet dieselben Messquellen aus der Anlagentopologie und den daraus berechneten Hausverbrauch."
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "NetzfahrplanAktiv",
|
||||
"caption": "Intelligenten Netzfahrplan zur Vermeidung von PV-Abregelung verwenden"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrognoseSendeintervall",
|
||||
"caption": "Aktuelle Messwerte an den Prognosedienst senden alle",
|
||||
"minimum": 60,
|
||||
"maximum": 3600,
|
||||
"suffix": " Sekunden"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "SDL / VGT",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "SDLAktiv",
|
||||
"caption": "SDL aktiv",
|
||||
"onChange": "IPS_RequestAction($id, 'FormSDLAktiv', $SDLAktiv);"
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"name": "SDLKonfiguration",
|
||||
"caption": "SDL / Regelenergie Messung",
|
||||
"visible": false,
|
||||
"items": [
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "SDLLeistungVariableID",
|
||||
"caption": "SDL Istleistung"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "SDLLeistungsfaktor",
|
||||
"caption": "SDL Leistungsfaktor nach Watt",
|
||||
"digits": 3
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "SDLSOCVariableID",
|
||||
"caption": "SDL SOC in Prozent (optional)"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "SDLEnergieflussAnzeigen",
|
||||
"caption": "SDL mit SOC im Energiefluss anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "SDLDiagrammeAnzeigen",
|
||||
"caption": "SDL mit SOC in den Diagrammen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Nach Skalierung: positiv = Laden / Bezug, negativ = Entladen / Abgabe. SDL muss separat gemessen sein und darf nicht bereits in der normalen Batterieleistung enthalten sein. Die Hauslast fuer Prognosen wird unabhaengig von den Anzeigeoptionen korrigiert."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ValidationTextBox",
|
||||
"name": "SDLAnschluss",
|
||||
"caption": "Konfiguration als JSON"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Stoerueberwachung",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "StoerueberwachungAktiv",
|
||||
"caption": "Stoerungen sicher an license.enelix.ch uebertragen"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "StoerungsSendeintervall",
|
||||
"caption": "Heartbeat-Intervall",
|
||||
"minimum": 60,
|
||||
"maximum": 3600,
|
||||
"suffix": " Sekunden"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Die Freischaltung als jaehrlich erneuerbare Lizenz ist vorbereitet und wird spaeter aktiviert."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Button",
|
||||
"caption": "Regelung jetzt ausfuehren",
|
||||
"onClick": "IPS_RequestAction($id, 'Regeln', true);"
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{"code": 102, "icon": "active", "caption": "Aktiv"},
|
||||
{"code": 201, "icon": "inactive", "caption": "Netzleistungsmessung fehlt oder ist veraltet"},
|
||||
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"},
|
||||
{"code": 203, "icon": "error", "caption": "Lizenz nicht freigegeben oder Verbraucherkontingent ueberschritten"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}",
|
||||
"name": "Manager",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Manager"
|
||||
],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Manager"
|
||||
}
|
||||
+3369
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Manager und Zeitverhalten",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{"type": "NumberSpinner", "name": "PrioritaetPV", "caption": "Prioritaet PV", "minimum": 0},
|
||||
{"type": "NumberSpinner", "name": "PrioritaetPeak", "caption": "Prioritaet Peak", "minimum": 0},
|
||||
{"type": "NumberSpinner", "name": "Meldeintervall", "caption": "Meldeintervall", "suffix": " s", "minimum": 1},
|
||||
{"type": "NumberSpinner", "name": "VorgabeTimeout", "caption": "Vorgabe-Timeout", "suffix": " s", "minimum": 1},
|
||||
{"type": "NumberSpinner", "name": "LastwechselSperrzeit", "caption": "Lastwechsel-Sperrzeit", "suffix": " s", "minimum": 0}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Pufferspeicher",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "List",
|
||||
"name": "LeistungsStufen",
|
||||
"caption": "Leistungsstufen",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "Stufe",
|
||||
"name": "Stufe",
|
||||
"width": "100px",
|
||||
"add": 1,
|
||||
"edit": {"type": "NumberSpinner", "minimum": 1}
|
||||
},
|
||||
{
|
||||
"caption": "Leistung",
|
||||
"name": "Leistung",
|
||||
"width": "180px",
|
||||
"add": 0,
|
||||
"edit": {"type": "NumberSpinner", "minimum": 1, "suffix": " W"}
|
||||
},
|
||||
{
|
||||
"caption": "Schaltkontakt",
|
||||
"name": "Schaltkontakt_Stufe",
|
||||
"width": "300px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable", "validVariableTypes": [0]}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "PufferfuehlerVariableID",
|
||||
"caption": "Puffertemperatur",
|
||||
"validVariableTypes": [1, 2]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "AussentemperaturVariableID",
|
||||
"caption": "Aussentemperatur",
|
||||
"validVariableTypes": [1, 2]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Heizkurve",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "FusspunktVorlauftemperatur",
|
||||
"caption": "Vorlauftemperatur bei 20 Grad C aussen",
|
||||
"suffix": " Grad C",
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "HeizkurvenSteigung",
|
||||
"caption": "Heizkurvensteigung",
|
||||
"minimum": 0,
|
||||
"digits": 2
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "HeizkurveMinimaltemperatur",
|
||||
"caption": "Untere Begrenzung der Solltemperatur",
|
||||
"suffix": " Grad C",
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "HeizkurveMaximaltemperatur",
|
||||
"caption": "Obere Begrenzung der Solltemperatur",
|
||||
"suffix": " Grad C",
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Hysterese",
|
||||
"caption": "Hysterese",
|
||||
"suffix": " K",
|
||||
"minimum": 0.1,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "MindesttemperaturModus",
|
||||
"caption": "Optionale Mindesttemperatur",
|
||||
"options": [
|
||||
{"caption": "Aus", "value": 0},
|
||||
{"caption": "Statisch", "value": 1},
|
||||
{"caption": "Differenz zur Solltemperatur", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindesttemperatur",
|
||||
"caption": "Statische Mindesttemperatur",
|
||||
"suffix": " Grad C",
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MindesttemperaturDifferenz",
|
||||
"caption": "Differenz zur Solltemperatur",
|
||||
"suffix": " K",
|
||||
"minimum": 0,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "WaermepumpenSolltemperaturVariableID",
|
||||
"caption": "Optionale Solltemperatur der Waermepumpe",
|
||||
"validVariableTypes": [1, 2]
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "HeizkurveVonWaermepumpeUebernehmen",
|
||||
"caption": "Wert von Waermepumpe uebernehmen",
|
||||
"onClick": "IPS_RequestAction($id, 'HeizkurveVonWaermepumpeUebernehmen', 0);"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Erweiterte Einstellungen",
|
||||
"items": [
|
||||
{"type": "NumberSpinner", "name": "TemperaturMaxAlter", "caption": "Maximales Temperaturalter", "suffix": " s", "minimum": 1},
|
||||
{"type": "CheckBox", "name": "PuffertemperaturGlaetten", "caption": "Puffertemperatur glaetten"},
|
||||
{"type": "NumberSpinner", "name": "ZeitKonstante", "caption": "PT1-Zeitkonstante", "suffix": " s", "minimum": 1},
|
||||
{"type": "CheckBox", "name": "EinstellungenInVisu", "caption": "Einstellungen in der Visualisierung anzeigen"},
|
||||
{"type": "CheckBox", "name": "DiagnosevariablenAnzeigen", "caption": "Diagnosevariablen anzeigen"},
|
||||
{"type": "CheckBox", "name": "LoggingEin", "caption": "Diagnoseprotokoll aktivieren"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{"code": 102, "icon": "active", "caption": "Aktiv"},
|
||||
{"code": 201, "icon": "error", "caption": "Temperaturmessung ungueltig"},
|
||||
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"},
|
||||
{"code": 203, "icon": "error", "caption": "Schaltfehler"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}",
|
||||
"name": "VerbraucherPufferspeicher",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Pufferspeicher"
|
||||
],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Pufferspeicher"
|
||||
}
|
||||
@@ -0,0 +1,900 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../libs/VerbraucherSchnittstelle.php';
|
||||
require_once __DIR__ . '/../libs/VerbraucherBasisTrait.php';
|
||||
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
|
||||
require_once __DIR__ . '/../libs/PufferspeicherRegler.php';
|
||||
|
||||
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||
use Belevo\EnelixEMS\PufferspeicherRegler;
|
||||
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
||||
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
|
||||
|
||||
class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnittstelle
|
||||
{
|
||||
use VerbraucherBasisTrait;
|
||||
|
||||
private const MANAGER_MODULE_ID = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
||||
private const STATUS_AKTIV = 102;
|
||||
private const STATUS_TEMPERATUR_UNGUELTIG = 201;
|
||||
private const STATUS_KONFIGURATION_UNGUELTIG = 202;
|
||||
private const STATUS_SCHALTFEHLER = 203;
|
||||
private const VM_UPDATE = 10603;
|
||||
|
||||
/** @var list<string> */
|
||||
private const DIAGNOSE_VARIABLEN = [
|
||||
'Istleistung',
|
||||
'Leistungsquelle',
|
||||
'Sollleistung',
|
||||
'SollwertGueltig',
|
||||
'Verfuegbar',
|
||||
'AenderungMoeglich',
|
||||
'Stoerung',
|
||||
'Stoertext',
|
||||
'AktiveStufe',
|
||||
'BezogeneEnergie',
|
||||
'TemperaturenGueltig',
|
||||
];
|
||||
|
||||
public function Create(): void
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->registriereVerbraucherBasis();
|
||||
$this->RegisterPropertyString('LeistungsStufen', '[]');
|
||||
$this->RegisterPropertyInteger('PufferfuehlerVariableID', 0);
|
||||
$this->RegisterPropertyInteger('AussentemperaturVariableID', 0);
|
||||
$this->RegisterPropertyInteger('WaermepumpenSolltemperaturVariableID', 0);
|
||||
$this->RegisterPropertyFloat('FusspunktVorlauftemperatur', 35.0);
|
||||
$this->RegisterPropertyFloat('HeizkurvenSteigung', 1.0);
|
||||
$this->RegisterPropertyFloat('HeizkurveMinimaltemperatur', 20.0);
|
||||
$this->RegisterPropertyFloat('HeizkurveMaximaltemperatur', 80.0);
|
||||
$this->RegisterPropertyFloat('Hysterese', 5.0);
|
||||
$this->RegisterPropertyInteger('MindesttemperaturModus', PufferspeicherRegler::MINIMUM_AUS);
|
||||
$this->RegisterPropertyFloat('Mindesttemperatur', 20.0);
|
||||
$this->RegisterPropertyFloat('MindesttemperaturDifferenz', 5.0);
|
||||
$this->RegisterPropertyInteger('LastwechselSperrzeit', 5);
|
||||
$this->RegisterPropertyInteger('TemperaturMaxAlter', 120);
|
||||
$this->RegisterPropertyBoolean('PuffertemperaturGlaetten', false);
|
||||
$this->RegisterPropertyInteger('ZeitKonstante', 120);
|
||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||
|
||||
$this->RegisterVariableFloat('Puffertemperatur', 'Puffertemperatur', '~Temperature', 100);
|
||||
$this->RegisterVariableFloat('Aussentemperatur', 'Aussentemperatur', '~Temperature', 110);
|
||||
$this->RegisterVariableFloat('Solltemperatur', 'Solltemperatur', '~Temperature', 120);
|
||||
$this->RegisterVariableBoolean('Heizbedarf', 'Heizbedarf', '~Switch', 130);
|
||||
|
||||
$this->RegisterAttributeInteger('RegistriertePufferID', 0);
|
||||
$this->RegisterAttributeInteger('RegistrierteAussenID', 0);
|
||||
$this->RegisterAttributeInteger('LetzteBerechnung', 0);
|
||||
$this->RegisterAttributeInteger('LetzteTemperaturberechnung', 0);
|
||||
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
||||
$this->RegisterAttributeInteger('LetzterLastwechsel', 0);
|
||||
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
||||
$this->RegisterAttributeFloat('Glaettungswert', 0.0);
|
||||
$this->RegisterAttributeBoolean('GlaettungInitialisiert', false);
|
||||
$this->RegisterAttributeString('Leistungsangebot', '[0]');
|
||||
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
|
||||
$this->RegisterAttributeString('Schaltfehler', '');
|
||||
|
||||
$this->RegisterAttributeFloat('ZustandIstleistung', 0.0);
|
||||
$this->RegisterAttributeInteger('ZustandSollleistung', 0);
|
||||
$this->RegisterAttributeBoolean('ZustandSollwertGueltig', false);
|
||||
$this->RegisterAttributeBoolean('ZustandVerfuegbar', false);
|
||||
$this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false);
|
||||
$this->RegisterAttributeBoolean('ZustandStoerung', false);
|
||||
$this->RegisterAttributeString('ZustandStoertext', '');
|
||||
$this->RegisterAttributeInteger('ZustandAktiveStufe', 0);
|
||||
$this->RegisterAttributeFloat('ZustandBezogeneEnergie', 0.0);
|
||||
$this->RegisterAttributeBoolean('ZustandTemperaturenGueltig', false);
|
||||
|
||||
$this->RegisterTimer(
|
||||
'LastwechselFreigabe',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'LastwechselFreigabe', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'Meldezyklus',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'RueckmeldungVerzoegert',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
||||
);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
|
||||
$this->aktualisiereVariablen();
|
||||
$this->bereinigeUngueltigenSollwert();
|
||||
try {
|
||||
$this->pruefeKonfiguration();
|
||||
} catch (Throwable $fehler) {
|
||||
$this->deaktiviereTimer();
|
||||
$this->setzeZustand('Verfuegbar', false);
|
||||
$this->setzeZustand('AenderungMoeglich', false);
|
||||
$this->setzeZustand('Stoerung', true);
|
||||
$this->setzeZustand('Stoertext', $fehler->getMessage());
|
||||
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->registriereTemperaturmeldungen();
|
||||
$this->SetTimerInterval('LastwechselFreigabe', 0);
|
||||
$this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000);
|
||||
$this->regelzyklus(true);
|
||||
}
|
||||
|
||||
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
|
||||
{
|
||||
if ((int) $nachricht !== self::VM_UPDATE) {
|
||||
return;
|
||||
}
|
||||
if (in_array((int) $senderID, [
|
||||
$this->ReadPropertyInteger('PufferfuehlerVariableID'),
|
||||
$this->ReadPropertyInteger('AussentemperaturVariableID'),
|
||||
], true)) {
|
||||
$this->regelzyklus(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function RequestAction($ident, $wert): void
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'Aktiv':
|
||||
$this->SetValue('Aktiv', (bool) $wert);
|
||||
if (!(bool) $wert) {
|
||||
$this->verwerfeSollwert();
|
||||
}
|
||||
$this->regelzyklus(true);
|
||||
return;
|
||||
|
||||
case 'HeizkurveVonWaermepumpeUebernehmen':
|
||||
$this->uebernehmeWaermepumpenwert();
|
||||
return;
|
||||
|
||||
case 'LastwechselFreigabe':
|
||||
$this->SetTimerInterval('LastwechselFreigabe', 0);
|
||||
$this->regelzyklus(true);
|
||||
return;
|
||||
|
||||
case 'Melden':
|
||||
if ((bool) $wert) {
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||
}
|
||||
$this->regelzyklus(false);
|
||||
$this->sendeVerbraucherdaten();
|
||||
return;
|
||||
|
||||
case 'ManagerdatenEmpfangen':
|
||||
if (!is_string($wert)) {
|
||||
throw new InvalidArgumentException('Managerdaten muessen als JSON uebergeben werden.');
|
||||
}
|
||||
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($daten)) {
|
||||
throw new InvalidArgumentException('Managerdaten muessen ein JSON-Objekt sein.');
|
||||
}
|
||||
$this->ManagerdatenEmpfangen($daten);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $daten */
|
||||
public function ManagerdatenEmpfangen(array $daten): void
|
||||
{
|
||||
Nachrichtenvertrag::pruefeManagerdaten($daten);
|
||||
if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) {
|
||||
throw new InvalidArgumentException('Managerdaten sind an eine andere Instanz adressiert.');
|
||||
}
|
||||
|
||||
$managerID = $daten['Kopf']['AbsenderID'];
|
||||
if (!in_array($managerID, $this->zugeordneteManagerIDs(), true)) {
|
||||
throw new InvalidArgumentException('Der Manager hat diesen Pufferspeicher nicht aktiv zugeordnet.');
|
||||
}
|
||||
|
||||
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||
$this->regelzyklus(false);
|
||||
$angebot = $this->leseLeistungsangebot();
|
||||
$sollleistung = $daten['Sollleistung_W'];
|
||||
if ($sollleistung === null) {
|
||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||
&& !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true)
|
||||
) {
|
||||
$this->verwerfeSollwert();
|
||||
$this->regelzyklus(false);
|
||||
}
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||
return;
|
||||
}
|
||||
if (!in_array($sollleistung, $angebot, true)) {
|
||||
throw new InvalidArgumentException('Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.');
|
||||
}
|
||||
|
||||
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
|
||||
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
|
||||
$this->setzeZustand('Sollleistung', $sollleistung);
|
||||
$this->setzeZustand('SollwertGueltig', true);
|
||||
$this->regelzyklus(false);
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||
}
|
||||
|
||||
|
||||
private function regelzyklus(bool $meldungPlanen): void
|
||||
{
|
||||
try {
|
||||
$jetzt = time();
|
||||
$this->aktualisiereEnergie($jetzt);
|
||||
$temperaturenGueltig = $this->aktualisiereTemperaturen();
|
||||
$stufen = PufferspeicherRegler::dekodiereLeistungsstufen(
|
||||
$this->ReadPropertyString('LeistungsStufen')
|
||||
);
|
||||
|
||||
$solltemperatur = 0.0;
|
||||
$einschaltschwelle = 0.0;
|
||||
if ($temperaturenGueltig) {
|
||||
$solltemperatur = PufferspeicherRegler::solltemperatur(
|
||||
(float) $this->GetValue('Aussentemperatur'),
|
||||
$this->ReadPropertyFloat('FusspunktVorlauftemperatur'),
|
||||
$this->ReadPropertyFloat('HeizkurvenSteigung'),
|
||||
$this->ReadPropertyFloat('HeizkurveMinimaltemperatur'),
|
||||
$this->ReadPropertyFloat('HeizkurveMaximaltemperatur')
|
||||
);
|
||||
$einschaltschwelle = PufferspeicherRegler::einschaltschwelle(
|
||||
$solltemperatur,
|
||||
$this->ReadPropertyFloat('Hysterese'),
|
||||
$this->ReadPropertyInteger('MindesttemperaturModus'),
|
||||
$this->ReadPropertyFloat('Mindesttemperatur'),
|
||||
$this->ReadPropertyFloat('MindesttemperaturDifferenz')
|
||||
);
|
||||
}
|
||||
$this->SetValue('Solltemperatur', $solltemperatur);
|
||||
|
||||
$aktiv = (bool) $this->GetValue('Aktiv');
|
||||
$peak = $this->ReadAttributeString('Betriebsart') === Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||
$angebot = PufferspeicherRegler::leistungsangebot(
|
||||
$aktiv,
|
||||
$peak,
|
||||
$temperaturenGueltig,
|
||||
(float) $this->GetValue('Puffertemperatur'),
|
||||
$einschaltschwelle,
|
||||
$stufen
|
||||
);
|
||||
$heizbedarf = count($angebot) > 1;
|
||||
$this->SetValue('Heizbedarf', $heizbedarf);
|
||||
|
||||
$verfuegbar = $aktiv && $temperaturenGueltig && $stufen !== [];
|
||||
$aenderungMoeglich = $verfuegbar && count($angebot) > 1;
|
||||
$zwangsleistung = (!$verfuegbar || !$heizbedarf) ? 0 : null;
|
||||
$aktuelleLeistung = (int) round((float) $this->leseZustand('Istleistung'));
|
||||
$restzeit = PufferspeicherRegler::restzeitBisLastwechsel(
|
||||
$this->ReadAttributeInteger('LetzterLastwechsel'),
|
||||
$jetzt,
|
||||
$this->ReadPropertyInteger('LastwechselSperrzeit')
|
||||
);
|
||||
$lastwechselGesperrt = $restzeit > 0 && $verfuegbar && $zwangsleistung !== 0;
|
||||
$this->SetTimerInterval('LastwechselFreigabe', $lastwechselGesperrt ? $restzeit * 1000 : 0);
|
||||
if ($lastwechselGesperrt) {
|
||||
$angebot = [$aktuelleLeistung];
|
||||
$aenderungMoeglich = false;
|
||||
}
|
||||
|
||||
$letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit');
|
||||
if ($letzteVorgabe <= 0
|
||||
|| $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout')
|
||||
) {
|
||||
$this->verwerfeSollwert();
|
||||
}
|
||||
|
||||
$ziel = 0;
|
||||
if ($lastwechselGesperrt) {
|
||||
$ziel = $aktuelleLeistung;
|
||||
} elseif ($zwangsleistung !== null) {
|
||||
$ziel = $zwangsleistung;
|
||||
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
||||
$ziel = (int) $this->leseZustand('Sollleistung');
|
||||
if (!in_array($ziel, $angebot, true)) {
|
||||
$this->verwerfeSollwert();
|
||||
$ziel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$lastwechsel = $this->schalteLeistung($ziel, $jetzt);
|
||||
if ($lastwechsel) {
|
||||
$angebot = [$ziel];
|
||||
$aenderungMoeglich = false;
|
||||
}
|
||||
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
||||
$this->setzeZustand('Sollleistung', $ziel);
|
||||
$this->setzeZustand('Verfuegbar', $verfuegbar);
|
||||
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
|
||||
$this->aktualisiereStoerungen($temperaturenGueltig);
|
||||
if ($meldungPlanen) {
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
$this->WriteAttributeString('Schaltfehler', $fehler->getMessage());
|
||||
$this->setzeZustand('Verfuegbar', false);
|
||||
$this->setzeZustand('AenderungMoeglich', false);
|
||||
$this->setzeZustand('Stoerung', true);
|
||||
$this->setzeZustand('Stoertext', $fehler->getMessage());
|
||||
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||
$this->protokolliere('Regelzyklus', $fehler->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function aktualisiereTemperaturen(): bool
|
||||
{
|
||||
$pufferID = $this->ReadPropertyInteger('PufferfuehlerVariableID');
|
||||
$aussenID = $this->ReadPropertyInteger('AussentemperaturVariableID');
|
||||
if (!$this->temperaturwertGueltig($pufferID) || !$this->temperaturwertGueltig($aussenID)) {
|
||||
$this->setzeZustand('TemperaturenGueltig', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
$puffertemperatur = (float) GetValue($pufferID);
|
||||
$aussentemperatur = (float) GetValue($aussenID);
|
||||
$jetzt = time();
|
||||
$letzteBerechnung = $this->ReadAttributeInteger('LetzteTemperaturberechnung');
|
||||
$this->WriteAttributeInteger('LetzteTemperaturberechnung', $jetzt);
|
||||
if ($this->ReadPropertyBoolean('PuffertemperaturGlaetten')) {
|
||||
if (!$this->ReadAttributeBoolean('GlaettungInitialisiert')) {
|
||||
$this->WriteAttributeFloat('Glaettungswert', $puffertemperatur);
|
||||
$this->WriteAttributeBoolean('GlaettungInitialisiert', true);
|
||||
} elseif ($letzteBerechnung > 0 && $jetzt > $letzteBerechnung) {
|
||||
$puffertemperatur = PufferspeicherRegler::pt1(
|
||||
$this->ReadAttributeFloat('Glaettungswert'),
|
||||
$puffertemperatur,
|
||||
$jetzt - $letzteBerechnung,
|
||||
$this->ReadPropertyInteger('ZeitKonstante')
|
||||
);
|
||||
$this->WriteAttributeFloat('Glaettungswert', $puffertemperatur);
|
||||
} else {
|
||||
$puffertemperatur = $this->ReadAttributeFloat('Glaettungswert');
|
||||
}
|
||||
} else {
|
||||
$this->WriteAttributeBoolean('GlaettungInitialisiert', false);
|
||||
}
|
||||
|
||||
$this->SetValue('Puffertemperatur', $puffertemperatur);
|
||||
$this->SetValue('Aussentemperatur', $aussentemperatur);
|
||||
$this->setzeZustand('TemperaturenGueltig', true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function temperaturwertGueltig(int $variablenID): bool
|
||||
{
|
||||
if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) {
|
||||
return false;
|
||||
}
|
||||
$variable = IPS_GetVariable($variablenID);
|
||||
if (!in_array((int) $variable['VariableType'], [1, 2], true)) {
|
||||
return false;
|
||||
}
|
||||
if (time() - (int) $variable['VariableUpdated'] > $this->ReadPropertyInteger('TemperaturMaxAlter')) {
|
||||
return false;
|
||||
}
|
||||
$wert = GetValue($variablenID);
|
||||
|
||||
return is_int($wert) || is_float($wert);
|
||||
}
|
||||
|
||||
private function uebernehmeWaermepumpenwert(): void
|
||||
{
|
||||
$wpID = $this->ReadPropertyInteger('WaermepumpenSolltemperaturVariableID');
|
||||
$aussenID = $this->ReadPropertyInteger('AussentemperaturVariableID');
|
||||
if (!$this->temperaturwertGueltig($wpID) || !$this->temperaturwertGueltig($aussenID)) {
|
||||
throw new RuntimeException('Waermepumpen-Solltemperatur oder Aussentemperatur ist ungueltig.');
|
||||
}
|
||||
$fusspunkt = PufferspeicherRegler::fusspunktAusWaermepumpenwert(
|
||||
(float) GetValue($wpID),
|
||||
(float) GetValue($aussenID),
|
||||
$this->ReadPropertyFloat('HeizkurvenSteigung')
|
||||
);
|
||||
IPS_SetProperty($this->InstanceID, 'FusspunktVorlauftemperatur', $fusspunkt);
|
||||
IPS_ApplyChanges($this->InstanceID);
|
||||
$this->protokolliere('Heizkurve', ['FusspunktVorlauftemperatur' => $fusspunkt]);
|
||||
}
|
||||
|
||||
private function schalteLeistung(int $zielLeistung, int $jetzt): bool
|
||||
{
|
||||
$stufen = PufferspeicherRegler::dekodiereLeistungsstufen(
|
||||
$this->ReadPropertyString('LeistungsStufen')
|
||||
);
|
||||
$zielStufe = PufferspeicherRegler::stufeFuerLeistung($stufen, $zielLeistung);
|
||||
$bisher = (int) round((float) $this->leseZustand('Istleistung'));
|
||||
if ($zielLeistung === $bisher && $this->kontakteEntsprechenZiel($stufen, $zielLeistung)) {
|
||||
$this->WriteAttributeString('Schaltfehler', '');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($stufen as $stufe) {
|
||||
RequestAction($stufe['KontaktID'], false);
|
||||
}
|
||||
foreach ($stufen as $stufe) {
|
||||
if ($stufe['Leistung_W'] === $zielLeistung && $zielLeistung > 0) {
|
||||
RequestAction($stufe['KontaktID'], true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
foreach ($stufen as $stufe) {
|
||||
try {
|
||||
RequestAction($stufe['KontaktID'], false);
|
||||
} catch (Throwable $ignoriert) {
|
||||
}
|
||||
}
|
||||
$this->setzeZustand('Istleistung', 0.0);
|
||||
$this->setzeZustand('AktiveStufe', 0);
|
||||
throw new RuntimeException('Leistungsstufe konnte nicht sicher geschaltet werden: ' . $fehler->getMessage());
|
||||
}
|
||||
|
||||
$this->WriteAttributeString('Schaltfehler', '');
|
||||
$this->setzeZustand('Istleistung', (float) $zielLeistung);
|
||||
$this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET);
|
||||
$this->setzeZustand('AktiveStufe', $zielStufe);
|
||||
$this->WriteAttributeInteger('LetzterLastwechsel', $jetzt);
|
||||
$this->protokolliere('Leistungsstufe', ['Leistung_W' => $zielLeistung, 'Stufe' => $zielStufe]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen */
|
||||
private function kontakteEntsprechenZiel(array $stufen, int $zielLeistung): bool
|
||||
{
|
||||
foreach ($stufen as $stufe) {
|
||||
if (!IPS_VariableExists($stufe['KontaktID'])) {
|
||||
return false;
|
||||
}
|
||||
$sollEin = $zielLeistung > 0 && $stufe['Leistung_W'] === $zielLeistung;
|
||||
if ((bool) GetValue($stufe['KontaktID']) !== $sollEin) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function aktualisiereEnergie(int $jetzt): void
|
||||
{
|
||||
$letzteBerechnung = $this->ReadAttributeInteger('LetzteBerechnung');
|
||||
$this->WriteAttributeInteger('LetzteBerechnung', $jetzt);
|
||||
if ($letzteBerechnung <= 0 || $jetzt <= $letzteBerechnung) {
|
||||
return;
|
||||
}
|
||||
$zuwachs = PufferspeicherRegler::energieKWh(
|
||||
(float) $this->leseZustand('Istleistung'),
|
||||
$jetzt - $letzteBerechnung
|
||||
);
|
||||
$this->setzeZustand(
|
||||
'BezogeneEnergie',
|
||||
(float) $this->leseZustand('BezogeneEnergie') + $zuwachs
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function aktualisiereStoerungen(bool $temperaturenGueltig): void
|
||||
{
|
||||
$stoerungen = [];
|
||||
if (!$temperaturenGueltig) {
|
||||
$stoerungen[] = 'Puffer- oder Aussentemperatur fehlt oder ist veraltet.';
|
||||
}
|
||||
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||
$stoerungen[] = $this->ReadAttributeString('Schaltfehler');
|
||||
}
|
||||
$this->setzeZustand('Stoerung', $stoerungen !== []);
|
||||
$this->setzeZustand('Stoertext', implode("
|
||||
", array_values(array_unique($stoerungen))));
|
||||
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||
} elseif (!$temperaturenGueltig) {
|
||||
$this->SetStatus(self::STATUS_TEMPERATUR_UNGUELTIG);
|
||||
} else {
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendeVerbraucherdaten(): void
|
||||
{
|
||||
foreach ($this->zugeordneteManagerIDs() as $managerID) {
|
||||
$daten = $this->baueVerbraucherdaten($managerID);
|
||||
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
|
||||
try {
|
||||
IPS_RequestAction(
|
||||
$managerID,
|
||||
'VerbraucherdatenEmpfangen',
|
||||
json_encode($daten, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
} catch (Throwable $fehler) {
|
||||
$this->protokolliere('Managerkommunikation', $fehler->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function baueVerbraucherdaten(int $managerID): array
|
||||
{
|
||||
$temperaturenGueltig = (bool) $this->leseZustand('TemperaturenGueltig');
|
||||
$schaltfehler = $this->ReadAttributeString('Schaltfehler');
|
||||
|
||||
return [
|
||||
'Kopf' => [
|
||||
'Version' => Nachrichtenvertrag::VERSION,
|
||||
'AbsenderID' => $this->InstanceID,
|
||||
'EmpfaengerID' => $managerID,
|
||||
'Zeitpunkt' => time(),
|
||||
],
|
||||
'Betriebsart' => $this->ReadAttributeString('Betriebsart'),
|
||||
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
||||
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
||||
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
||||
'AenderungMoeglich' => (bool) $this->leseZustand('AenderungMoeglich'),
|
||||
'Verfuegbar' => (bool) $this->leseZustand('Verfuegbar'),
|
||||
'Istleistung_W' => (float) $this->leseZustand('Istleistung'),
|
||||
'Leistungsquelle' => Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET,
|
||||
'Zustand' => [
|
||||
[
|
||||
'Kennung' => 'Sollleistung_W',
|
||||
'Art' => 'Sollwert',
|
||||
'Wert' => (bool) $this->leseZustand('SollwertGueltig')
|
||||
? (int) $this->leseZustand('Sollleistung')
|
||||
: null,
|
||||
'Einheit' => 'W',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Puffertemperatur_C',
|
||||
'Art' => 'Istwert',
|
||||
'Wert' => $temperaturenGueltig ? (float) $this->GetValue('Puffertemperatur') : null,
|
||||
'Einheit' => 'C',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Aussentemperatur_C',
|
||||
'Art' => 'Istwert',
|
||||
'Wert' => $temperaturenGueltig ? (float) $this->GetValue('Aussentemperatur') : null,
|
||||
'Einheit' => 'C',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Solltemperatur_C',
|
||||
'Art' => 'Sollwert',
|
||||
'Wert' => $temperaturenGueltig ? (float) $this->GetValue('Solltemperatur') : null,
|
||||
'Einheit' => 'C',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Heizbedarf',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (bool) $this->GetValue('Heizbedarf'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'AktiveStufe',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (int) $this->leseZustand('AktiveStufe'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Fuehlerfehler',
|
||||
'Art' => 'Stoerung',
|
||||
'Wert' => !$temperaturenGueltig,
|
||||
'Einheit' => '',
|
||||
'Text' => $temperaturenGueltig
|
||||
? ''
|
||||
: 'Puffer- oder Aussentemperatur fehlt oder ist veraltet.',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Schaltfehler',
|
||||
'Art' => 'Stoerung',
|
||||
'Wert' => $schaltfehler !== '',
|
||||
'Einheit' => '',
|
||||
'Text' => $schaltfehler,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function zugeordneteManagerIDs(): array
|
||||
{
|
||||
$ergebnis = [];
|
||||
foreach (IPS_GetInstanceListByModuleID(self::MANAGER_MODULE_ID) as $managerID) {
|
||||
try {
|
||||
$automatisch = (bool) IPS_GetProperty($managerID, 'AutomatischeSuche');
|
||||
$property = $automatisch
|
||||
? 'AutomatischeVerbraucherZuordnung'
|
||||
: 'VerbraucherZuordnung';
|
||||
$zuordnung = $this->leseManagerZuordnung($managerID, $property);
|
||||
if ($automatisch && $zuordnung === []) {
|
||||
$zuordnung = $this->leseManagerZuordnung($managerID, 'VerbraucherZuordnung');
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($zuordnung as $eintrag) {
|
||||
if (!is_array($eintrag) || ($eintrag['Aktiv'] ?? false) !== true) {
|
||||
continue;
|
||||
}
|
||||
$instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null;
|
||||
if ($instanzID === $this->InstanceID) {
|
||||
$ergebnis[] = (int) $managerID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($ergebnis));
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
private function leseManagerZuordnung(int $managerID, string $property): array
|
||||
{
|
||||
$zuordnung = json_decode(
|
||||
(string) IPS_GetProperty($managerID, $property),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
return is_array($zuordnung) ? $zuordnung : [];
|
||||
}
|
||||
|
||||
private function pruefeKonfiguration(): void
|
||||
{
|
||||
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) < 0) {
|
||||
throw new InvalidArgumentException($property . ' muss mindestens 0 sein.');
|
||||
}
|
||||
}
|
||||
foreach ([
|
||||
'Meldeintervall',
|
||||
'VorgabeTimeout',
|
||||
'TemperaturMaxAlter',
|
||||
'ZeitKonstante',
|
||||
] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||
}
|
||||
}
|
||||
if ($this->ReadPropertyInteger('LastwechselSperrzeit') < 0) {
|
||||
throw new InvalidArgumentException('LastwechselSperrzeit darf nicht negativ sein.');
|
||||
}
|
||||
if ($this->ReadPropertyFloat('Hysterese') <= 0.0) {
|
||||
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
|
||||
}
|
||||
|
||||
PufferspeicherRegler::solltemperatur(
|
||||
20.0,
|
||||
$this->ReadPropertyFloat('FusspunktVorlauftemperatur'),
|
||||
$this->ReadPropertyFloat('HeizkurvenSteigung'),
|
||||
$this->ReadPropertyFloat('HeizkurveMinimaltemperatur'),
|
||||
$this->ReadPropertyFloat('HeizkurveMaximaltemperatur')
|
||||
);
|
||||
PufferspeicherRegler::einschaltschwelle(
|
||||
50.0,
|
||||
$this->ReadPropertyFloat('Hysterese'),
|
||||
$this->ReadPropertyInteger('MindesttemperaturModus'),
|
||||
$this->ReadPropertyFloat('Mindesttemperatur'),
|
||||
$this->ReadPropertyFloat('MindesttemperaturDifferenz')
|
||||
);
|
||||
|
||||
foreach (['PufferfuehlerVariableID', 'AussentemperaturVariableID'] as $property) {
|
||||
$variablenID = $this->ReadPropertyInteger($property);
|
||||
if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) {
|
||||
throw new InvalidArgumentException($property . ' ist nicht eingerichtet.');
|
||||
}
|
||||
$variable = IPS_GetVariable($variablenID);
|
||||
if (!in_array((int) $variable['VariableType'], [1, 2], true)) {
|
||||
throw new InvalidArgumentException($property . ' muss eine Integer- oder Floatvariable sein.');
|
||||
}
|
||||
}
|
||||
|
||||
$wpID = $this->ReadPropertyInteger('WaermepumpenSolltemperaturVariableID');
|
||||
if ($wpID > 0) {
|
||||
if (!IPS_VariableExists($wpID)) {
|
||||
throw new InvalidArgumentException('WaermepumpenSolltemperaturVariableID existiert nicht.');
|
||||
}
|
||||
$wpVariable = IPS_GetVariable($wpID);
|
||||
if (!in_array((int) $wpVariable['VariableType'], [1, 2], true)) {
|
||||
throw new InvalidArgumentException(
|
||||
'WaermepumpenSolltemperaturVariableID muss eine Integer- oder Floatvariable sein.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$stufen = PufferspeicherRegler::dekodiereLeistungsstufen(
|
||||
$this->ReadPropertyString('LeistungsStufen')
|
||||
);
|
||||
if ($stufen === []) {
|
||||
throw new InvalidArgumentException('Mindestens eine Leistungsstufe ist erforderlich.');
|
||||
}
|
||||
foreach ($stufen as $stufe) {
|
||||
if (!IPS_VariableExists($stufe['KontaktID'])) {
|
||||
throw new InvalidArgumentException('Schaltkontakt ' . $stufe['KontaktID'] . ' existiert nicht.');
|
||||
}
|
||||
$kontakt = IPS_GetVariable($stufe['KontaktID']);
|
||||
if ((int) $kontakt['VariableType'] !== 0) {
|
||||
throw new InvalidArgumentException('Alle Schaltkontakte muessen Booleanvariablen sein.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function registriereTemperaturmeldungen(): void
|
||||
{
|
||||
$zuordnungen = [
|
||||
'RegistriertePufferID' => $this->ReadPropertyInteger('PufferfuehlerVariableID'),
|
||||
'RegistrierteAussenID' => $this->ReadPropertyInteger('AussentemperaturVariableID'),
|
||||
];
|
||||
foreach ($zuordnungen as $attribut => $neueID) {
|
||||
$alteID = $this->ReadAttributeInteger($attribut);
|
||||
if ($alteID > 0 && $alteID !== $neueID) {
|
||||
$this->UnregisterMessage($alteID, self::VM_UPDATE);
|
||||
}
|
||||
if ($neueID > 0 && $neueID !== $alteID) {
|
||||
$this->RegisterMessage($neueID, self::VM_UPDATE);
|
||||
}
|
||||
$this->WriteAttributeInteger($attribut, $neueID);
|
||||
}
|
||||
}
|
||||
|
||||
private function aktualisiereVariablen(): void
|
||||
{
|
||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||
if ($ident === 'Leistungsquelle') {
|
||||
continue;
|
||||
}
|
||||
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||
$this->setzeZustand($ident, GetValue($variablenID));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||
$this->registriereVerbraucherDiagnose();
|
||||
$this->RegisterVariableInteger('AktiveStufe', 'Aktive Stufe', '', 200);
|
||||
$this->RegisterVariableFloat('BezogeneEnergie', 'Bezogene Energie', '~Electricity', 210);
|
||||
$this->RegisterVariableBoolean(
|
||||
'TemperaturenGueltig',
|
||||
'Temperaturen gueltig',
|
||||
'~Switch',
|
||||
220
|
||||
);
|
||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||
$wert = $ident === 'Leistungsquelle'
|
||||
? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET
|
||||
: $this->leseZustand($ident);
|
||||
$this->setzeZustand($ident, $wert);
|
||||
}
|
||||
} else {
|
||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||
$this->UnregisterVariable($ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param bool|int|float|string $wert */
|
||||
private function setzeZustand(string $ident, $wert): void
|
||||
{
|
||||
$booleanAttribute = [
|
||||
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
||||
'Verfuegbar' => 'ZustandVerfuegbar',
|
||||
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
||||
'Stoerung' => 'ZustandStoerung',
|
||||
'TemperaturenGueltig' => 'ZustandTemperaturenGueltig',
|
||||
];
|
||||
$integerAttribute = [
|
||||
'Sollleistung' => 'ZustandSollleistung',
|
||||
'AktiveStufe' => 'ZustandAktiveStufe',
|
||||
];
|
||||
$floatAttribute = [
|
||||
'Istleistung' => 'ZustandIstleistung',
|
||||
'BezogeneEnergie' => 'ZustandBezogeneEnergie',
|
||||
];
|
||||
$stringAttribute = ['Stoertext' => 'ZustandStoertext'];
|
||||
|
||||
if (isset($booleanAttribute[$ident])) {
|
||||
$wert = (bool) $wert;
|
||||
$this->WriteAttributeBoolean($booleanAttribute[$ident], $wert);
|
||||
} elseif (isset($integerAttribute[$ident])) {
|
||||
$wert = (int) $wert;
|
||||
$this->WriteAttributeInteger($integerAttribute[$ident], $wert);
|
||||
} elseif (isset($floatAttribute[$ident])) {
|
||||
$wert = (float) $wert;
|
||||
$this->WriteAttributeFloat($floatAttribute[$ident], $wert);
|
||||
} elseif (isset($stringAttribute[$ident])) {
|
||||
$wert = (string) $wert;
|
||||
$this->WriteAttributeString($stringAttribute[$ident], $wert);
|
||||
} elseif ($ident !== 'Leistungsquelle') {
|
||||
throw new LogicException('Unbekannter interner Zustand: ' . $ident);
|
||||
}
|
||||
|
||||
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||
SetValue($variablenID, $wert);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return bool|int|float|string */
|
||||
private function leseZustand(string $ident)
|
||||
{
|
||||
$booleanAttribute = [
|
||||
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
||||
'Verfuegbar' => 'ZustandVerfuegbar',
|
||||
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
||||
'Stoerung' => 'ZustandStoerung',
|
||||
'TemperaturenGueltig' => 'ZustandTemperaturenGueltig',
|
||||
];
|
||||
$integerAttribute = [
|
||||
'Sollleistung' => 'ZustandSollleistung',
|
||||
'AktiveStufe' => 'ZustandAktiveStufe',
|
||||
];
|
||||
$floatAttribute = [
|
||||
'Istleistung' => 'ZustandIstleistung',
|
||||
'BezogeneEnergie' => 'ZustandBezogeneEnergie',
|
||||
];
|
||||
$stringAttribute = ['Stoertext' => 'ZustandStoertext'];
|
||||
|
||||
if (isset($booleanAttribute[$ident])) {
|
||||
return $this->ReadAttributeBoolean($booleanAttribute[$ident]);
|
||||
}
|
||||
if (isset($integerAttribute[$ident])) {
|
||||
return $this->ReadAttributeInteger($integerAttribute[$ident]);
|
||||
}
|
||||
if (isset($floatAttribute[$ident])) {
|
||||
return $this->ReadAttributeFloat($floatAttribute[$ident]);
|
||||
}
|
||||
if (isset($stringAttribute[$ident])) {
|
||||
return $this->ReadAttributeString($stringAttribute[$ident]);
|
||||
}
|
||||
|
||||
throw new LogicException('Unbekannter interner Zustand: ' . $ident);
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function leseLeistungsangebot(): array
|
||||
{
|
||||
$angebot = json_decode(
|
||||
$this->ReadAttributeString('Leistungsangebot'),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
if (!is_array($angebot)) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
return array_values(array_map('intval', $angebot));
|
||||
}
|
||||
|
||||
private function deaktiviereTimer(): void
|
||||
{
|
||||
foreach (['LastwechselFreigabe', 'Meldezyklus', 'RueckmeldungVerzoegert'] as $timer) {
|
||||
$this->SetTimerInterval($timer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $daten */
|
||||
private function protokolliere(string $bezeichnung, $daten): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('LoggingEin')) {
|
||||
return;
|
||||
}
|
||||
$this->SendDebug(
|
||||
$bezeichnung,
|
||||
is_string($daten) ? $daten : json_encode($daten, JSON_THROW_ON_ERROR),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,69 @@ Energiemanagement, Manager und steuerbare Verbraucher fuer IP-Symcon.
|
||||
|
||||
## Status
|
||||
|
||||
Das Repository befindet sich im Aufbau. Aktuell ist ausschliesslich der gemeinsame Nachrichtenvertrag implementiert. Noch nicht implementierte Module werden nicht als leere Platzhalter angelegt.
|
||||
Version 0.1 Build 1 ist die erste Beta fuer kontrollierte Anlagen- und
|
||||
Feldtests. Der gemeinsame Nachrichtenvertrag, die Verbraucherbasis, der Manager
|
||||
und alle unten aufgefuehrten Module sind implementiert. Produktiver Einsatz
|
||||
setzt eine anlagenspezifische Pruefung, Sicherung und Rueckfallplanung voraus.
|
||||
|
||||
## Geplante Module
|
||||
## Module
|
||||
|
||||
- Manager
|
||||
- Batterie
|
||||
- Wassererwaermer
|
||||
- Pufferspeicher
|
||||
- Verbraucher 1-Stufig
|
||||
- Waermepumpe
|
||||
- Ladestation Stand-Alone
|
||||
- Ladestation Gateway
|
||||
- Easee Gateway
|
||||
- Manager (implementiert)
|
||||
- Batterie (implementiert)
|
||||
- Wassererwaermer (implementiert)
|
||||
- Pufferspeicher (implementiert)
|
||||
- Verbraucher 1-Stufig (implementiert)
|
||||
- Waermepumpe (implementiert)
|
||||
- Ladestation Stand-Alone (implementiert)
|
||||
- Ladestation Gateway (implementiert)
|
||||
- Easee Gateway (implementiert)
|
||||
|
||||
Die Verbraucher kommunizieren mit dem Manager ueber den gemeinsamen Vertrag in `libs/`. Der Manager waehlt die Verbraucher aus; in einem Verbraucher gibt es keine Manager-ID-Property.
|
||||
Die Properties, Variablen, Zustandsdaten und offenen Punkte stehen in der
|
||||
[Modulübersicht](docs/module/README.md). Gemeinsame Felder werden dort nicht
|
||||
abweichend neu definiert, sondern verweisen auf den zentralen Vertrag.
|
||||
|
||||
Die Verbraucher kommunizieren mit dem Manager ueber den gemeinsamen Vertrag
|
||||
in `libs/`. Der Manager waehlt die Verbraucher aus; in einem Verbraucher gibt
|
||||
es keine Manager-ID-Property.
|
||||
|
||||
## Betriebsartabhaengige Leistungsangebote
|
||||
|
||||
Der Nachrichtenvertrag `4.0` uebermittelt in beiden Richtungen verpflichtend
|
||||
die aktuelle `Betriebsart`. Zulaessige Werte sind `PV` fuer Solarbetrieb und
|
||||
`Peak` fuer die Lastspitzenbegrenzung. Dadurch kann jeder Verbraucher fuer
|
||||
beide Betriebsarten unterschiedliche PowerSteps melden.
|
||||
|
||||
Ein Betriebsartwechsel wird synchronisiert:
|
||||
|
||||
1. Der Manager bestimmt die neue Betriebsart.
|
||||
2. Er sendet sie mit `Sollleistung_W: null` an alle aktiven Verbraucher.
|
||||
3. `null` ist nur eine Ankuendigung und kein Schaltbefehl.
|
||||
4. Jeder Verbraucher berechnet sein zustandsabhaengiges Leistungsangebot neu
|
||||
und meldet es mit derselben Betriebsart zurueck.
|
||||
5. Der Manager verteilt konkrete Sollleistungen an alle bereits
|
||||
synchronisierten Verbraucher.
|
||||
6. Fehlende, veraltete oder noch nicht umgeschaltete Verbraucher bleiben von
|
||||
der Verteilung ausgeschlossen und werden als Stoerung ausgewiesen.
|
||||
|
||||
Der Manager verwendet damit nie ein PV-Angebot fuer Peak oder umgekehrt. Ein
|
||||
nicht antwortender Verbraucher blockiert die Regelung der uebrigen Anlage
|
||||
nicht; sein aktueller Einfluss ist bereits in der Netzleistungsmessung
|
||||
enthalten. Eine technische Schaltsperre oder eine Mindestzeit kann das Angebot
|
||||
weiterhin auf die aktuell gehaltene Leistung begrenzen.
|
||||
|
||||
| Verbraucher | PV-Angebot | Peak-Angebot |
|
||||
| --- | --- | --- |
|
||||
| Verbraucher 1-Stufig | Normal `[0, Nennleistung]`; bei faelliger Tagesmindestlaufzeit `[Nennleistung]` | Normal `[0]`; bei faelliger Tagesmindestlaufzeit konfigurierbar `[0, Nennleistung]` oder `[Nennleistung]` |
|
||||
| Ladestation Stand-Alone | Mit Solarladen `[0, ...Ladestufen]`, sonst maximale Ladeleistung | Mit Solarladen `[0]`, sonst `[0, ...Ladestufen]` |
|
||||
| Ladestation Gateway | Mit Solarladen `[0, ...Ladestufen]`, sonst maximale Ladeleistung | Mit Solarladen `[0]`, sonst `[0, ...Ladestufen]` |
|
||||
| Warmwassererwaermer | Unter Mindesttemperatur maximale Stufe, sonst zustandsabhaengig | Unter wirksamer Mindesttemperatur `[0, ...Leistungsstufen]`, sonst `[0]` |
|
||||
| Pufferspeicher | Unter Einschaltschwelle `[0, ...Leistungsstufen]`, sonst `[0]` | Unabhaengig vom Zustand `[0]` |
|
||||
| Batterie | SoC-abhaengiges Lade-/Entladeraster | SoC-, Reserve- und netzabhaengiges Angebot |
|
||||
|
||||
Die vollstaendigen Nachrichtenfelder und JSON-Beispiele stehen in der
|
||||
[Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md). Hintergrund,
|
||||
Alternativen und Folgen beschreibt
|
||||
[ADR 0004](docs/adr/0004-betriebsartabhaengige-leistungsangebote.md).
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
@@ -32,7 +80,18 @@ composer install
|
||||
composer check
|
||||
```
|
||||
|
||||
Neue oder geaenderte Funktionen werden zusammen mit ihren PHPUnit-Tests eingecheckt. Die CI-Pruefung muss vor der Uebernahme in einen Freigabebranch erfolgreich sein.
|
||||
Der Gesamttest schliesst alle Modulsuiten ein. Der einstufige Verbraucher kann
|
||||
zusaetzlich unabhaengig ausgefuehrt werden:
|
||||
|
||||
```bash
|
||||
composer check:verbraucher-einstufig
|
||||
```
|
||||
|
||||
Neue oder geaenderte Funktionen werden zusammen mit ihren PHPUnit-Tests eingecheckt.
|
||||
Jedes Modul besitzt zusätzlich einen standardisierten Laufzeittest für IP-Symcon
|
||||
8.x. Einzel-, Änderungs- und Gesamtläufe sowie Cleanup und Berichte sind im
|
||||
[Testleitfaden](docs/testing/README.md) beschrieben. Die CI-Pruefung muss vor
|
||||
der Uebernahme in einen Freigabebranch erfolgreich sein.
|
||||
|
||||
| Git-Branch | IP-Symcon-Kanal |
|
||||
| --- | --- |
|
||||
@@ -40,5 +99,25 @@ Neue oder geaenderte Funktionen werden zusammen mit ihren PHPUnit-Tests eingeche
|
||||
| `beta` | Beta |
|
||||
| `develop` | Testing |
|
||||
|
||||
Die ausfuehrliche Schnittstellenbeschreibung steht unter [docs/Schnittstelle.md](docs/Schnittstelle.md).
|
||||
## Dokumentation
|
||||
|
||||
- [Changelog](CHANGELOG.md)
|
||||
- [Zentrale Liste offener Teamentscheidungen](docs/Offene-Punkte.md)
|
||||
- [Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md)
|
||||
- [Batterieschnittstelle](docs/Schnittstelle-Batterie.md)
|
||||
- [ADR 0004: Betriebsartabhaengige Leistungsangebote](docs/adr/0004-betriebsartabhaengige-leistungsangebote.md)
|
||||
- [ADR 0005: Anlagentopologie fuer Prognosen](docs/adr/0005-anlagentopologie-fuer-prognosen.md)
|
||||
- [Manager-Modul inklusive Lizenzierung](docs/module/Manager/README.md)
|
||||
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
||||
- [Pufferspeicher-Modul](docs/module/Pufferspeicher/README.md)
|
||||
- [Batteriemodul](docs/module/Batterie/README.md)
|
||||
- [Verbraucher-1-Stufig-Modul](docs/module/Verbraucher-1-Stufig/README.md)
|
||||
- [Ladestation-Stand-Alone-Modul](docs/module/Ladestation-Stand-Alone/README.md)
|
||||
- [Ladestation-Gateway-Modul](docs/module/Ladestation-Gateway/README.md)
|
||||
- [Easee-Gateway-Modul](docs/module/Easee-Gateway/README.md)
|
||||
- [Easee-Gateway-Schnittstelle](docs/Schnittstelle-Easee-Gateway.md)
|
||||
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
||||
- [Migration Verbraucher 1-Stufig](docs/migration/Verbraucher-1-Stufig.md)
|
||||
- [Migration Batterie](docs/migration/Batterie.md)
|
||||
- [Obere Anschlüsse des Managers](docs/Obere-Anschluesse.md)
|
||||
- [Modulübersicht](docs/module/README.md)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Verbraucher 1-Stufig
|
||||
|
||||
IP-Symcon-Modul fuer einen elektrischen Verbraucher, der genau zwei
|
||||
Leistungszustaende kennt: aus (`0 W`) und ein (`Nennleistung`).
|
||||
|
||||
Das Modul ist fuer IP-Symcon ab Version 8.0 und den Enelix-Nachrichtenvertrag
|
||||
`4.0` ausgelegt. Es arbeitet ereignisbasiert und besitzt weder einen
|
||||
Regelzyklus noch eine konfigurierbare Zyklusanzahl oder Zykluszeit.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Schalten eines Boolean-Aktors auf `0 W` oder die konfigurierte Nennleistung
|
||||
- optionale separate Schaltzustands-Rueckmeldung
|
||||
- Mindest-Einschalt- und Mindest-Ausschaltdauer
|
||||
- taegliche Mindestlaufzeit
|
||||
- getrennte Prioritaeten fuer PV- und Peakbetrieb
|
||||
- ereignisbasierte Zustandsmeldung an den Enelix Manager
|
||||
- Diagnosevariablen und optionales Debug-Logging
|
||||
- sichere lokale Deaktivierung ueber die Variable `Aktiv`
|
||||
|
||||
## Installation
|
||||
|
||||
1. Im IP-Symcon Module Control die Bibliothek
|
||||
`https://git.belevo.ch/ENELIX/Enelix-EMS.git` installieren.
|
||||
2. Fuer Entwicklung und Tests den Kanal beziehungsweise Branch `develop`
|
||||
verwenden.
|
||||
3. Unter **Instanz hinzufuegen** nach **Verbraucher 1-Stufig** suchen.
|
||||
4. Eine Instanz anlegen und mindestens Nennleistung und Schaltkontakt
|
||||
konfigurieren.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
| Einstellung | Standard | Beschreibung |
|
||||
| --- | ---: | --- |
|
||||
| `PrioritaetPV` | `0` | Reihenfolge bei der PV-Leistungsverteilung. |
|
||||
| `PrioritaetPeak` | `0` | Reihenfolge bei der Lastspitzenregelung. |
|
||||
| `Meldeintervall` | `10 s` | Periodische Vollmeldung an zugeordnete Manager. |
|
||||
| `VorgabeTimeout` | `120 s` | Gueltigkeitsdauer einer Manager-Vorgabe. |
|
||||
| `Mindesteinschaltdauer` | `5 s` | Mindestdauer eines bestaetigten Ein-Zustands. |
|
||||
| `Mindestausschaltdauer` | `5 s` | Mindestdauer eines bestaetigten Aus-Zustands. |
|
||||
| `Nennleistung` | `0 W` | Leistungsaufnahme im eingeschalteten Zustand. |
|
||||
| `SchaltkontaktVariableID` | `0` | Booleanvariable des zu schaltenden Aktors. |
|
||||
| `SchaltkontaktInvertiert` | `false` | Invertiert die Aktorlogik. |
|
||||
| `RueckmeldungVariableID` | `0` | Optionale Booleanvariable fuer den physischen Zustand. |
|
||||
| `Mindestlaufzeit` | `0 s` | Geforderte Laufzeit pro lokalem Kalendertag. |
|
||||
| `PeakSperreBeiMindestlaufzeitAnbieten` | `true` | Erlaubt im Peakbetrieb eine Sperre trotz faelliger Tagesmindestlaufzeit. |
|
||||
| `DiagnosevariablenAnzeigen` | `false` | Blendet technische Diagnosevariablen ein. |
|
||||
| `LoggingEin` | `false` | Aktiviert zusaetzliche Debug-Ausgaben. |
|
||||
|
||||
`Nennleistung` muss groesser als `0` sein. Der Schaltkontakt muss eine
|
||||
Booleanvariable mit funktionsfaehiger Standard- oder benutzerdefinierter
|
||||
Aktion sein. Beide Mindestzeiten duerfen auf `0` gesetzt werden.
|
||||
|
||||
Der fruehere allgemeine `Umschaltabstand` sowie Properties fuer Zyklusanzahl
|
||||
und Zykluszeit existieren nicht.
|
||||
|
||||
## Verhalten ohne separate Rueckmeldung
|
||||
|
||||
Ist keine `RueckmeldungVariableID` konfiguriert, wird die Aktorvariable als
|
||||
unmittelbare Schaltbestaetigung verwendet.
|
||||
|
||||
Nach einem erfolgreichen Schaltbefehl beginnt ab dem uebernommenen
|
||||
Aktorzustand:
|
||||
|
||||
- beim Einschalten die `Mindesteinschaltdauer`,
|
||||
- beim Ausschalten die `Mindestausschaltdauer`.
|
||||
|
||||
Uebernimmt die Aktorvariable den angeforderten Wert nicht, meldet das Modul
|
||||
einen Schaltfehler und stellt sich dem Manager nicht als schaltbereit dar.
|
||||
|
||||
## Verhalten mit separater Rueckmeldung
|
||||
|
||||
Ist eine `RueckmeldungVariableID` konfiguriert, bestimmt ausschliesslich deren
|
||||
Booleanwert den bestaetigten Schaltzustand. `true` muss dabei physisch
|
||||
eingeschaltet bedeuten.
|
||||
|
||||
Zwischen Aktorbefehl und passender Rueckmeldung meldet das Modul:
|
||||
|
||||
- den bisherigen bestaetigten Schaltzustand und die bisherige Istleistung,
|
||||
- `SchaltbefehlAusstehend=true`,
|
||||
- `Schaltbereit=false`,
|
||||
- `AenderungMoeglich=false`.
|
||||
|
||||
Die jeweilige Mindestzeit beginnt erst, sobald die Rueckmeldung den neuen
|
||||
Zustand bestaetigt. Bleibt die Rueckmeldung aus, bleibt der Schaltbefehl
|
||||
sichtbar ausstehend. Eine abweichende Rueckmeldung ohne laufenden
|
||||
Schaltvorgang wird als Rueckmeldefehler gemeldet.
|
||||
|
||||
## Mindestzeiten und Leistungsangebot
|
||||
|
||||
Waerend einer Mindest-Ein- oder Mindest-Aus-Zeit bietet das Modul nur die
|
||||
bestaetigte aktuelle Leistung an. Ein regulaerer Lastwechsel ist erst nach
|
||||
Ablauf der Mindestzeit wieder moeglich.
|
||||
|
||||
Der Manager erhaelt dazu unter anderem:
|
||||
|
||||
| Zustand | Bedeutung |
|
||||
| --- | --- |
|
||||
| `Schaltzustand` | Bestaetigter Ein-/Aus-Zustand. |
|
||||
| `SchaltbefehlAusstehend` | Aktorbefehl wartet auf physische Bestaetigung. |
|
||||
| `Schaltbereit` | Ein weiterer regulaerer Lastwechsel ist moeglich. |
|
||||
| `RestMindestzeit_s` | Verbleibende Mindestzeit in Sekunden. |
|
||||
| `Tageslaufzeit_s` | Bestaetigte Laufzeit des aktuellen Tages. |
|
||||
| `Rueckmeldefehler` | Aktor und Rueckmeldung widersprechen sich unerwartet. |
|
||||
|
||||
Die lokale Aktion `Aktiv=false` ist ein bewusster Sicherheits-Override. Sie
|
||||
schaltet den Verbraucher auch waehrend einer laufenden
|
||||
Mindesteinschaltdauer aus.
|
||||
|
||||
## Sichtbare Variablen
|
||||
|
||||
Immer vorhanden sind:
|
||||
|
||||
- `Aktiv`: lokale Freigabe fuer das Energiemanagement
|
||||
- `Schaltzustand`: bestaetigter oder aus dem Aktor abgeleiteter Zustand
|
||||
- `Tageslaufzeit`: bestaetigte Laufzeit des aktuellen Tages in Sekunden
|
||||
|
||||
Bei aktivierter Diagnose werden zusaetzlich Soll- und Istleistung,
|
||||
Verfuegbarkeit, Schaltbereitschaft, Stoerung, Rueckmeldefehler,
|
||||
ausstehender Schaltbefehl und verbleibende Mindestzeit angezeigt.
|
||||
|
||||
## Inbetriebnahme
|
||||
|
||||
1. Nennleistung und Schaltkontakt konfigurieren.
|
||||
2. Falls vorhanden, die separate Rueckmeldung auswaehlen und ihre
|
||||
`true`-Semantik pruefen.
|
||||
3. Mindest-Ein- und Mindest-Ausschaltdauer passend zum angeschlossenen
|
||||
Geraet festlegen.
|
||||
4. Den Verbraucher im Enelix Manager manuell oder automatisch aktiv
|
||||
zuordnen.
|
||||
5. Fuer die Erstpruefung Diagnosevariablen und bei Bedarf Logging aktivieren.
|
||||
6. Die Variable `Aktiv` einschalten.
|
||||
7. Unter Aufsicht je eine Ein- und Aus-Vorgabe durch den Manager ausfuehren.
|
||||
8. Kontrollieren, dass Rueckmeldung, Mindestzeiten und `Schaltbereit`
|
||||
erwartungsgemaess wechseln.
|
||||
|
||||
## Fehlersuche
|
||||
|
||||
- **Konfiguration ungueltig:** Nennleistung, Variablentyp und Aktoraktion
|
||||
pruefen.
|
||||
- **Schaltbefehl bleibt ausstehend:** Separate Rueckmeldung und deren
|
||||
`true`-Semantik pruefen.
|
||||
- **Rueckmeldefehler:** Aktor- und Rueckmeldewert stimmen ausserhalb eines
|
||||
laufenden Schaltvorgangs nicht ueberein.
|
||||
- **Kein Lastwechsel moeglich:** `RestMindestzeit`, `Aktiv`,
|
||||
`SollwertGueltig` und die Managerzuordnung kontrollieren.
|
||||
- **Keine Manager-Vorgabe akzeptiert:** Der Verbraucher muss beim sendenden
|
||||
Manager aktiv zugeordnet sein.
|
||||
|
||||
## Tests
|
||||
|
||||
Nur dieses Modul:
|
||||
|
||||
~~~bash
|
||||
composer check:verbraucher-einstufig
|
||||
~~~
|
||||
|
||||
Gesamtes Repository inklusive dieser Testsuite:
|
||||
|
||||
~~~bash
|
||||
composer check
|
||||
~~~
|
||||
|
||||
Die Modulsuite kann damit unabhaengig weiterentwickelt werden und bleibt
|
||||
gleichzeitig Bestandteil des allgemeinen Tests.
|
||||
|
||||
## Weiterfuehrende Dokumentation
|
||||
|
||||
- [Ausfuehrliche Modulbeschreibung](../docs/module/Verbraucher-1-Stufig/README.md)
|
||||
- [Manager-Verbraucher-Schnittstelle](../docs/Schnittstelle.md)
|
||||
- [Migration von Enelix 1](../docs/migration/Verbraucher-1-Stufig.md)
|
||||
- [Aufbau der separaten Testsuite](../tests/VerbraucherEinStufig/README.md)
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Manager und Zeitverhalten",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPV",
|
||||
"caption": "Prioritaet PV",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPeak",
|
||||
"caption": "Prioritaet Peak",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Meldeintervall",
|
||||
"caption": "Meldeintervall",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "VorgabeTimeout",
|
||||
"caption": "Vorgabe-Timeout",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindesteinschaltdauer",
|
||||
"caption": "Mindest-Einschaltdauer",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindestausschaltdauer",
|
||||
"caption": "Mindest-Ausschaltdauer",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Verbraucher",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Nennleistung",
|
||||
"caption": "Nennleistung",
|
||||
"suffix": " W",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "SchaltkontaktVariableID",
|
||||
"caption": "Schaltkontakt",
|
||||
"validVariableTypes": [
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "SchaltkontaktInvertiert",
|
||||
"caption": "Schaltkontakt invertieren"
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "RueckmeldungVariableID",
|
||||
"caption": "Optionale Schaltzustands-Rueckmeldung",
|
||||
"validVariableTypes": [
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindestlaufzeit",
|
||||
"caption": "Taegliche Mindestlaufzeit",
|
||||
"suffix": " s",
|
||||
"minimum": 0,
|
||||
"maximum": 86400
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "PeakSperreBeiMindestlaufzeitAnbieten",
|
||||
"caption": "Bei Peak Sperre waehrend der Mindestlaufzeit anbieten"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Darstellung und Diagnose",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EinstellungenInVisu",
|
||||
"caption": "Lokale Einstellungen in der Visualisierung anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "DiagnosevariablenAnzeigen",
|
||||
"caption": "Diagnosevariablen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LoggingEin",
|
||||
"caption": "Diagnoseprotokoll aktivieren"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{
|
||||
"code": 102,
|
||||
"icon": "active",
|
||||
"caption": "Aktiv"
|
||||
},
|
||||
{
|
||||
"code": 201,
|
||||
"icon": "error",
|
||||
"caption": "Konfiguration ungueltig"
|
||||
},
|
||||
{
|
||||
"code": 202,
|
||||
"icon": "error",
|
||||
"caption": "Schalt- oder Rueckmeldefehler"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "{15879A4E-D0C2-4495-83DE-46E1E462591E}",
|
||||
"name": "VerbraucherEinStufig",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Verbraucher 1-Stufig"
|
||||
],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/VerbraucherEinStufig"
|
||||
}
|
||||
@@ -0,0 +1,987 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../libs/VerbraucherSchnittstelle.php';
|
||||
require_once __DIR__ . '/../libs/VerbraucherBasisTrait.php';
|
||||
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
|
||||
require_once __DIR__ . '/../libs/EinStufigRegler.php';
|
||||
|
||||
use Belevo\EnelixEMS\EinStufigRegler;
|
||||
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
||||
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
|
||||
|
||||
class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
||||
{
|
||||
use VerbraucherBasisTrait;
|
||||
|
||||
private const MANAGER_MODULE_ID = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
||||
private const STATUS_AKTIV = 102;
|
||||
private const STATUS_KONFIGURATION_UNGUELTIG = 201;
|
||||
private const STATUS_SCHALTFEHLER = 202;
|
||||
private const VM_UPDATE = 10603;
|
||||
|
||||
/** @var list<string> */
|
||||
private const DIAGNOSE_VARIABLEN = [
|
||||
'Istleistung',
|
||||
'Leistungsquelle',
|
||||
'Sollleistung',
|
||||
'SollwertGueltig',
|
||||
'Verfuegbar',
|
||||
'AenderungMoeglich',
|
||||
'Stoerung',
|
||||
'Stoertext',
|
||||
'Rueckmeldefehler',
|
||||
'SchaltbefehlAusstehend',
|
||||
'RestMindestzeit',
|
||||
];
|
||||
|
||||
public function Create(): void
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->registriereVerbraucherBasis();
|
||||
$this->RegisterPropertyInteger('Nennleistung', 0);
|
||||
$this->RegisterPropertyInteger('SchaltkontaktVariableID', 0);
|
||||
$this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false);
|
||||
$this->RegisterPropertyInteger('RueckmeldungVariableID', 0);
|
||||
$this->RegisterPropertyInteger('Mindestlaufzeit', 0);
|
||||
$this->RegisterPropertyBoolean('PeakSperreBeiMindestlaufzeitAnbieten', true);
|
||||
$this->RegisterPropertyInteger('Mindesteinschaltdauer', 5);
|
||||
$this->RegisterPropertyInteger('Mindestausschaltdauer', 5);
|
||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||
|
||||
$this->RegisterVariableBoolean('Schaltzustand', 'Schaltzustand', '~Switch', 100);
|
||||
$this->RegisterVariableInteger('Tageslaufzeit', 'Tageslaufzeit', '', 110);
|
||||
|
||||
$this->RegisterAttributeInteger('RegistrierterSchaltkontakt', 0);
|
||||
$this->RegisterAttributeInteger('RegistrierteRueckmeldung', 0);
|
||||
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
||||
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
||||
$this->RegisterAttributeInteger('BestaetigtSeit', 0);
|
||||
$this->RegisterAttributeInteger('FreigabeAb', 0);
|
||||
$this->RegisterAttributeInteger('LaufzeitStandZeit', 0);
|
||||
$this->RegisterAttributeString('LaufzeitTag', '');
|
||||
$this->RegisterAttributeString('Leistungsangebot', '[0]');
|
||||
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
|
||||
$this->RegisterAttributeBoolean('Initialisiert', false);
|
||||
$this->RegisterAttributeBoolean('SchaltvorgangLaeuft', false);
|
||||
$this->RegisterAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||
$this->RegisterAttributeBoolean('Schaltziel', false);
|
||||
$this->RegisterAttributeString('Schaltfehler', '');
|
||||
|
||||
$this->RegisterAttributeFloat('ZustandIstleistung', 0.0);
|
||||
$this->RegisterAttributeInteger('ZustandSollleistung', 0);
|
||||
$this->RegisterAttributeBoolean('ZustandSollwertGueltig', false);
|
||||
$this->RegisterAttributeBoolean('ZustandVerfuegbar', false);
|
||||
$this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false);
|
||||
$this->RegisterAttributeBoolean('ZustandStoerung', false);
|
||||
$this->RegisterAttributeString('ZustandStoertext', '');
|
||||
$this->RegisterAttributeBoolean('ZustandSchaltzustand', false);
|
||||
$this->RegisterAttributeInteger('ZustandTageslaufzeit', 0);
|
||||
$this->RegisterAttributeBoolean('ZustandRueckmeldefehler', false);
|
||||
$this->RegisterAttributeBoolean('ZustandSchaltbefehlAusstehend', false);
|
||||
$this->RegisterAttributeInteger('ZustandRestMindestzeit', 0);
|
||||
|
||||
$this->RegisterTimer(
|
||||
'Meldezyklus',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'RueckmeldungVerzoegert',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
||||
);
|
||||
// Bestehende Instanzen behalten den alten Timer deaktiviert.
|
||||
$this->RegisterTimer('Umschaltfreigabe', 0, '');
|
||||
$this->RegisterTimer(
|
||||
'MindestzeitAbgelaufen',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'MindestzeitAbgelaufen', 0);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'VorgabeTimeout',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', 0);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'Tagesplanung',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Tagesplanung', 0);"
|
||||
);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
|
||||
$this->aktualisiereVariablen();
|
||||
$this->bereinigeUngueltigenSollwert();
|
||||
$this->registriereMeldungen();
|
||||
|
||||
try {
|
||||
$this->pruefeKonfiguration();
|
||||
} catch (Throwable $fehler) {
|
||||
$this->deaktiviereTimer();
|
||||
$this->setzeZustand('Verfuegbar', false);
|
||||
$this->setzeZustand('AenderungMoeglich', false);
|
||||
$this->setzeStoerung($fehler->getMessage(), false);
|
||||
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000);
|
||||
$this->initialisiereZustand();
|
||||
$this->aktualisiere(true);
|
||||
}
|
||||
|
||||
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
|
||||
{
|
||||
if ((int) $nachricht !== self::VM_UPDATE || $this->ReadAttributeBoolean('SchaltvorgangLaeuft')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relevanteIDs = array_filter([
|
||||
$this->ReadPropertyInteger('SchaltkontaktVariableID'),
|
||||
$this->ReadPropertyInteger('RueckmeldungVariableID'),
|
||||
]);
|
||||
if (in_array((int) $senderID, $relevanteIDs, true)) {
|
||||
$this->aktualisiere(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function RequestAction($ident, $wert): void
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'Aktiv':
|
||||
$this->SetValue('Aktiv', (bool) $wert);
|
||||
if (!(bool) $wert) {
|
||||
$this->verwerfeSollwert();
|
||||
$this->SetTimerInterval('VorgabeTimeout', 0);
|
||||
}
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'Melden':
|
||||
if ((bool) $wert) {
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||
}
|
||||
$this->aktualisiere(false);
|
||||
$this->sendeVerbraucherdaten();
|
||||
return;
|
||||
|
||||
case 'MindestzeitAbgelaufen':
|
||||
case 'VorgabeTimeout':
|
||||
case 'Tagesplanung':
|
||||
$this->SetTimerInterval((string) $ident, 0);
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'ManagerdatenEmpfangen':
|
||||
if (!is_string($wert)) {
|
||||
throw new InvalidArgumentException('Managerdaten muessen als JSON uebergeben werden.');
|
||||
}
|
||||
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($daten)) {
|
||||
throw new InvalidArgumentException('Managerdaten muessen ein JSON-Objekt sein.');
|
||||
}
|
||||
$this->ManagerdatenEmpfangen($daten);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $daten */
|
||||
public function ManagerdatenEmpfangen(array $daten): void
|
||||
{
|
||||
Nachrichtenvertrag::pruefeManagerdaten($daten);
|
||||
if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) {
|
||||
throw new InvalidArgumentException('Managerdaten sind an eine andere Instanz adressiert.');
|
||||
}
|
||||
|
||||
$managerID = $daten['Kopf']['AbsenderID'];
|
||||
if (!in_array($managerID, $this->zugeordneteManagerIDs(), true)) {
|
||||
throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.');
|
||||
}
|
||||
|
||||
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||
$this->aktualisiere(false);
|
||||
$sollleistung = $daten['Sollleistung_W'];
|
||||
if ($sollleistung === null) {
|
||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
|
||||
) {
|
||||
$this->verwerfeSollwert();
|
||||
}
|
||||
$this->planeMeldung();
|
||||
return;
|
||||
}
|
||||
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
|
||||
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
|
||||
$this->setzeZustand('Sollleistung', $sollleistung);
|
||||
$this->setzeZustand('SollwertGueltig', true);
|
||||
$this->planeVorgabeTimeout(time());
|
||||
$this->aktualisiere(false);
|
||||
$this->planeMeldung();
|
||||
}
|
||||
|
||||
private function aktualisiere(bool $meldungPlanen): void
|
||||
{
|
||||
try {
|
||||
$jetzt = time();
|
||||
$this->aktualisiereTageslaufzeit($jetzt);
|
||||
$vorherigerSchaltzustand = (bool) $this->leseZustand('Schaltzustand');
|
||||
$schaltzustand = $this->leseSchaltzustand();
|
||||
|
||||
if ($schaltzustand !== $vorherigerSchaltzustand) {
|
||||
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
|
||||
}
|
||||
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
|
||||
$this->pruefeVorgabeTimeout($jetzt);
|
||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||
&& !in_array(
|
||||
(int) $this->leseZustand('Sollleistung'),
|
||||
[0, $this->ReadPropertyInteger('Nennleistung')],
|
||||
true
|
||||
)
|
||||
) {
|
||||
$this->verwerfeSollwert();
|
||||
}
|
||||
|
||||
$aktiv = (bool) $this->GetValue('Aktiv');
|
||||
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
||||
$rueckmeldefehler = $this->ReadPropertyInteger('RueckmeldungVariableID') > 0
|
||||
&& !$wartetAufRueckmeldung
|
||||
&& $schaltzustand !== $this->leseAktorSchaltzustand();
|
||||
$this->setzeZustand('Rueckmeldefehler', $rueckmeldefehler);
|
||||
|
||||
$fehlerfrei = $this->ReadAttributeString('Schaltfehler') === ''
|
||||
&& !$rueckmeldefehler;
|
||||
$verfuegbar = $aktiv && $fehlerfrei;
|
||||
$mindestlaufzeitErzwungen = $verfuegbar
|
||||
&& $this->mussMindestlaufzeitErzwingen($jetzt);
|
||||
$restMindestzeit = $wartetAufRueckmeldung
|
||||
? 0
|
||||
: EinStufigRegler::restMindestzeit(
|
||||
$this->ReadAttributeInteger('FreigabeAb'),
|
||||
$jetzt
|
||||
);
|
||||
|
||||
$peakbetrieb = $this->ReadAttributeString('Betriebsart')
|
||||
=== Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||
$peakSperreBeiMindestlaufzeitAnbieten = $this->ReadPropertyBoolean(
|
||||
'PeakSperreBeiMindestlaufzeitAnbieten'
|
||||
);
|
||||
$mindestlaufzeitLokalErzwingen = $mindestlaufzeitErzwungen
|
||||
&& (!$peakbetrieb || !$peakSperreBeiMindestlaufzeitAnbieten);
|
||||
$ziel = 0;
|
||||
if ($verfuegbar) {
|
||||
if ($mindestlaufzeitLokalErzwingen) {
|
||||
$ziel = $this->ReadPropertyInteger('Nennleistung');
|
||||
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
||||
$ziel = (int) $this->leseZustand('Sollleistung');
|
||||
}
|
||||
}
|
||||
|
||||
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
||||
$zielzustand = $ziel > 0;
|
||||
$befehlNoetig = $ziel !== $istleistung
|
||||
|| ($wartetAufRueckmeldung
|
||||
&& $zielzustand !== $this->ReadAttributeBoolean('Schaltziel'));
|
||||
// Die lokale Deaktivierung bleibt der einzige bewusste Mindestzeit-Override.
|
||||
$sicheresAusschalten = !$zielzustand && !$aktiv;
|
||||
if ($befehlNoetig
|
||||
&& ($sicheresAusschalten
|
||||
|| ($restMindestzeit === 0 && !$wartetAufRueckmeldung && $fehlerfrei))
|
||||
) {
|
||||
$this->schalte($zielzustand, $jetzt);
|
||||
$schaltzustand = $this->leseSchaltzustand();
|
||||
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
|
||||
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
||||
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
||||
$restMindestzeit = $wartetAufRueckmeldung
|
||||
? 0
|
||||
: EinStufigRegler::restMindestzeit(
|
||||
$this->ReadAttributeInteger('FreigabeAb'),
|
||||
$jetzt
|
||||
);
|
||||
}
|
||||
|
||||
$this->setzeZustand('Sollleistung', $ziel);
|
||||
$this->setzeZustand('Verfuegbar', $verfuegbar);
|
||||
$this->setzeZustand('SchaltbefehlAusstehend', $wartetAufRueckmeldung);
|
||||
$this->setzeZustand('RestMindestzeit', $restMindestzeit);
|
||||
$aenderungMoeglich = $verfuegbar
|
||||
&& (!$mindestlaufzeitErzwungen
|
||||
|| ($peakbetrieb && $peakSperreBeiMindestlaufzeitAnbieten))
|
||||
&& $restMindestzeit === 0
|
||||
&& !$wartetAufRueckmeldung;
|
||||
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
|
||||
|
||||
$angebot = EinStufigRegler::leistungsangebot(
|
||||
$this->ReadPropertyInteger('Nennleistung'),
|
||||
$istleistung,
|
||||
$verfuegbar,
|
||||
$aenderungMoeglich,
|
||||
$mindestlaufzeitErzwungen,
|
||||
$peakbetrieb,
|
||||
$peakSperreBeiMindestlaufzeitAnbieten
|
||||
);
|
||||
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
||||
$this->aktualisiereStoerung();
|
||||
$this->planeTimer($jetzt);
|
||||
$this->SetStatus(
|
||||
$this->ReadAttributeString('Schaltfehler') === ''
|
||||
&& !(bool) $this->leseZustand('Rueckmeldefehler')
|
||||
? self::STATUS_AKTIV
|
||||
: self::STATUS_SCHALTFEHLER
|
||||
);
|
||||
|
||||
if ($meldungPlanen) {
|
||||
$this->planeMeldung();
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
$this->WriteAttributeString('Schaltfehler', $fehler->getMessage());
|
||||
$this->setzeZustand('Verfuegbar', false);
|
||||
$this->setzeZustand('AenderungMoeglich', false);
|
||||
$this->setzeStoerung($fehler->getMessage(), false);
|
||||
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||
$this->protokolliere('Aktualisierung', $fehler->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function schalte(bool $einschalten, int $jetzt): void
|
||||
{
|
||||
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
|
||||
$aktorwert = $einschalten !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
|
||||
|
||||
$this->WriteAttributeBoolean('Schaltziel', $einschalten);
|
||||
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', true);
|
||||
$this->setzeZustand('SchaltbefehlAusstehend', true);
|
||||
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', true);
|
||||
try {
|
||||
RequestAction($kontaktID, $aktorwert);
|
||||
} catch (Throwable $fehler) {
|
||||
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||
$this->setzeZustand('SchaltbefehlAusstehend', false);
|
||||
throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage());
|
||||
} finally {
|
||||
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', false);
|
||||
}
|
||||
|
||||
$istzustand = $this->leseSchaltzustand();
|
||||
if ($istzustand === $einschalten) {
|
||||
$this->bestaetigeZustandswechsel($istzustand, $jetzt);
|
||||
} elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) {
|
||||
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||
$this->setzeZustand('SchaltbefehlAusstehend', false);
|
||||
throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.');
|
||||
} else {
|
||||
// Die physische Rueckmeldung bestimmt Zustand und Beginn der Mindestzeit.
|
||||
$this->setzeZustand('Rueckmeldefehler', false);
|
||||
}
|
||||
|
||||
$this->WriteAttributeString('Schaltfehler', '');
|
||||
$this->protokolliere('Schaltbefehl', ['Eingeschaltet' => $einschalten]);
|
||||
}
|
||||
|
||||
private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void
|
||||
{
|
||||
if ($this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
||||
&& $schaltzustand === $this->ReadAttributeBoolean('Schaltziel')
|
||||
) {
|
||||
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
|
||||
}
|
||||
|
||||
$this->setzeZustand(
|
||||
'SchaltbefehlAusstehend',
|
||||
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
||||
);
|
||||
}
|
||||
|
||||
private function bestaetigeZustandswechsel(bool $schaltzustand, int $jetzt): void
|
||||
{
|
||||
$vorherigerZustand = (bool) $this->leseZustand('Schaltzustand');
|
||||
$zustandGeaendert = $schaltzustand !== $vorherigerZustand
|
||||
|| $this->ReadAttributeInteger('BestaetigtSeit') <= 0;
|
||||
|
||||
$this->setzeSchaltzustand($schaltzustand);
|
||||
if ($zustandGeaendert) {
|
||||
$this->WriteAttributeInteger('BestaetigtSeit', $jetzt);
|
||||
$this->WriteAttributeInteger(
|
||||
'FreigabeAb',
|
||||
EinStufigRegler::freigabezeitpunkt(
|
||||
$schaltzustand,
|
||||
$jetzt,
|
||||
$this->ReadPropertyInteger('Mindesteinschaltdauer'),
|
||||
$this->ReadPropertyInteger('Mindestausschaltdauer')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ($schaltzustand === $this->ReadAttributeBoolean('Schaltziel')) {
|
||||
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||
}
|
||||
$this->setzeZustand(
|
||||
'SchaltbefehlAusstehend',
|
||||
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
||||
);
|
||||
$this->setzeZustand('Rueckmeldefehler', false);
|
||||
$this->protokolliere('Schaltzustand bestaetigt', ['Eingeschaltet' => $schaltzustand]);
|
||||
}
|
||||
|
||||
private function aktualisiereTageslaufzeit(int $jetzt): void
|
||||
{
|
||||
$werte = EinStufigRegler::aktualisiereTageslaufzeit(
|
||||
$this->ReadAttributeString('LaufzeitTag'),
|
||||
(int) $this->leseZustand('Tageslaufzeit'),
|
||||
$this->ReadAttributeInteger('LaufzeitStandZeit'),
|
||||
$jetzt,
|
||||
(bool) $this->leseZustand('Schaltzustand'),
|
||||
$this->zeitzone()
|
||||
);
|
||||
|
||||
$this->WriteAttributeString('LaufzeitTag', $werte['Tag']);
|
||||
$this->WriteAttributeInteger('LaufzeitStandZeit', $werte['StandZeit']);
|
||||
$this->setzeZustand('Tageslaufzeit', $werte['Sekunden']);
|
||||
}
|
||||
|
||||
private function mussMindestlaufzeitErzwingen(int $jetzt): bool
|
||||
{
|
||||
return EinStufigRegler::mussMindestlaufzeitErzwingen(
|
||||
(int) $this->leseZustand('Tageslaufzeit'),
|
||||
$this->ReadPropertyInteger('Mindestlaufzeit'),
|
||||
EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone())
|
||||
);
|
||||
}
|
||||
|
||||
private function pruefeVorgabeTimeout(int $jetzt): void
|
||||
{
|
||||
if (!(bool) $this->leseZustand('SollwertGueltig')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit');
|
||||
if ($letzteVorgabe <= 0
|
||||
|| $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout')
|
||||
) {
|
||||
$this->verwerfeSollwert();
|
||||
$this->SetTimerInterval('VorgabeTimeout', 0);
|
||||
}
|
||||
}
|
||||
|
||||
private function planeTimer(int $jetzt): void
|
||||
{
|
||||
$this->SetTimerInterval('Umschaltfreigabe', 0);
|
||||
$freigabeAb = $this->ReadAttributeInteger('FreigabeAb');
|
||||
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
||||
$this->SetTimerInterval(
|
||||
'MindestzeitAbgelaufen',
|
||||
!$wartetAufRueckmeldung && $freigabeAb > $jetzt
|
||||
? max(1, ($freigabeAb - $jetzt) * 1000)
|
||||
: 0
|
||||
);
|
||||
$this->planeVorgabeTimeout($jetzt);
|
||||
|
||||
$bisTagesende = EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone());
|
||||
$naechstePruefung = EinStufigRegler::sekundenBisNaechsteTagespruefung(
|
||||
(int) $this->leseZustand('Tageslaufzeit'),
|
||||
$this->ReadPropertyInteger('Mindestlaufzeit'),
|
||||
$bisTagesende,
|
||||
(bool) $this->leseZustand('Schaltzustand')
|
||||
);
|
||||
$this->SetTimerInterval('Tagesplanung', $naechstePruefung * 1000);
|
||||
}
|
||||
|
||||
private function planeVorgabeTimeout(int $jetzt): void
|
||||
{
|
||||
if (!(bool) $this->leseZustand('SollwertGueltig')) {
|
||||
$this->SetTimerInterval('VorgabeTimeout', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
$ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit')
|
||||
+ $this->ReadPropertyInteger('VorgabeTimeout');
|
||||
$this->SetTimerInterval('VorgabeTimeout', max(1, ($ablauf - $jetzt) * 1000));
|
||||
}
|
||||
|
||||
private function planeMeldung(): void
|
||||
{
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||
}
|
||||
|
||||
private function initialisiereZustand(): void
|
||||
{
|
||||
$jetzt = time();
|
||||
$schaltzustand = $this->leseSchaltzustand();
|
||||
|
||||
if (!$this->ReadAttributeBoolean('Initialisiert')) {
|
||||
$this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt));
|
||||
$this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt);
|
||||
$this->setzeZustand('Tageslaufzeit', 0);
|
||||
$this->verwerfeSollwert();
|
||||
$this->setzeZustand('Rueckmeldefehler', false);
|
||||
$this->WriteAttributeBoolean('Initialisiert', true);
|
||||
}
|
||||
|
||||
if ($this->ReadAttributeInteger('BestaetigtSeit') <= 0) {
|
||||
// Migration bestehender Instanzen ohne bestaetigten Zeitbezug.
|
||||
$this->WriteAttributeBoolean('Schaltziel', $schaltzustand);
|
||||
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
|
||||
} else {
|
||||
$this->setzeSchaltzustand($schaltzustand);
|
||||
$this->setzeZustand(
|
||||
'SchaltbefehlAusstehend',
|
||||
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function setzeSchaltzustand(bool $eingeschaltet): void
|
||||
{
|
||||
$this->setzeZustand('Schaltzustand', $eingeschaltet);
|
||||
$this->setzeZustand('Istleistung', (float) $this->leistungFuerSchaltzustand($eingeschaltet));
|
||||
$this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET);
|
||||
}
|
||||
|
||||
private function leseSchaltzustand(): bool
|
||||
{
|
||||
$rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID');
|
||||
if ($rueckmeldungID > 0) {
|
||||
return (bool) GetValue($rueckmeldungID);
|
||||
}
|
||||
|
||||
return $this->leseAktorSchaltzustand();
|
||||
}
|
||||
|
||||
private function leseAktorSchaltzustand(): bool
|
||||
{
|
||||
$kontaktwert = (bool) GetValue($this->ReadPropertyInteger('SchaltkontaktVariableID'));
|
||||
|
||||
return $kontaktwert !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
|
||||
}
|
||||
|
||||
private function leistungFuerSchaltzustand(bool $eingeschaltet): int
|
||||
{
|
||||
return $eingeschaltet ? $this->ReadPropertyInteger('Nennleistung') : 0;
|
||||
}
|
||||
|
||||
private function aktualisiereStoerung(): void
|
||||
{
|
||||
$texte = [];
|
||||
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||
$texte[] = $this->ReadAttributeString('Schaltfehler');
|
||||
}
|
||||
if ((bool) $this->leseZustand('Rueckmeldefehler')) {
|
||||
$texte[] = 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.';
|
||||
}
|
||||
$this->setzeStoerung(implode("\n", $texte), $texte === []);
|
||||
}
|
||||
|
||||
private function setzeStoerung(string $text, bool $fehlerfrei): void
|
||||
{
|
||||
$this->setzeZustand('Stoerung', !$fehlerfrei);
|
||||
$this->setzeZustand('Stoertext', $text);
|
||||
}
|
||||
|
||||
private function sendeVerbraucherdaten(): void
|
||||
{
|
||||
foreach ($this->zugeordneteManagerIDs() as $managerID) {
|
||||
$daten = $this->baueVerbraucherdaten($managerID);
|
||||
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
|
||||
try {
|
||||
IPS_RequestAction(
|
||||
$managerID,
|
||||
'VerbraucherdatenEmpfangen',
|
||||
json_encode($daten, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
} catch (Throwable $fehler) {
|
||||
$this->protokolliere('Managerkommunikation', $fehler->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function baueVerbraucherdaten(int $managerID): array
|
||||
{
|
||||
return [
|
||||
'Kopf' => [
|
||||
'Version' => Nachrichtenvertrag::VERSION,
|
||||
'AbsenderID' => $this->InstanceID,
|
||||
'EmpfaengerID' => $managerID,
|
||||
'Zeitpunkt' => time(),
|
||||
],
|
||||
'Betriebsart' => $this->ReadAttributeString('Betriebsart'),
|
||||
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
||||
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
||||
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
||||
'AenderungMoeglich' => (bool) $this->leseZustand('AenderungMoeglich'),
|
||||
'Verfuegbar' => (bool) $this->leseZustand('Verfuegbar'),
|
||||
'Istleistung_W' => (float) $this->leseZustand('Istleistung'),
|
||||
'Leistungsquelle' => Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET,
|
||||
'Zustand' => [
|
||||
[
|
||||
'Kennung' => 'Sollleistung_W',
|
||||
'Art' => 'Sollwert',
|
||||
'Wert' => (bool) $this->leseZustand('SollwertGueltig')
|
||||
? (int) $this->leseZustand('Sollleistung')
|
||||
: null,
|
||||
'Einheit' => 'W',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Schaltzustand',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (bool) $this->leseZustand('Schaltzustand'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'SchaltbefehlAusstehend',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (bool) $this->leseZustand('SchaltbefehlAusstehend'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Schaltbereit',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (bool) $this->leseZustand('AenderungMoeglich'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'RestMindestzeit_s',
|
||||
'Art' => 'Istwert',
|
||||
'Wert' => (int) $this->leseZustand('RestMindestzeit'),
|
||||
'Einheit' => 's',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Tageslaufzeit_s',
|
||||
'Art' => 'Istwert',
|
||||
'Wert' => (int) $this->leseZustand('Tageslaufzeit'),
|
||||
'Einheit' => 's',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Rueckmeldefehler',
|
||||
'Art' => 'Stoerung',
|
||||
'Wert' => (bool) $this->leseZustand('Rueckmeldefehler'),
|
||||
'Einheit' => '',
|
||||
'Text' => (bool) $this->leseZustand('Rueckmeldefehler')
|
||||
? 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.'
|
||||
: '',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function zugeordneteManagerIDs(): array
|
||||
{
|
||||
$ergebnis = [];
|
||||
foreach (IPS_GetInstanceListByModuleID(self::MANAGER_MODULE_ID) as $managerID) {
|
||||
try {
|
||||
$automatisch = (bool) IPS_GetProperty($managerID, 'AutomatischeSuche');
|
||||
$property = $automatisch
|
||||
? 'AutomatischeVerbraucherZuordnung'
|
||||
: 'VerbraucherZuordnung';
|
||||
$zuordnung = $this->leseManagerZuordnung($managerID, $property);
|
||||
if ($automatisch && $zuordnung === []) {
|
||||
$zuordnung = $this->leseManagerZuordnung($managerID, 'VerbraucherZuordnung');
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($zuordnung as $eintrag) {
|
||||
if (!is_array($eintrag) || ($eintrag['Aktiv'] ?? false) !== true) {
|
||||
continue;
|
||||
}
|
||||
$instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null;
|
||||
if ($instanzID === $this->InstanceID) {
|
||||
$ergebnis[] = (int) $managerID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($ergebnis));
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
private function leseManagerZuordnung(int $managerID, string $property): array
|
||||
{
|
||||
$zuordnung = json_decode(
|
||||
(string) IPS_GetProperty($managerID, $property),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
return is_array($zuordnung) ? $zuordnung : [];
|
||||
}
|
||||
|
||||
private function pruefeKonfiguration(): void
|
||||
{
|
||||
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) < 0) {
|
||||
throw new InvalidArgumentException($property . ' muss mindestens 0 sein.');
|
||||
}
|
||||
}
|
||||
foreach (['Meldeintervall', 'VorgabeTimeout'] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||
}
|
||||
}
|
||||
if ($this->ReadPropertyInteger('Nennleistung') <= 0) {
|
||||
throw new InvalidArgumentException('Nennleistung muss groesser als 0 sein.');
|
||||
}
|
||||
if ($this->ReadPropertyInteger('Mindestlaufzeit') < 0
|
||||
|| $this->ReadPropertyInteger('Mindestlaufzeit') > 86400
|
||||
) {
|
||||
throw new InvalidArgumentException('Mindestlaufzeit muss zwischen 0 und 86400 Sekunden liegen.');
|
||||
}
|
||||
foreach (['Mindesteinschaltdauer', 'Mindestausschaltdauer'] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) < 0) {
|
||||
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
|
||||
}
|
||||
}
|
||||
|
||||
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
|
||||
$this->pruefeBooleanVariable($kontaktID, 'Schaltkontakt', true);
|
||||
|
||||
$rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID');
|
||||
if ($rueckmeldungID > 0) {
|
||||
$this->pruefeBooleanVariable($rueckmeldungID, 'Rueckmeldung', false);
|
||||
}
|
||||
}
|
||||
|
||||
private function pruefeBooleanVariable(int $variableID, string $bezeichnung, bool $aktionErforderlich): void
|
||||
{
|
||||
if ($variableID <= 0 || !IPS_VariableExists($variableID)) {
|
||||
throw new InvalidArgumentException($bezeichnung . ' ist nicht eingerichtet.');
|
||||
}
|
||||
$variable = IPS_GetVariable($variableID);
|
||||
if ((int) $variable['VariableType'] !== 0) {
|
||||
throw new InvalidArgumentException($bezeichnung . ' muss eine Booleanvariable sein.');
|
||||
}
|
||||
if ($aktionErforderlich
|
||||
&& (int) $variable['VariableAction'] <= 0
|
||||
&& (int) $variable['VariableCustomAction'] <= 0
|
||||
) {
|
||||
throw new InvalidArgumentException($bezeichnung . ' benoetigt eine Standard- oder benutzerdefinierte Aktion.');
|
||||
}
|
||||
}
|
||||
|
||||
private function registriereMeldungen(): void
|
||||
{
|
||||
$alteIDs = array_unique(array_filter([
|
||||
$this->ReadAttributeInteger('RegistrierterSchaltkontakt'),
|
||||
$this->ReadAttributeInteger('RegistrierteRueckmeldung'),
|
||||
]));
|
||||
$neueIDs = array_unique(array_filter([
|
||||
$this->ReadPropertyInteger('SchaltkontaktVariableID'),
|
||||
$this->ReadPropertyInteger('RueckmeldungVariableID'),
|
||||
]));
|
||||
|
||||
foreach ($alteIDs as $alteID) {
|
||||
if (!in_array($alteID, $neueIDs, true)) {
|
||||
$this->UnregisterMessage($alteID, self::VM_UPDATE);
|
||||
}
|
||||
}
|
||||
foreach ($neueIDs as $neueID) {
|
||||
if (IPS_VariableExists($neueID)) {
|
||||
$this->RegisterMessage($neueID, self::VM_UPDATE);
|
||||
}
|
||||
}
|
||||
|
||||
$this->WriteAttributeInteger(
|
||||
'RegistrierterSchaltkontakt',
|
||||
$this->ReadPropertyInteger('SchaltkontaktVariableID')
|
||||
);
|
||||
$this->WriteAttributeInteger(
|
||||
'RegistrierteRueckmeldung',
|
||||
$this->ReadPropertyInteger('RueckmeldungVariableID')
|
||||
);
|
||||
}
|
||||
|
||||
private function aktualisiereVariablen(): void
|
||||
{
|
||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||
if ($ident === 'Leistungsquelle') {
|
||||
continue;
|
||||
}
|
||||
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||
$this->setzeZustand($ident, GetValue($variablenID));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||
$this->registriereVerbraucherDiagnose();
|
||||
$this->RegisterVariableBoolean('Rueckmeldefehler', 'Rueckmeldefehler', '~Alert', 200);
|
||||
$this->RegisterVariableBoolean(
|
||||
'SchaltbefehlAusstehend',
|
||||
'Schaltbefehl ausstehend',
|
||||
'~Switch',
|
||||
210
|
||||
);
|
||||
$this->RegisterVariableInteger('RestMindestzeit', 'Restliche Mindestzeit', '', 220);
|
||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||
$wert = $ident === 'Leistungsquelle'
|
||||
? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET
|
||||
: $this->leseZustand($ident);
|
||||
$this->setzeZustand($ident, $wert);
|
||||
}
|
||||
} else {
|
||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||
$this->UnregisterVariable($ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param bool|int|float|string $wert */
|
||||
private function setzeZustand(string $ident, $wert): void
|
||||
{
|
||||
$booleanAttribute = [
|
||||
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
||||
'Verfuegbar' => 'ZustandVerfuegbar',
|
||||
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
||||
'Stoerung' => 'ZustandStoerung',
|
||||
'Schaltzustand' => 'ZustandSchaltzustand',
|
||||
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
|
||||
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
|
||||
];
|
||||
$integerAttribute = [
|
||||
'Sollleistung' => 'ZustandSollleistung',
|
||||
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
||||
'RestMindestzeit' => 'ZustandRestMindestzeit',
|
||||
];
|
||||
$floatAttribute = [
|
||||
'Istleistung' => 'ZustandIstleistung',
|
||||
];
|
||||
$stringAttribute = [
|
||||
'Stoertext' => 'ZustandStoertext',
|
||||
];
|
||||
|
||||
if (isset($booleanAttribute[$ident])) {
|
||||
$wert = (bool) $wert;
|
||||
$this->WriteAttributeBoolean($booleanAttribute[$ident], $wert);
|
||||
} elseif (isset($integerAttribute[$ident])) {
|
||||
$wert = (int) $wert;
|
||||
$this->WriteAttributeInteger($integerAttribute[$ident], $wert);
|
||||
} elseif (isset($floatAttribute[$ident])) {
|
||||
$wert = (float) $wert;
|
||||
$this->WriteAttributeFloat($floatAttribute[$ident], $wert);
|
||||
} elseif (isset($stringAttribute[$ident])) {
|
||||
$wert = (string) $wert;
|
||||
$this->WriteAttributeString($stringAttribute[$ident], $wert);
|
||||
} elseif ($ident !== 'Leistungsquelle') {
|
||||
throw new LogicException('Unbekannter interner Zustand: ' . $ident);
|
||||
}
|
||||
|
||||
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||
SetValue($variablenID, $wert);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return bool|int|float|string */
|
||||
private function leseZustand(string $ident)
|
||||
{
|
||||
$booleanAttribute = [
|
||||
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
||||
'Verfuegbar' => 'ZustandVerfuegbar',
|
||||
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
||||
'Stoerung' => 'ZustandStoerung',
|
||||
'Schaltzustand' => 'ZustandSchaltzustand',
|
||||
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
|
||||
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
|
||||
];
|
||||
$integerAttribute = [
|
||||
'Sollleistung' => 'ZustandSollleistung',
|
||||
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
||||
'RestMindestzeit' => 'ZustandRestMindestzeit',
|
||||
];
|
||||
$floatAttribute = [
|
||||
'Istleistung' => 'ZustandIstleistung',
|
||||
];
|
||||
$stringAttribute = [
|
||||
'Stoertext' => 'ZustandStoertext',
|
||||
];
|
||||
|
||||
if (isset($booleanAttribute[$ident])) {
|
||||
return $this->ReadAttributeBoolean($booleanAttribute[$ident]);
|
||||
}
|
||||
if (isset($integerAttribute[$ident])) {
|
||||
return $this->ReadAttributeInteger($integerAttribute[$ident]);
|
||||
}
|
||||
if (isset($floatAttribute[$ident])) {
|
||||
return $this->ReadAttributeFloat($floatAttribute[$ident]);
|
||||
}
|
||||
if (isset($stringAttribute[$ident])) {
|
||||
return $this->ReadAttributeString($stringAttribute[$ident]);
|
||||
}
|
||||
|
||||
throw new LogicException('Unbekannter interner Zustand: ' . $ident);
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function leseLeistungsangebot(): array
|
||||
{
|
||||
$angebot = json_decode(
|
||||
$this->ReadAttributeString('Leistungsangebot'),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
if (!is_array($angebot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_map('intval', $angebot));
|
||||
}
|
||||
|
||||
private function zeitzone(): DateTimeZone
|
||||
{
|
||||
return new DateTimeZone(date_default_timezone_get());
|
||||
}
|
||||
|
||||
private function deaktiviereTimer(): void
|
||||
{
|
||||
foreach ([
|
||||
'Meldezyklus',
|
||||
'RueckmeldungVerzoegert',
|
||||
'Umschaltfreigabe',
|
||||
'MindestzeitAbgelaufen',
|
||||
'VorgabeTimeout',
|
||||
'Tagesplanung',
|
||||
] as $timer) {
|
||||
$this->SetTimerInterval($timer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $daten */
|
||||
private function protokolliere(string $bezeichnung, $daten): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('LoggingEin')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SendDebug(
|
||||
$bezeichnung,
|
||||
is_string($daten) ? $daten : json_encode($daten, JSON_THROW_ON_ERROR),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Manager und Zeitverhalten",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{"type": "NumberSpinner", "name": "PrioritaetPV", "caption": "Prioritaet PV", "minimum": 0},
|
||||
{"type": "NumberSpinner", "name": "PrioritaetPeak", "caption": "Prioritaet Peak", "minimum": 0},
|
||||
{"type": "NumberSpinner", "name": "Meldeintervall", "caption": "Meldeintervall", "suffix": " s", "minimum": 1},
|
||||
{"type": "NumberSpinner", "name": "VorgabeTimeout", "caption": "Vorgabe-Timeout", "suffix": " s", "minimum": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Waermepumpe und Kontakte",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Kontaktart",
|
||||
"caption": "Kontaktart",
|
||||
"options": [
|
||||
{"caption": "Sperre und Erhoehung", "value": 0},
|
||||
{"caption": "SG Ready", "value": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "Kontakt1VariableID",
|
||||
"caption": "Kontakt 1 (Sperre / SG Ready 1)",
|
||||
"validVariableTypes": [0]
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "Kontakt1Invertiert",
|
||||
"caption": "Kontakt 1 invertieren"
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "Kontakt2VariableID",
|
||||
"caption": "Kontakt 2 (Erhoehung / SG Ready 2)",
|
||||
"validVariableTypes": [0]
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "Kontakt2Invertiert",
|
||||
"caption": "Kontakt 2 invertieren"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Nennleistung",
|
||||
"caption": "Nennleistung",
|
||||
"suffix": " W",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Betriebsrueckmeldung",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "Rueckmeldungsart",
|
||||
"caption": "Rueckmeldungsart",
|
||||
"options": [
|
||||
{"caption": "Gemessene Leistung", "value": 0},
|
||||
{"caption": "Betriebsstatus", "value": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "IstleistungVariableID",
|
||||
"caption": "Leistung oder Betriebsstatus",
|
||||
"validVariableTypes": [0, 1, 2]
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Laufschwelle",
|
||||
"caption": "Laufschwelle bei Leistungsmessung",
|
||||
"suffix": " W",
|
||||
"minimum": 0,
|
||||
"digits": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Schutzzeiten",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Anlaufwartezeit",
|
||||
"caption": "Anlaufwartezeit",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Wiederholsperre",
|
||||
"caption": "Wiederholsperre nach erfolglosem Anlauf",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindestlaufzeit",
|
||||
"caption": "Mindestlaufzeit",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Mindestsperrzeit",
|
||||
"caption": "Mindestsperrzeit",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaximaleSperrzeit",
|
||||
"caption": "Maximale Sperrzeit am Stueck",
|
||||
"suffix": " s",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaximaleSperrzeit24h",
|
||||
"caption": "Maximale Sperrzeit in 24 Stunden",
|
||||
"suffix": " s",
|
||||
"minimum": 0,
|
||||
"maximum": 86400
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Darstellung und Diagnose",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EinstellungenInVisu",
|
||||
"caption": "Lokale Einstellungen in der Visualisierung anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "DiagnosevariablenAnzeigen",
|
||||
"caption": "Diagnosevariablen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LoggingEin",
|
||||
"caption": "Diagnoseprotokoll aktivieren"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{"code": 102, "icon": "active", "caption": "Aktiv"},
|
||||
{"code": 201, "icon": "error", "caption": "Konfiguration ungueltig"},
|
||||
{"code": 202, "icon": "error", "caption": "Schalt- oder Rueckmeldefehler"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}",
|
||||
"name": "VerbraucherWaermepumpe",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Waermepumpe"
|
||||
],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Waermepumpe"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Manager und Zeitverhalten",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPV",
|
||||
"caption": "Priorität PV",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrioritaetPeak",
|
||||
"caption": "Priorität Peak",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Meldeintervall",
|
||||
"caption": "Meldeintervall",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "VorgabeTimeout",
|
||||
"caption": "Vorgabe-Timeout",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "LastwechselSperrzeit",
|
||||
"caption": "Mindestzeit zwischen Lastwechseln",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Speichereinstellungen",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "List",
|
||||
"name": "LeistungsStufen",
|
||||
"caption": "Exklusive Leistungsstufen und Schaltkontakte",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "Stufe",
|
||||
"name": "Stufe",
|
||||
"width": "100px",
|
||||
"add": 1,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Leistung",
|
||||
"name": "Leistung",
|
||||
"width": "180px",
|
||||
"add": 0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"minimum": 1,
|
||||
"suffix": " W"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Schaltkontakt",
|
||||
"name": "Schaltkontakt_Stufe",
|
||||
"width": "auto",
|
||||
"add": 0,
|
||||
"edit": {
|
||||
"type": "SelectVariable"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "Boilerfuehler_PT1",
|
||||
"caption": "Boilerfühler"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LegionellenfunktionAktiv",
|
||||
"caption": "Legionellenschaltung aktiv"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "LegionellenMinimalintervallTage",
|
||||
"caption": "Frühestens nach",
|
||||
"suffix": " Tagen",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "LegionellenMaximalintervallTage",
|
||||
"caption": "Spätestens nach",
|
||||
"suffix": " Tagen",
|
||||
"minimum": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Erweiterte Speichereinstellungen",
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Boilervolumen",
|
||||
"caption": "Boilervolumen",
|
||||
"suffix": " l",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Hysterese",
|
||||
"caption": "Temperaturhysterese",
|
||||
"suffix": " K",
|
||||
"minimum": 0.1,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "TemperaturUntergrenze",
|
||||
"caption": "Untertemperaturgrenze",
|
||||
"suffix": " °C",
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "TemperaturObergrenze",
|
||||
"caption": "Übertemperaturgrenze",
|
||||
"suffix": " °C",
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "Zeitplan",
|
||||
"caption": "Zeitplan für Solltemperaturen",
|
||||
"add": true,
|
||||
"delete": true,
|
||||
"sortable": true,
|
||||
"columns": [
|
||||
{
|
||||
"caption": "Uhrzeit",
|
||||
"name": "Uhrzeit",
|
||||
"width": "150px",
|
||||
"add": "00:00",
|
||||
"edit": {
|
||||
"type": "ValidationTextBox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Solltemperatur",
|
||||
"name": "Solltemperatur",
|
||||
"width": "180px",
|
||||
"add": 50,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"digits": 1,
|
||||
"suffix": " °C"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Erweiterte sonstige Einstellungen",
|
||||
"items": [
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "TemperaturMaxAlter",
|
||||
"caption": "Maximales Messwertalter",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "BoilertemperaturGlaetten",
|
||||
"caption": "Boilertemperatur mit PT1 glätten"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "ZeitKonstante",
|
||||
"caption": "PT1-Zeitkonstante",
|
||||
"suffix": " s",
|
||||
"minimum": 1
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EinstellungenInVisu",
|
||||
"caption": "Temperatursollwerte in der Visualisierung anzeigen und bedienen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "DiagnosevariablenAnzeigen",
|
||||
"caption": "Diagnosevariablen anlegen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LoggingEin",
|
||||
"caption": "Laufendes Debug-Logging aktivieren"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": [
|
||||
{
|
||||
"code": 102,
|
||||
"icon": "active",
|
||||
"caption": "Aktiv"
|
||||
},
|
||||
{
|
||||
"code": 201,
|
||||
"icon": "inactive",
|
||||
"caption": "Temperaturmessung fehlt oder ist veraltet"
|
||||
},
|
||||
{
|
||||
"code": 202,
|
||||
"icon": "error",
|
||||
"caption": "Konfiguration ungültig"
|
||||
},
|
||||
{
|
||||
"code": 203,
|
||||
"icon": "error",
|
||||
"caption": "Schaltfehler"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "{B7C54AF4-AD7D-4FE4-B75D-203693906251}",
|
||||
"name": "VerbraucherWarmwassererwaermer",
|
||||
"type": 3,
|
||||
"vendor": "Enelix",
|
||||
"aliases": [
|
||||
"Wassererwärmer"
|
||||
],
|
||||
"parentRequirements": [],
|
||||
"childRequirements": [],
|
||||
"implemented": [],
|
||||
"prefix": "ENELIX",
|
||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Warmwassererwaermer"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
"name": "belevo/enelix-ems",
|
||||
"description": "Energiemanagement und steuerbare Verbraucher fuer IP-Symcon",
|
||||
"type": "library",
|
||||
"license": "proprietary",
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
},
|
||||
@@ -20,10 +21,19 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "find . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l",
|
||||
"lint:verbraucher-einstufig": "find VerbraucherEinStufig libs/EinStufigRegler.php tests/VerbraucherEinStufig -name '*.php' -print0 | xargs -0 -n1 php -l",
|
||||
"test": "phpunit",
|
||||
"test:verbraucher-einstufig": "phpunit --configuration phpunit.verbraucher-einstufig.xml",
|
||||
"symcon:all": "bash tests/Symcon/bin/run-symcon-tests.sh all",
|
||||
"symcon:single": "bash tests/Symcon/bin/run-symcon-tests.sh single",
|
||||
"symcon:affected": "bash tests/Symcon/bin/run-symcon-tests.sh affected",
|
||||
"check": [
|
||||
"@lint",
|
||||
"@test"
|
||||
],
|
||||
"check:verbraucher-einstufig": [
|
||||
"@lint:verbraucher-einstufig",
|
||||
"@test:verbraucher-einstufig"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Forecast- und SDL-Integration, 6. Oktober 2026
|
||||
|
||||
## Auftrag und Umfang
|
||||
|
||||
Daniel Haefliger hat die Weiterfuehrung passender unveroeffentlichter Arbeiten,
|
||||
die Integration der Prognosebedienung, SDL-Anzeigen und die Veroeffentlichung
|
||||
auf develop und beta ausdruecklich beauftragt. main bleibt unveraendert.
|
||||
|
||||
- Manager: Prognose / Forecast ist der gemeinsame Bedienort. Die bisherigen
|
||||
separaten Diagnosevariablen sind umbenannt und in der Visualisierung verborgen.
|
||||
- Batterie: die bereits integrierte Bereinigung des separaten V4-Formulars bleibt
|
||||
erhalten. Bestehende Idents, APIs und gespeicherte Freigaben werden aus
|
||||
Kompatibilitaetsgruenden nicht umbenannt oder geloescht.
|
||||
- Portal: derselbe Planner erscheint im vorhandenen Prognosebereich. Die exakte
|
||||
ausgewaehlte Eingangsprognose wird als forecastPoints eingefroren, unabhaengig
|
||||
vom durch publizierte Preise begrenzten Optimierungshorizont. Netzplan,
|
||||
Batteriestellwerte und Kosten werden nicht kuenstlich verlaengert.
|
||||
- Diagramme zeigen PV, Last, SDL-Szenario, Netz, Batterie, SOC, Preise und Kosten.
|
||||
Fehlende Werte bleiben Luecken. Prognosewerte sind keine gemessenen Erfolge.
|
||||
- Utils kapselt die generische Darstellung nicht bestimmbarer Energieanteile.
|
||||
EMS entscheidet bei SDL-Bilanzierung, dass die Haus-PV-Anteile nicht eindeutig
|
||||
aus dem SDL-haltigen Netzbezug hergeleitet werden koennen. Zaehler bleiben sichtbar.
|
||||
|
||||
## Migration und Erhaltung
|
||||
|
||||
Utils vor EMS aktualisieren. Bei alten Utils blendet EMS den eigenen Energy Pie
|
||||
bei aktiver SDL aus und protokolliert den Aktualisierungsbedarf, statt falsche
|
||||
Prozentwerte anzuzeigen oder die Manager-Regelung zu stoppen.
|
||||
|
||||
Bestehende Energy-Pie-Konfiguration, individuelle Flow-Knoten und Nicht-SDL-
|
||||
Diagrammreihen bleiben erhalten. Visualisierungsschalter nicht aus-/einschalten:
|
||||
das wuerde teilweise eine Neuerzeugung ausloesen. Historien, Energiezaehler,
|
||||
Mess-Outbox und Betriebsfreigaben werden nicht zurueckgesetzt.
|
||||
|
||||
Die Testanlage wurde mit gesicherten, hashgeprueften Quelldateien aktualisiert.
|
||||
Manager 17004 verwendet SDL-Leistung 25085 (W, Faktor +1) und SOC 23879 (%).
|
||||
Diese Werte gehoeren zur vorhandenen virtuellen EV/SDL-Aufteilung und sind kein
|
||||
unabhaengiger physischer SDL-Messnachweis. Der einzelne Batterie-Flow-Knoten wurde
|
||||
von Proxy 19274 auf 42728 umgestellt; alle anderen Felder wurden erhalten.
|
||||
Flow 57933 enthaelt zusaetzlich SDL. Leistungsdiagramm 53754 wuchs von vier auf
|
||||
sechs Reihen, Energiediagramm 19668 von sechs auf neun. Keine Alt-Reihe entfernt.
|
||||
|
||||
Private Rueckfallsicherung auf der Anlage:
|
||||
`/var/lib/symcon/backup/enelix-forecast-sdl-20261006T171851Z`.
|
||||
Sie enthaelt Konfigurationen und darf nicht in Git, Logs oder Chat kopiert werden.
|
||||
Der temporaere Beobachtungshook 12555 wurde wiederhergestellt.
|
||||
|
||||
## Pruefungen
|
||||
|
||||
- EMS PHPUnit: 360 Tests, 1771 Assertions, erfolgreich.
|
||||
- Utils PHPUnit: 58 Tests, 350 Assertions, erfolgreich.
|
||||
- Backend: 312 Tests, erfolgreich, isolierter aktueller develop-Stand mit Patch.
|
||||
- Formular: 71 Pruefungen; RequestAction: 26; Regeltests: 96; Empfaenger: 32.
|
||||
- Chart- und Energy-Pie-JavaScript: 14 Tests, erfolgreich.
|
||||
- Portal-Mount: acht Tests, erfolgreich, inklusive Anlagen-/Ansichtswechsel,
|
||||
Abmeldung, verspaeteter Antworten und unabhaengiger Fehlerbehandlung.
|
||||
- Echter Chromium: Desktop 1280 px, Mobil 390 und 320 px; Kurven, Auswahl,
|
||||
Maus/Tastatur, Lebenszyklus, CSS-Abgrenzung und Screenshots erfolgreich.
|
||||
- Testanlage nach Installation: Manager, Batterie, Energy Pie und Flow Status 102;
|
||||
SDL-Quellen und erhaltene Diagrammreihen kontrolliert.
|
||||
|
||||
## Offene Betriebsschritte, keine Produktionsfreigabe
|
||||
|
||||
Die Nachkontrolle 2026-10-06 17:26:21 UTC zeigte den bekannten Symcon-Timerfehler
|
||||
nach Module Control Reload: alter Batterie-Timer 70 blieb laufend, neue
|
||||
Melde-/Rueckmeldetimer hatten noch keinen Lauf. Ein kontrollierter Symcon-Neustart
|
||||
und danach erneute Pruefung sind erforderlich. Status 102 allein ist kein Beleg
|
||||
fuer funktionierende Timer. Die neue Netzfahrplansteuerung 46716 bleibt false.
|
||||
|
||||
Portal-Assets wurden ohne Containerneustart aktualisiert. Das Backend-Image ist
|
||||
noch nicht aktualisiert: Docker-Socketzugriff fuer agent ist nicht erlaubt.
|
||||
Vorbereitete, erfolgreich vorgepruefte Administratoruebergabe auf enelix-services:
|
||||
|
||||
```sh
|
||||
sudo python3 /srv/agent/forecast-completion-20261006/deploy_forecast_backend.py --apply
|
||||
```
|
||||
|
||||
Das Skript prueft zwei exakte Quelldateien, sichert Datenbank und Quellen,
|
||||
baut/testet das Image vor dem Austausch und bewahrt die bisherige Image-ID fuer
|
||||
Rollback. Keine Socketrechte werden geaendert. Keine Stellfreigabe wird erteilt.
|
||||
Nach erfolgreicher Installation Neuberechnung und neue forecastPoints pruefen.
|
||||
Bis dahin kennzeichnet die Anzeige alte Plaene und zeigt keine erfundenen Werte.
|
||||
|
||||
Unbefristeter Betrieb ist NICHT freigegeben: Nachweis eines geraeteseitigen
|
||||
Ausfallschutzes, physischer Soll-/Ist-Nachlauf, sichere Null-/Stop-Reaktion und
|
||||
Fortfuehrung ueber mehrere Planerneuerungen fehlen weiterhin. Die bestehende
|
||||
befristete Zustimmung bis 7. Oktober 2026, 17:38:07 Europe/Zurich wird nicht
|
||||
verlaengert. EV-Konto bleibt 161.44 kWh / 39 kW; physische Gesamtkapazitaet und
|
||||
virtuelle SDL-Energie werden nicht als zusaetzliche EV-Energie angesetzt.
|
||||
|
||||
## Pull-Request-Text
|
||||
|
||||
Forecast-Bedienung im Manager konsolidieren, vollstaendige gespeicherte
|
||||
Eingangsprognose im bestehenden Portal einbetten und SDL-Anzeigen vervollstaendigen.
|
||||
Individuelle Visualisierungen und kompatible Kennungen bleiben bestehen;
|
||||
nicht bestimmbare Energieanteile werden ehrlich als unbekannt dargestellt.
|
||||
Automatisierte Tests und responsive Browserpruefung erfolgreich. Zwei explizite
|
||||
Betriebsblocker bleiben dokumentiert; kein unbegrenzter Anlagenbetrieb aktiviert.
|
||||
@@ -0,0 +1,406 @@
|
||||
# ENELIX Netzfahrplan V4 – verbindlicher Gesprächs- und Arbeitskontext
|
||||
|
||||
## Fortsetzung 06.10.2026, 17:32 UTC: Forecast/SDL installiert, Betriebsblocker
|
||||
|
||||
Massgeblicher neuer Bericht: `docs/FORECAST_SDL_RELEASE_20261006.md`.
|
||||
Portal-Assets sind auf license.enelix.ch im vorhandenen Prognosebereich aktiv
|
||||
und ueber HTTPS hashgleich geprueft. Vollstaendige forecastPoints benoetigen noch
|
||||
das vorbereitete Backend-Image-Update durch einen Administrator. agent hat keine
|
||||
Docker-Socketrechte; keine Umgehung, kein ausgefuehrtes Backend-Deployment.
|
||||
|
||||
Testanlage: Utils und EMS aus gesichertem Kandidat geladen, SDL-Messung und
|
||||
Anzeigen eingerichtet, individuelle Reihen/Knoten und Historien erhalten.
|
||||
Alte Diagnoseanzeigen und die zwei V4-Diagnosekategorien verborgen, nichts
|
||||
geloescht oder deren Beobachter abgeschaltet. Hook 12555 wiederhergestellt.
|
||||
Nach Reload haengt erneut alter Batterietimer 70; neue Batterie-Meldetimer
|
||||
laufen nicht. Kontrollierter Symcon-Neustart und Nachkontrolle erforderlich.
|
||||
Schalter 46716 bleibt false; keine neue Stellprobe, keine Fristverlaengerung.
|
||||
Unbefristete Steuerung ist ohne unabhaengigen Ausfallschutz nicht freigegeben.
|
||||
Die automatisierten Softwaretests und responsive Chromium-Pruefung sind gruen;
|
||||
dies ist ausdruecklich keine erfolgreiche physische Anlagenabnahme.
|
||||
|
||||
## Integration 06.10.2026: Freigabe fuer develop und beta, kein Anlagenrollout
|
||||
|
||||
Daniel hat nach der Bedienintegration Merge, Commit und Push auf **develop und
|
||||
beta** ausdruecklich beauftragt. Der getestete Integrationsstand verbindet die
|
||||
22 lokalen V4-Commits bis ce525a1 mit origin/develop bis 5437f3f und den
|
||||
zugehoerigen V4-/Formularkorrekturen. Originalarbeitsverzeichnisse bleiben
|
||||
unangetastet; Integration in /srv/agent/prognose-integration-20261006/repo auf
|
||||
ubuntu. Keine Feature-Branches, kein Force-Push, main bleibt unveraendert.
|
||||
|
||||
Die Bedienung liegt im bisherigen Bereich Prognose / Forecast; der separate
|
||||
V4-Bereich entfaellt. Einzelheiten und verbleibender Umfang stehen in
|
||||
docs/prognose-bedienintegration.md. Es bleibt der vorhandene begrenzte Testpfad,
|
||||
kein regulaerer Dauerbetrieb. Keine Laufzeitordner geloescht, keine Dienste
|
||||
neu gestartet, keine Module geladen und kein neuer Stellversuch ausgefuehrt.
|
||||
Die unten dokumentierte Batterietimerblockade bleibt offen. Die ausdrueckliche
|
||||
Branchfreigabe ersetzt keine physische Anlagenabnahme oder Stellfreigabe.
|
||||
|
||||
Isoliert bestanden: 357 PHPUnit-Tests / 1758 Assertions, Syntax aller 150 PHP-
|
||||
Dateien, 310 Backend-Tests, 16 Portal-Tests und die Bedien-/V4-Einzelchecks.
|
||||
Kein neuer Symcon-Kernel-/GUI-Test, weil Docker im Agentzugang nicht nutzbar war.
|
||||
Eine lokale Backend-Aenderung, die ungepruefte Messkonfigurationen automatisch
|
||||
mit accountingEvidenceId versah, wurde nach fehlgeschlagenem Schutztest nicht
|
||||
uebernommen; Originalarbeitskopie und laufendes Backend bleiben erhalten.
|
||||
|
||||
Commit-Autor ist gemaess bestehender Teamklarstellung dh_Agent <dh@belevo.ch>,
|
||||
konfiguriertes Gitea-Konto dh. Die Repository-Abfrage bestaetigte Pushrecht;
|
||||
die Profilabfrage war mangels read:user-Scope nicht verfuegbar. Fuer den
|
||||
tatsaechlichen Veroeffentlichungsstand die Remote-Refs und den Merge-Commit
|
||||
pruefen, nicht historische Ahead-/Behind-Angaben weiter unten verwenden.
|
||||
|
||||
## Kontext-Sicherung 06.10.2026: Lesereihenfolge und Quellen
|
||||
|
||||
Auf Daniels ausdruecklichen Wunsch wird der Arbeitsstand dauerhaft verlinkt.
|
||||
Fuer den Laufzeitstand ist der unmittelbar folgende Abschnitt **06.10.2026,
|
||||
12:01 UTC** massgeblich, nicht die aelteren Abschnitte 1, 10, 11 oder 15.
|
||||
Insbesondere ist der bestaetigte Geraeteabruf bereits implementiert/installiert;
|
||||
der naechste Schritt ist die dort belegte instanzbezogene Batterietimerblockade.
|
||||
Der Abschlussnachweis `/srv/agent/v4-recovery-20261006/finished.json` auf
|
||||
iot-symcon01 wurde erneut gelesen, aber kein neuer Stellversuch ausgefuehrt.
|
||||
|
||||
Git-Nachkontrolle in dieser Kontext-Sicherung: nach erfolgreichem Fetch steht
|
||||
das Server-89-develop **22 Commits vor / 3 hinter origin/develop**; Remote-Stand
|
||||
bei Fetch `58f91e4`. Kein sicherer Fast-Forward. Bestehende fremde/lokale
|
||||
Aenderungen bleiben erhalten; kein Merge, Projekt-Commit oder Projekt-Push.
|
||||
Das ist vom anderen Checkout auf ubuntu und dessen separat dokumentierter
|
||||
Manager-Leistungsverteilung zu unterscheiden.
|
||||
|
||||
Die mitgesendeten Screenshots haben kein bestaetigtes Aufnahmedatum und sind
|
||||
historische Anschauung, kein neuer Laufzeitnachweis. Der Aktualisierungsdialog
|
||||
ist keine Freigabe, lokale Modulaenderungen zu verwerfen. Es wurden keine
|
||||
Module aktualisiert, Dienste neu gestartet oder Anlagenfreigaben veraendert.
|
||||
|
||||
## Fortsetzung 06.10.2026, 12:01 UTC: Code installiert, Batterietimer blockieren Live-Abnahme
|
||||
|
||||
- Daniel hat nach der ausdruecklichen Rueckfrage den Test ohne zusaetzliches +/-5-kW-Limit bestaetigt. Dynamische Geraetegrenzen (hier bis 39 kW), SOC- und Netzgrenzen bleiben verbindlich. Die urspruengliche Freigabe endet unveraendert am 07.10.2026 um 17:38:07 Europe/Zurich (1791387487). Kein Neustart der 48-h-Frist. Der fehlende unabhaengige Hardware-Watchdog bleibt nur durch die bereits genehmigte Testanlagen-Ausnahme abgedeckt.
|
||||
- Zusaetzliche allgemeine Arbeitsfreigabe: Routinemaessige Schreibzugriffe auf Lihrenmoos, tunel100 und Server89 benoetigen keine erneute Rueckfrage. Das hebt Sicherheitsgrenzen, technische Berechtigungen, Git-Regeln und konkrete Anlagenfreigaben nicht auf.
|
||||
- Der vorstehend beschriebene Kandidat wurde inzwischen installiert und getestet. Weitere eng begrenzte Korrekturen dieser Fortsetzung: (1) Batteriebefehl meldet den konkreten Ablehnungsgrund; (2) physische Geraeteabfrage aus dem Batterie-Stellmutex getrennt, eigenstaendiger 1-s-Samplingtimer mit nicht wartender Lesesperre; (3) Stellpfad nutzt nur unveraenderten bestaetigten Cache, maximal 2 s alt nach Wand- und monotoner Zeit, mit Konfigurations-/Quellenbindungspruefung; (4) passendes Server-Pending fuehrt auch vor Aktivstart zu 15-s-Retry statt zum Verpassen des fertigen Plans, HTTP-429 behaelt normalen Backoff. Cache-90-s-Grenze und physische Zeitstempel wurden nicht verlaengert oder erfunden.
|
||||
- Live-Installationen mit SHA-256-Pruefung und Backups: Fehlerdiagnose 11:31:18 UTC; Sampler 11:38:43 UTC; Empfang um 11:50 UTC. Arbeits- und Sicherungsordner: /srv/agent/v4-recovery-20261006 auf iot-symcon01. Berichte deployed.json, sampler-deployed.json, receiver-deployed.json und test-results.json. Geaenderte Quellen und Tests sind auch im bestehenden develop-Arbeitsverzeichnis auf enelix-services, fremde Aenderungen sind erhalten.
|
||||
- Letzter Aktivversuch: 11:50:52 UTC, Session 13395dbe-0dbb-42b8-9063-511bd7166f41. Manager meldete danach controller_command_sent (seq 5, 18693 W); Batterie wurde wegen feedback_confirmed_cache_stale_or_invalid gestoppt. Danach Managerabbruch wegen ungueltiger lokaler Betriebsdaten, controllerStopAccepted=true. Kein belastbarer Nachweis von physischem Tracking oder >90-s-Dauerbetrieb. Ein gesendeter Befehl ist ausdruecklich keine Anlagenabnahme.
|
||||
- KONKRETE OFFENE BLOCKADE: Saemtliche Timer der Batterieinstanz 44234 zeigen LastRun=0. Meldezyklus (5000 ms) und neuer Rueckmeldungstimer (1000 ms) sind lange ueberfaellig, Running=false. Manager- und uebrige Anlagentimer laufen. Instanzstatus 102; Instanz und alle Elternobjekte nicht deaktiviert; keine dauerhaft belegten Skriptthreads nachgewiesen. IPS_ApplyChanges(44234) bei bestaetigtem Null-Stopp um 11:57:31 UTC gab TRUE zurueck, hat die Timer aber nicht repariert. Die Ursache ist NICHT geklaert. Auch der Batterie-Watchdog hat bisher keinen automatischen Lauf nachgewiesen. Deshalb kein weiterer Aktivstart und keine Umgehung durch einen externen Tick-Hook.
|
||||
- Weitere beobachtete Abbruchbedingung: Durch Reloads/Messluecken fehlte zeitweise laufende Viertelstundenenergie; der Planner verwirft Luecken >120 s weiterhin korrekt. Nach einem Quartalswechsel wurden wieder optimale Plaene empfangen. Zum Schluss meldete der Betriebssender zudem veraltete Verbraucher-Rueckmeldung, passend zum nicht laufenden Batterie-Meldezyklus. Keine Energiestaende, Cursor oder Datenbankwerte wurden in dieser Fortsetzung umgeschrieben.
|
||||
- Endzustand 12:01:11 UTC: Schalter 46716=false, alter Netzfahrplan=false, Batterie status=stopped, EV-Sollwert 19651=0 W. SDL-Sollwert war 7600 W aus dem unabhaengigen bestehenden Betrieb und wurde nicht auf null gezwungen. Kein physischer V4-Regelungsnachweis. Verbindlicher Abschlussbericht: /srv/agent/v4-recovery-20261006/finished.json.
|
||||
- Alle temporaeren Start-, Deploy- und Diagnosehooks wurden aus 12555.ips.php entfernt. Original aus /srv/agent/12555.ips.php.v4hook-backup wiederhergestellt, SHA-256 c702a4947ae200b23c3eecccabeb843ad6a55e97509292584ca2de779d5e4940. Nur v4viewsRun des urspruenglichen getrennten Beobachters verbleibt. Inhaltsuche in /var/lib/symcon/scripts fand keine Verweise auf die V4-Recovery-/Continuation-Hooks. Keine unbeobachtete Wiederanlaufautomatik hinterlassen.
|
||||
- Tests erneut erfolgreich: 96 Regel-/Cache-/Lockpruefungen, 32 Empfangspruefungen, 8 Rueckmeldungs-Empfangspruefungen, 9 Geraeteabrufpruefungen und 1 Test der echten Registermethode mit synthetischen Aktionen, insgesamt 146. Syntaxpruefungen erfolgreich; git diff --check sauber. Das sind automatisierte Tests, kein Hardwareabnahmetest.
|
||||
- Git bleibt develop, ahead 22 / behind 1 gegen origin/develop, kein sicherer Fast-Forward. Keine Projekt-Commits, Merges oder Pushes. Konto nicht fuer Schreibaktion verifiziert; weder beta noch main geaendert. Die fehlende Live-Abnahme schliesst eine Beta-Freigabe zusaetzlich aus.
|
||||
- Naechster fachlicher Schritt: Instanzbezogene Symcon-Timerblockade beheben, automatische Sampler-/Melde-/Watchdog-Ausfuehrung belegen und erst dann einen kontrollierten Neustart des V4-Tests versuchen. Ein etwa erforderlicher Symcon-Dienstneustart betrifft weitere Anlagenfunktionen und wurde hier NICHT ausgefuehrt. Danach physisches Tracking, Planwechsel >90 s und bestaetigten Null-Stopp nachweisen. Alte Startmarker oder Installationshooks keinesfalls einfach loeschen/reaktivieren.
|
||||
|
||||
## Fortsetzung 06.10.2026: gepruefter Kandidat, Live-Eingriff noch gesperrt
|
||||
|
||||
- Daniel hat die Fortsetzung und die Aufhebung der 5-kW-Testgrenze beauftragt. Die bestehende 48-h-Frist, SOC-/Netz-/Geraetegrenzen und der bereits bekannte fehlende unabhaengige Hardware-Watchdog bleiben unveraendert.
|
||||
- Aktuell auf iot-symcon01 gelesen: Refresh-Deployment 07:43:14 UTC; Full-Power-Deployment 08:46:59 UTC. Diese waren schon vor dieser Fortsetzung installiert. Snapshot 09:30:41 UTC: Schalter AUS, Batterie disabled, Manager stopped_fallback_pending wegen ungueltiger lokaler Betriebsdaten. Keine physische V4-Regelung belegt.
|
||||
- Neuer Kandidat korrigiert ignorierte FALSE-Rueckgaben von IPS_RequestAction/RequestAction (Start, Command, Stop und Register), den Vor-I/O-Zeitvergleich beim Batterie-Scharfschalten und den blockierenden HTTP-Refresh im Regel-Tick. Separater Empfangstimer: 10 s Prueftakt, aktiver Abruf fruehestens nach 45 s, Fehler-Retry 15 s; Cache-Lebensdauer und Serverzeit bleiben maximal 90 s.
|
||||
- Kandidat liegt auf develop im Arbeitsverzeichnis und isoliert unter /srv/agent/v4-continuation-20261006 auf Lihrenmoos. 87 Regeltest-, 29 Empfangs-, 8 Rueckmeldungspruefungen plus 1 echter Modulmethoden-Test mit synthetischen Aktionen bestanden; 5 geaenderte Live-PHP-Dateien syntaxgeprueft. Keine Hardwaretests daraus ableiten. Testbericht: test-results.json im Stage.
|
||||
- Neue Tests decken insbesondere FALSE ohne Exception, Sekundengrenze beim Start, bytegleich behaltenen aktiven Cache, 15-s-Retry, 90-s-Ablauf und zukuenftige Empfangszeit ab. git diff --check erfolgreich.
|
||||
- Die automatische Sicherheitspruefung hat die Ausfuehrung von /srv/agent/activate_v4_continuation.py abgelehnt, weil diese Live-Installation und Aktivstart koppelt. Der Benutzer wurde um ausdrueckliche Freigabe fuer Installation und Start ohne 5-kW-Limit bis zum bisherigen Ende (07.10.2026 17:38 Europe/Zurich) gebeten. NICHT ueber andere Werkzeuge umgehen. Die Aktivierung wurde nicht ausgefuehrt; 12555.ips.php ist unveraendert.
|
||||
- Achtung: Der bestehende temporaere v4_direct_battery_command_hook.php sendet alle 30 s den letzten Managerbefehl erneut. Das kann den Replay-Schutz ausloesen. Vor freigegebenem Neustart kontrolliert entfernen. Vorbereitete Aktivierung sichert den alten Timer und ersetzt alte Schreib-/Diagnosehooks durch eine einmalige gesicherte Installation, genau einen Startversuch und reine Beobachtung. Sie ist noch nicht ausgefuehrt. Bei Live-Drift abbrechen.
|
||||
- Git nach erfolgreichem Fetch: develop ist 22 Commits vor und 1 Commit hinter origin/develop (fremder Commit 37addf4). Fremde Lade-/GUI-Aenderungen sind erhalten. Kein Fast-Forward moeglich; kein Commit, Merge oder Push in dieser Fortsetzung. Kandidatenpatch: /srv/agent/v4-continuation-20261006.patch auf enelix-services.
|
||||
- Naechster Schritt erst nach Freigabe: Stage/Live-Hashes und Zustand erneut pruefen, Kandidat installieren, genau einen Startversuch beobachten und dessen echte Batterie-Antwort pruefen. Danach >90 s mit Planwechsel, reale Leistungs-/Netzreaktion und sicheren Nullstell-Stopp nachweisen. Erst anschliessend regulaeren Testlauf belassen und temporaere Hooks entfernen. Kein stabiler Aktivbetrieb zugesichert.
|
||||
|
||||
Übergabe erstellt am 04.10.2026 auf ausdrücklichen Wunsch von Daniel Häfliger (BELEVO AG). Sie fasst die Anforderungen, Entscheidungen, Fehlerbehebungen und den zuletzt belegten Stand dieses Gesprächs zusammen. Sie ist **kein wörtliches Chatarchiv, keine neue Stellfreigabe und kein Nachweis der Produktionsreife**.
|
||||
|
||||
**Zeitbezug:** Die letzten hier belegten Betriebsberichte stammen vom **03.10.2026, ca. 14:02 Uhr Europe/Zurich**. Am 04.10. wurden für diese Übergabe die genannten Berichte und der Git-Stand erneut gelesen, aber keine neue Anlagenabnahme durchgeführt. Alte Messzahlen deshalb niemals als heutige Live-Werte ausgeben.
|
||||
|
||||
**Fortsetzung Prognose am 04.10.2026:** Der aktuelle Runtime-Stand wurde gelesen, ohne die alten Diagnose- oder Installationsschritte zu wiederholen. Der abgeleitete Datensatz `lihrenmoos-physical-published-v2` ist `model_ready` und erfüllt mit mehr als 24 nutzbaren äquivalenten Stunden die Trainingsschwelle. Die V4-Einstellungen stehen seit Revision 2 in `shadow` auf `forecastSource=corrected_profile` und `measurementDataset=lihrenmoos-physical-published-v2`; `liveEnabled` und Stellfreigabe bleiben aus. Der erste echte Lauf zeigte einen kompatibilitätsbedingten Stopp bei mikrosekundengenauen `observedAt`-Zeitstempeln. Die Korrektur rundet ausschliesslich die kausale Verfügbarkeit von Prognoseereignissen auf die nächste volle Sekunde auf; Rohmessungen bleiben streng ganzsekündlich. 310 Python- und 16 Portal-Tests sind erfolgreich. Repository und Runtime-Build-Kontext enthalten bytegleich den geprüften Fix; der laufende Container enthält ihn noch nicht, weil beide Agentzugänge weder Docker-Socket noch `sudo` erhalten. Der vorbereitete, syntaktisch geprüfte Root-Rollout liegt unter `/home/agent/services/netplan-v4-shadow/commissioning/finish_corrected_forecast_rollout.sh` und baut/testet vor dem Austausch, hält das vorige Image als Rollback fest und ändert keine Stellfreigabe. Daniel hat danach einen Aktivbetrieb angefragt; der aktuelle V4-Kern ist jedoch technisch `shadow-only`, und die weiterhin unbelegte Rückmeldung (`feedback_source_skew`, `usableForTrial=false`, `canDispatch=false`) verbietet ein blosses Umstellen. Keine Live-Freigabe wurde erteilt oder eingebaut. Git-Klarstellung durch Daniel: Commit-Autor `dh_Agent <dh@belevo.ch>`, Gitea-Konto `dh`; Commit/Push auf `develop` und `beta` am 04.10.2026 ausdrücklich freigegeben. Der Agentzugang besitzt derzeit keine HTTPS-Schreibanmeldung für `dh`.
|
||||
|
||||
**Fortsetzung bestätigte Rückmeldung am 04.10.2026:** Daniels Freigabe des modellierten `virtual_split` gilt für den ausdrücklich begrenzten Feldtest. Die physische Rückmeldung wurde um synchrone, durch den Gerätetreiber bestätigte Lesevorgänge erweitert: M-Bus verwendet `MBUS_UpdateValues`, ModBus Device und ModBus Address verwenden `ModBus_RequestRead`. Nur ein erfolgreicher Geräteaufruf erzeugt `confirmedAt`; `VariableUpdated` bleibt getrennte Herkunftsinformation. Die Regeltest-Gates verlangen `deviceReadConfirmed=true`. Das reproduzierbare inkrementelle Paket liegt auf der Testanlage unter `/srv/agent/netplan-v4-confirmed-feedback-20261004`; 18 Paketprüfsummen, 37 Rückmeldetests, 7 Geräteabruf-Tests und 71 Regeltest-Prüfungen sind erfolgreich. Es ist noch **nicht im IP-Symcon-Kernel installiert**, weil der lokale JSON-RPC-Zugang ohne Anmeldung mit HTTP 401 antwortet. Die Installation selbst lässt alten Netzfahrplan und beide Regeltestfreigaben aus. Ein realer Stellversuch bleibt zusätzlich durch den nicht belegten unabhängigen Geräte-Watchdog und die fehlende Serverfreigabe blockiert; der vorhandene Symcon-`VorgabeTimeout` ist kein unabhängiger Hardware-Nachweis. Auf `enelix-services` und der Testanlage ist kein nutzbarer Git-HTTPS-Credential-Helper hinterlegt; die Prüfung des Tunnel-Servers lief zweimal in eine externe Connector-Freigabezeitüberschreitung. Tokeninhalt wurde weder gelesen noch ausgegeben. Der Commit des Folgepakets ist lokal mit der vereinbarten Identitaet `dh_Agent <dh@belevo.ch>` erstellt. Der atomare Push auf `develop` und `beta` scheiterte ohne Teilaktualisierung, weil fuer `https://dh@git.belevo.ch` kein Passwort/Token bereitstand.
|
||||
|
||||
**Fortsetzung Aktivbereitschaft am 04.10.2026:** Die reale Stellkette wurde in den gespeicherten Testanlagen-Settings bis zu den Aktionszielen geprüft. Die ENELIX-Batterieinstanz 44234 schreibt über das Aktionsskript 31800 auf den EV-Sollwerteingang 19651 des Gateways 58448. Dieses Gateway verteilt im Zwei-Sekunden-Takt auf zwei GoodWe-Pfade über reine SetValue-Aktionsskripte und auf drei SolarEdge-Modbus-Adressen. Der ENELIX-`VorgabeTimeout` setzt bei laufendem Symcon nach 30 Sekunden auf 0 W zurück. Weder im Gateway noch in den drei GoodWe-Aktionsskripten wurde jedoch ein vom Symcon-Kernel unabhängiger Geräte-Watchdog gefunden; auch die SolarEdge-Modbus-Konfiguration weist nur Poll-/Write-Parameter, keinen Geräte-Timeout aus. Ein unbeaufsichtigter 1-2-Tage-Stellbetrieb bleibt deshalb gesperrt. Beide lokalen Regeltestfreigaben sind weiterhin aus, es wurde kein Stellbefehl ausgelöst. Der bestätigte Rückmeldeinstaller ist für einen gestoppten Kernel vorbereitet und geprüft, konnte mangels privilegierter `systemctl`-Berechtigung des Connector-Benutzers aber noch nicht ausgeführt werden. Auf `tunel100` ist ein Credential-Helper für das Konto `dh` vorhanden und ein `develop`-Dry-Run war erfolgreich; die danach nötige Connector-Freigabe lief erneut ab, bevor der lokale 19-Commit-Stand übertragen und veröffentlicht werden konnte.
|
||||
|
||||
## 1. Zuerst lesen: Wo wir tatsächlich stehen
|
||||
|
||||
- Daniel möchte die **fertige, produktionsgeeignete Anwendung**, nicht weitere isolierte Sammler, Diagnosekategorien oder wiederholte Bestätigungsrunden. Er hat die fortlaufende Umsetzung mehrfach beauftragt. Probleme im Code selbst beheben, Tests und Auslieferung bündeln; ihn nur für wirklich notwendige Root-/Symcon-Ausführung oder echte Anlagenfreigaben einbeziehen.
|
||||
- Datenaufnahme, bestätigter Versand, historische Aufbereitung, Modellaufbau, V4-Optimierer, Planempfang und ein ausdrücklich begrenzter Regeltest sind implementiert. Der zuletzt installierte Teil verbindet korrigierte physische Rückmeldung mit Vorschau und dem **begrenzten** Test-Stellpfad.
|
||||
- Der Versandblocker durch `100.0`/`100` ist **behoben und der Nachversand hat aufgeholt**. Nicht erneut beim Cursor oder der alten Kennungsdiagnose anfangen.
|
||||
- Die letzte Feedback-Installation ist **erfolgreich abgeschlossen**, nicht mehr `waiting_for_registration`. Bericht: `corrected_feedback_installed_trial_disabled`, Rückmeldung `unavailable`, Grund **`feedback_source_skew`**.
|
||||
- **Nächster konkreter Entwicklungsauftrag:** bestätigte, zum tatsächlich gelesenen Wert passende Geräte-Lesezeitpunkte an den lokalen Rückmeldepfad anschliessen. Bisher wird `VariableUpdated` als Live-Zeitbasis verwendet; unveränderte Modbus-Werte werden aber nicht zwingend bei jedem erfolgreichen Abruf erneut publiziert. Geräteabruf, Variablenpublikation und aktueller Lesezugriff müssen auseinandergehalten werden.
|
||||
- Keine künstliche Zeitstempelverjüngung, pauschale Toleranzerhöhung, Nullersetzung, historische Interpolation als Live-Messung oder Rückkehr zum festgehaltenen virtuellen EV-Wert.
|
||||
- **Weiterhin keine produktive V4-Batterieausführung:** alter intelligenter Netzfahrplan AUS; beide lokalen Regeltestfreigaben AUS; kein gestarteter Stellversuch. Bestehende lokale Regelung und SDL laufen unabhängig weiter.
|
||||
- Ein uneingeschränkter Produktions-Dauerregler ist **noch offene Umsetzung**, nicht nur ein fehlender Haken. Automatik, reale Modellqualität, Geräteausfallverhalten und Mehranlagenfreigabe haben ebenfalls verbleibende Grenzen.
|
||||
- Letzter fachlicher EMS-Commit: **`9878197`**, `develop`, bei Übergabeprüfung sauber und **15 Commits vor lokalem `origin/develop`**. Kein Remote-Fetch in dieser Prüfung. Vorherige Pushversuche scheiterten an Authentifizierung. Dokumentations-Commits dieser Übergabe können danach folgen.
|
||||
|
||||
## 2. Arbeitsweise und Freigaben
|
||||
|
||||
Daniel ist technisch versiert, will aber keine unnötige manuelle Kleinarbeit. Deutsch, Schweizer Schreibweise, konkrete nutzbare Befehle. Ein abgeschlossener Test ist nicht automatisch Installation, Installation nicht Stellfreigabe, `optimal` nicht reale Einsparung. Immer getrennt berichten: implementiert / isoliert getestet / Zielcontainer getestet / installiert / im Kernel geprüft / am Gerät erprobt / veröffentlicht.
|
||||
|
||||
**Nicht wieder in die alte Schleife zurückfallen:**
|
||||
|
||||
1. Keine neuen Diagnosekategorien als Ersatz für die eigentliche Integration.
|
||||
2. Keine erneuten Fragen nach bereits bestätigten EV-Kapazitäten oder pauschal immer wieder nach Wechselrichtertypen. Bestehende Quellen nutzen. Eine tatsächlich fehlende sicherheitsrelevante Eigenschaft darf trotzdem nicht erfunden werden.
|
||||
3. Nicht einfach „24 Stunden warten“, solange ein Software- oder Übertragungsfehler das Training verhindert.
|
||||
4. Keine alten Einzelinstaller erneut ausführen; neueren Quellstand und parallele GUI-Arbeit nicht überschreiben.
|
||||
5. Nicht behaupten, im Hintergrund weiterzuarbeiten. Nur tatsächlich ausgeführte Arbeit und belegte Ergebnisse berichten.
|
||||
6. Die Regel „ein einziger konkreter Punkt“ betraf Einträge in **Offene Punkte**, nicht Entwicklungsaufträge. Implementierungsaufträge dürfen zusammenhängende Anforderungen bündeln.
|
||||
|
||||
**Git:** Keine Feature-Branches. Entwicklung auf `develop`; nach Tests `beta`; nach Feldtest `main`. Daniel hat im Gespräch Commit/Push für `develop` und damals auch `beta` ausdrücklich freigegeben. Diese Freigabe nicht als Auftrag interpretieren, einen aktuell ungetesteten Kandidaten blind auf `beta`/`main` zu schieben. `develop`-Push ohne erneute Nachfrage im bestehenden Rahmen erlaubt. Nicht force-pushen, nicht unzusammenhängende Änderungen übernehmen, keine Zugangsdaten verlangen/ausgeben. Historische Agenten-Commitidentität war `ENELIX Agent <agent@enelix.invalid>`; keine globale Git-Konfiguration ungeprüft ändern.
|
||||
|
||||
Die aktuelle Nachricht beauftragt **Kontextübergabe**. Sie erteilt keine zusätzliche Anlagen-, Runtime- oder Stellfreigabe. Bei späterer Fortsetzung greifen die bestehenden Sicherheits- und Branchregeln weiter.
|
||||
|
||||
## 3. Ursprüngliches fachliches Ziel
|
||||
|
||||
### 3.1 Prognosefamilien und Erweiterbarkeit
|
||||
|
||||
Im GUI manuell eine Familie wählen oder `auto`:
|
||||
|
||||
| Familienschlüssel / Fahrplan | PV | Last | Fahrplanvariable |
|
||||
|---|---|---|---|
|
||||
| `3` | `prog_var_1` | `prog_var_2` | `prog_var_3` |
|
||||
| `13` | `prog_var_10` | `prog_var_11` | `prog_var_13` |
|
||||
| `23` | `prog_var_21` | `prog_var_22` | `prog_var_23` |
|
||||
|
||||
Die neun Zahlen sind nicht neun unabhängige Fahrplanmodelle. Daniel akzeptierte die Auswahl der vollständigen Modellfamilie. Registry und Datenadapter so gestalten, dass weitere Familien und später weitere flexible Anlagen ergänzt werden können. Nicht behaupten, drei bereits vorhandene Lastmethoden seien identisch mit den neu aufgebauten physischen Profilen; Datenquelle und Modellversion müssen sichtbar sein.
|
||||
|
||||
`auto` soll jene Familie wählen, die in der jüngeren Vergangenheit **wirtschaftlich am besten abgeschnitten hätte**, mit gleichen Anfangsbedingungen, Tarifen, SOC-/Restenergiebewertung und Randbedingungen. Keine Auswahl allein nach R², keine Zukunftsinformation im historischen Replay. Mindestabdeckung, Mindestdauer, Rückblick und Wechselmarge verhindern häufige/schlecht belegte Wechsel.
|
||||
|
||||
### 3.2 Netzfahrplan / Optimierung
|
||||
|
||||
- Bis zu **48 Stunden**, grundsätzlich **5-Minuten-Schritte** (höchstens 576), rollende Neuberechnung.
|
||||
- Ausgangsverlauf: korrekt abgegrenzte Verbraucherlast minus PV plus separat berücksichtigte externe Flüsse. Darauf flexible Verbraucher optimieren; aktuell EV-Batterie, später Ladestationen, Boiler usw.
|
||||
- Batterie: aktueller virtueller EV-SOC, dessen zugehörige EV-Gesamtkapazität, aktuelle maximale Lade-/Entladeleistung, Min-/Max-SOC, Reserve, Hysterese, Verfügbarkeit und lokale Einschränkungen. Keine zukünftige Energie vor dem Laden nutzen.
|
||||
- **90 % Roundtrip-Wirkungsgrad**, nicht zweimal 90 %. Implementierung verteilt Verluste mit `sqrt(0.9)` auf Laden/Entladen.
|
||||
- Netzladen ist ausdrücklich erlaubt/gewünscht, wenn freigegeben und wirtschaftlich sinnvoll. Nicht generell Nachtbezug verbieten oder tagsüber Entladung verbieten.
|
||||
- Kosten pro Intervall: Bezugsenergie mal Bezugspreis minus Einspeiseenergie mal Einspeisepreis. Leistungswerte in W/kW korrekt über Zeit in kWh umrechnen. Gesamtkosten minimieren, nicht mathematisch auf exakt null zwingen; negative Kosten sind möglich, nicht garantiert.
|
||||
- Statische oder dynamische, in der Anlage konfigurierte Bezugspreise und Einspeisevergütungen verwenden. GUI-Preisänderungen speichern und Neuberechnung anfordern. PV-Vergütung, Netzladekosten, Verluste und Peakpreis gehören in dieselbe Wirtschaftlichkeitsbetrachtung.
|
||||
- Wenn dynamische Preise noch nicht veröffentlicht sind, den **ausführbaren wirtschaftlichen Horizont auf den bekannten Preisbereich begrenzen** (`published_only`); kein frei erfundener Preis für den Rest der 48 h. Volle Prognose und ausführbaren Preishorizont unterscheiden; Ende passend zur vollständigen Abrechnungsviertelstunde.
|
||||
- Restenergie am Horizont fair bewerten/absichern, damit die Batterie nicht am Planende oder zu fast wertlosen Mittagstarifen grundlos leerverkauft wird.
|
||||
- Bezug/Einspeisung sowie Laden/Entladen nicht als unphysikalische gleichzeitige Arbitrage zulassen.
|
||||
- Einspeisebegrenzung, zulässige Abregelung/entgangene Vergütung und lokale harte Netzgrenzen berücksichtigen. Einen unerreichbaren Plan nicht als eingehalten darstellen.
|
||||
|
||||
### 3.3 Monatspeak
|
||||
|
||||
- Leistungstarif auf den relevanten **15-Minuten-Netzbezug**; persistenter Monatsmaximalwert, nicht momentane Spitzenleistung. Für den laufenden Monat zusätzliche Kosten nur für Erhöhung über den bereits erreichten Peak; Monatswechsel korrekt behandeln.
|
||||
- Bereits verstrichene Energie der aktuellen Viertelstunde mitnehmen. Fehlende Messbasis nicht durch eine konfigurierte Grenze ersetzen.
|
||||
- Am Monatsanfang nicht künstlich jede Aktivität verhindern: Restmonatsbewertung/empirische Aussicht ist vorgesehen, mit ausgewiesener Unsicherheit. Ein erwarteter künftiger Peak ist **keine bereits bezahlte kostenlose Freigabe**.
|
||||
- Anlagen können **keine feste Bezugsgrenze** haben. Falls im Manager statische oder monatliche Peakgrenzen konfiguriert sind, diese zusätzlich respektieren.
|
||||
- Tarifeinheiten und tatsächliche Abrechnungsdefinition prüfen; gemessene, geschätzte und nur konfigurierte Peakwerte getrennt kennzeichnen.
|
||||
|
||||
### 3.4 Training, Reaktion und Validierung
|
||||
|
||||
Tägliches oder wöchentliches Nachtraining nach GUI-Einstellung; Parameter-/Preisänderungen sollen Neuberechnung auslösen. Getrennte versionierte Historien und Modelle, keine alten belasteten Daten als bereinigt umbenennen. Ein Cold-Start-Modell nach Mindestdatenbasis ist noch keine validierte Feldprognose. Vergleich gegen die bisherige Batterie-Betriebsweise, nicht nur gegen „Anlage ohne Batterie“.
|
||||
|
||||
## 4. Lihrenmoos: bestätigte Anlage und massgebliche Korrekturen
|
||||
|
||||
- Installation: **`e3a08f9e-af12-4695-99bd-8b51c0520021`**.
|
||||
- Symcon Manager **`17004`**, ENELIX-Batterieinstanz **`44234`**, virtuelles Asset **`anlage01-virtual-ev`**, bestehendes EV-/SDL-Gateway **`58448`**.
|
||||
- **Massgeblich bestätigt: EV-Kapazität `161.44 kWh`, EV-Leistung `39 kW`.** Die vorher genannten `160 kWh`, `30 kW` und „je 10 kW pro WR“ hat Daniel ausdrücklich als Erinnerungskorrektur zurückgenommen. Daraus keine neue Verteilung auf Einzelgeräte ableiten.
|
||||
- Virtueller EV-SOC und die EV-Kapazität bilden ein gemeinsames Energiekonto. Keine SOC-/Energie-Resets oder Umrechnung auf 322 kWh. **SDL-Reserve ist bereits ausgegliedert** und darf nicht nochmals von EV abgezogen werden.
|
||||
- Vom Nutzer genannte physische Ausstattung: zwei GoodWe à **50 kW AC**, je **156 kWh** Batterie; SolarEdge **10 kW AC / 10 kWh** Batterie. Summe physische Kapazitäten rechnerisch 322 kWh, nicht die EV-Kapazität. AC-Nennleistung ist kein unabhängiger Nachweis jeder aktuell zulässigen Batterie-Ladeleistung.
|
||||
- Dach-PV ungefähr **20 kWp Ost + 20 kWp West**.
|
||||
- Exakte Gerätetypen/Firmware, jede AC-/DC-Messgrenze und autonomes Verhalten beim Befehlsausfall sind durch diese Nennwerte nicht automatisch bestätigt. Vorhandene Dokumente/Quellen verwenden, statt Angaben zu erfinden oder Daniel wiederholt pauschal zu befragen.
|
||||
- Historischer Tarif-Snapshot: Bezug `CKW Dynamisch Home`, statischer Fallbackwert 0.246 CHF/kWh, Einspeisung „Preis selber eingeben“ 0.10 CHF/kWh, Peakparameter 5.0. **Nicht als aktuelle Tarifvorgabe neu setzen**, vor Verwendung aktuelle Konfiguration lesen.
|
||||
|
||||
## 5. Bilanzierung und SDL: nicht wieder die alte Fehlerquelle einbauen
|
||||
|
||||
Der alte Prognosesender verwendete sinngemäss `PV + Netz − virtuelle EV-Batterieleistung`. Weil nur die virtuelle EV-Batterie in der Topologie stand, konnte **SDL als Hausverbrauch gelernt** werden. Zusätzlich hält das bestehende Gateway unter bestimmten Bedingungen gefilterte virtuelle EV-/SDL-Istleistungen fest und schreibt sie erneut. Neuer Variablenzeitstempel allein machte diese Werte nicht zur frischen physikalischen Messung.
|
||||
|
||||
**Neue Grundidee, bei passender elektrischer Messgrenze:**
|
||||
|
||||
`Verbraucherlast = Netzbezug + PV-Erzeugung − gesamte physische Batterieladung`
|
||||
|
||||
`Grundlast = Verbraucherlast − separat geplante flexible Verbraucher`
|
||||
|
||||
Grundlast bedeutet hier zeitabhängiger nicht separat geplanter Verbrauch, **keine Konstante**. Nicht separat geplante Boiler/Wallboxen bleiben vorerst darin. Später beim Übergang zu eigenständiger Planung genau einmal herausrechnen.
|
||||
|
||||
**SolarEdge-Sonderfall:** PV-Variable 20335 war bereits aus skaliertem AC-Ausgang plus Batterieleistung berechnet und auf null begrenzt. Neuer Lastadapter `solar_terminal_v1` verwendet den AC-Ausgang aus 37975/41853 direkt und zieht den SolarEdge-Batteriesaldo nicht nochmals ab. Herkunft, Vorzeichen, AC/DC und Null-Clipping beachten. Algebraischer Bilanzschluss allein ist kein unabhängiger Zählernachweis.
|
||||
|
||||
SDL wird **nicht vom V4-Flexibilitätsoptimierer gesteuert**. Bekannten externen Fahrplan berücksichtigen; bei unbekanntem Verlauf nicht still null einsetzen. Die vorhandene Schattenplanung kann den aktuellen SDL-Auftrag als ausdrücklich gekennzeichnete Fortschreibung verwenden. Das ist kein veröffentlichter Zukunftsfahrplan. Für Dauerproduktion sind belastbare externe Grenzen/Reservebehandlung und Konfliktauflösung noch zu prüfen. Ein aktueller SDL-Wert null garantiert keine SDL-freie kommende halbe Stunde.
|
||||
|
||||
Bestehendes Gateway: `/var/lib/symcon/modules/Symcon_Belevo_Energiemanagement_testing/Bat_EV_SDL_V4/module.php`. Aktionsbrücke historisch `scripts/31800.ips.php`. `State=false` war dort **kein nachgewiesener Nullstell-/Notstopp**. EV-Stopp darf nicht pauschal unabhängige SDL-Aufträge löschen. Virtuelle Energiekonten/Filter nicht ungeprüft umstellen.
|
||||
|
||||
## 6. Quellzuordnung für die nächste Arbeit
|
||||
|
||||
Werte/Identitäten mit aktueller Quellenkonfiguration abgleichen; Tabelle ist zuletzt geprüfte Zuordnung, keine Erlaubnis, Register umzulegen.
|
||||
|
||||
| Grösse | Variable | Ursprung/Umrechnung |
|
||||
|---|---:|---|
|
||||
| Original-Netzleistung | 40348 | Parent 11490, `Power_8`, kW ×1000 → W, Bezug positiv |
|
||||
| Netz-Anzeige | 49301 | Abgeleitet; nicht mit Originalquelle verwechseln |
|
||||
| GoodWe 1 PV | 48459 | Parent 19742, `A_3_3_35301`, W |
|
||||
| GoodWe 2 PV | 53802 | Parent 57658, `A_3_3_35301`, W |
|
||||
| GoodWe 1 Batterie physisch | 47725 | Parent 19742, `A_6_3_35182`, Faktor −1 → Laden positiv |
|
||||
| GoodWe 2 Batterie physisch | 35724 | Parent 57658, `A_6_3_35182`, Faktor −1 |
|
||||
| SolarEdge Batterie physisch | 21447 | Parent 30789, `Value`, Faktor +1 |
|
||||
| SolarEdge abgeleitete PV | 20335 | Parent 36915; nicht unabhängige PV-Messung |
|
||||
| SolarEdge AC-Rohwert | 37975 | Parent 35514, `Value`, ReadAddress 40083 |
|
||||
| SolarEdge AC-Skalierung | 41853 | Parent 48996, `Value`, ReadAddress 40084 |
|
||||
| Virtuelle EV-Istleistung | 52020 | Parent 58448, `Aktuelle_Leistung_EV`; Filterhaltewert möglich |
|
||||
| Virtuelle SDL-Istleistung | 25085 | Parent 58448, `Aktuelle_Leistung_SDL` |
|
||||
| EV-Auftrag | 19651 | Parent 58448, `Nennleistung_Soll_EV` |
|
||||
| SDL-Auftrag | 38943 | Parent 58448, `Nennleistung_Soll_SDL` |
|
||||
| Gateway aktiv | 23483 | Parent 58448, `State`; kein Gerätewatchdog-Nachweis |
|
||||
| EV-SOC | 32871 | Parent 58448, `SoC_EV` |
|
||||
| SDL-SOC | 23879 | Parent 58448, `SDL_Pos` |
|
||||
| EV verfügbare Ladeleistung | 50230 | Parent 58448, `P_EV_laden` |
|
||||
| EV verfügbare Entladeleistung | 43899 | Parent 58448, `P_EV_entladen` |
|
||||
| GoodWe 1 / 2 / SolarEdge SOC | 27361 / 23109 / 51938 | Originalzuordnung siehe Capture-Konfiguration |
|
||||
| Wirkenergie Bezug T1 | 59607 | Parent 11490, `Energy_0`, kWh |
|
||||
| Wirkenergie Bezug T2 | 26620 | Parent 11490, `Energy_1`; Zählerarchivierung aktiviert |
|
||||
|
||||
**Nicht wieder 53476 als nachgewiesenen Bezugszähler verwenden:** Im Gespräch zeigte 53476 / Quelle 32797 (`Energy_6`) einen unplausibel anderen Verlauf gegenüber Netzleistung und T1. Alte Historie 53476 wurde bewusst nicht überschrieben. Archivinstanz 16207. Neuzeitlicher Peak muss aus geeigneter T1/T2-Summe bzw. ausdrücklich zugelassener vollständiger Viertelstundenschätzung stammen.
|
||||
|
||||
Poller laut zuletzt gelesenem Snapshot: GoodWe 1 Parent 19742 `60000 ms`, GoodWe 2 Parent 57658 `5000 ms`; SolarEdge Batterie Parent 30789 `2000 ms`, AC Parent 35514 `1000 ms`, Skalierung Parent 48996 `20000 ms`. M-Bus Parent 11490 hatte `Interval=0`, trotzdem laufend frische Originalwerte. Nicht daraus schliessen, die Erfassung sei aus; zusätzliche bestehende Aufrufwege prüfen. Poller nie allein als erfolgreichen Geräteabruf interpretieren.
|
||||
|
||||
## 7. Hosts, Code, Dienste und Berechtigungen
|
||||
|
||||
### enelix-services / Connector Server_89
|
||||
|
||||
- Host `enelix-services`, IPv4 `87.106.60.89`, Agentbenutzer `agent`. Erlaubte Wurzeln `/home/agent/services`, `/srv/agent`.
|
||||
- Entwicklung: **`/srv/agent/repos/Enelix-EMS`**, Gitea `ENELIX/Enelix-EMS` auf `git.belevo.ch`. Weiteres Projekt `ENELIX/Enelix-Utils`; historische Referenz `dh/Symcon_Belevo_Energiemanagement_testing`. Andere Hosts/Klone nicht ohne Nachweis gleichsetzen.
|
||||
- Versionierter neuer Servercode: **`services/netplan-v4/`** im EMS-Repo.
|
||||
- Runtime/Build-Staging: **`/home/agent/services/netplan-v4-shadow`**, SQLite `data/netplan-v4.sqlite`, Compose `compose.yaml`, interner V4-Port 9100.
|
||||
- Bisheriger Forecast/API-Dienst: `/home/agent/services/prognosis-manager-enelix2`; Forecast Engine interner Port 9000. Nicht durch neue V4-Arbeit unbemerkt ersetzen.
|
||||
- Portal: `/home/agent/services/license`, `server.mjs`, `public/`, `compose.yaml`. Parallele GUI-Arbeit möglich. Unified-RC1 aktualisierte nur V4 und zugehöriges Planner-JS, nicht pauschal alle Portalprozesse.
|
||||
- Vor Veränderungen `/srv/agent/AGENT_CONTEXT.md` lesen. Agent hatte zuletzt keinen Docker-Socket-/Root-Zugriff; den Nutzer Root-Ausführung nur für tatsächlich benötigte Deployment-Schritte erledigen lassen. Keine Berechtigungen auf Docker-Socket ausweiten.
|
||||
|
||||
### iot-symcon01 / Connector Testanlage_Lihrenmoos
|
||||
|
||||
- IP-Symcon 8.0, Dienst `symcon.service`, Kerneldateien `/var/lib/symcon`, Logs `/var/log/symcon`, Programme `/usr/share/symcon`.
|
||||
- Installiertes EMS-Repo: `/var/lib/symcon/modules/Enelix-EMS`. Das ist produktionsnah laufender Code, nicht der Entwicklungscheckout auf Server_89.
|
||||
- Staging, Tests, Diagnose: `/srv/agent/netplan-v4-...`.
|
||||
- Hostregeln `/srv/agent/AGENT_CONTEXT.md` lesen. Keine allgemeinen Settings-/Credential-Exports. Für gezielte schreibgeschützte Zustandsprüfung nur erlaubte Felder extrahieren, niemals ganze secret-bearing Konfiguration ausgeben.
|
||||
- **PHP mit IPS-Funktionen gehört in Symcon**, nicht in die Linux-Bash und nicht in gewöhnliches CLI-PHP. Isolierte CLI-Tests mit simulierten IPS-Funktionen umgekehrt niemals im echten Kernel ausführen.
|
||||
- `MC_ReloadModule`/`IPS_ApplyChanges` können bestehende Initialisierungen und deren Nebenwirkungen ausführen. Nicht pauschal als ohne Auswirkungen versprechen. Code- und Settings-Sicherung, Hash-/Driftprüfung, Rücksetzweg nötig.
|
||||
- Bei verzögerter Modulregistrierung wurde ein Wiederholen angefordert. Die letzte Installation ist inzwischen fertig: **jetzt nicht wiederholen**.
|
||||
|
||||
## 8. Installierter Daten-/Prognosepfad
|
||||
|
||||
1. Manager erfasst 23 numerische Quellen alle 30 s, mit originalen Zeitstempeln und Qualitätsmerkmalen. Keine zusätzlichen Modbus-Abfragen durch den bisherigen Sammler.
|
||||
2. Lokale append-only Tagesdateien/Outbox. Versand regulär höchstens einmal/min, bis 120 Aufnahmen pro Batch; geordneter, bestätigter Cursor. Backoff bei Fehlern; Originale bleiben erhalten. `scheduled` kann reguläre Sendepause oder Backoff bedeuten; getrennte letzte erfolgreiche Bestätigung beachten.
|
||||
3. Server-Datensatz **`lihrenmoos-physical-v1`** speichert Originalaufnahmen. Abgeleiteter Datensatz **`lihrenmoos-physical-published-v2`** referenziert dieselben Daten für die verbesserte historische Zeitbehandlung.
|
||||
4. `equal_endpoint_v1`: begrenzte rückblickende Schätzung bei gleichen Werten an getrennten Originalzeitpunkten. Verfügbarkeit der späteren Bestätigung wird mitgeführt; keine Leckage in damalige Entscheidungen. Unterschiedliche Werte, lange Ausfälle, widersprüchliche Quellen oder Sammellücken nicht unbemerkt überbrücken. Live-Grenzen unverändert.
|
||||
5. Worker verarbeitet im 5-Minuten-Takt. Mindestbasis 24 **verwertbare äquivalente Stunden**, nicht bloss seit Start verstrichene Zeit. Neue Tagesprofile, versionierte Modelle, konfigurierbare tägliche/wöchentliche Neubildung. Kaltstart/Validierung sind getrennt.
|
||||
6. Neue Datenquelle muss als `corrected_profile` mit passendem `measurementDataset` gewählt werden, sobald verwendbar; zuletzt blieb **`forecastSource=legacy`, `family=3`**. Nicht bei Installation automatisch umgeschaltet. `optimal` auf der Legacy-Quelle ist kein Nachweis der neuen Prognosequalität.
|
||||
7. Wirtschaftlicher Vergleich Unified-RC1 ist angeschlossen, aber v1 ist **eingefrorener Tagesplan + simulierte lokale Netzzielnachführung**, nicht vollständiges rollendes MPC-Replay. Eine netzladefähige Batterie; gleiche Anfangsbedingungen, Restenergie, Verluste und einmalige Monatspeakabrechnung. Historischer SDL-Auftrag als gekennzeichnete Schätzung. Vollständige Modellautomatik/Produktversprechen nicht über diesen Scope hinaus behaupten.
|
||||
8. Archivierung ist implementiert, **AUS als Standard** (native `NetzfahrplanV4ArchivAktiv`, serverseitig `NETPLAN_V4_ARCHIVE_ENABLED`). Verifizierte komprimierte Archive ersetzen kein externes Backup und lösen keine unendliche Speicherkapazität. Keine unbestätigten Outbox-Daten löschen.
|
||||
|
||||
## 9. Behobene Fehler – nicht erneut aufrollen
|
||||
|
||||
- Früherer numpy-Fehler `assignment destination is read-only`, falscher Import `battery_optimizer_new`, Dateirechte im unprivilegierten Container: damalige Deployment-/Codefehler, nicht aktuelle Blocker.
|
||||
- Direkt nach Start `Connection refused`, danach HTTP 409 „bereits ein Lauf aktiv“: Start-/Parallelitätszustand, nicht durch wiederholte Neustarts lösen.
|
||||
- Native Betriebsdaten fehlten; anschließend Sender installiert. Tarifimporter und Portal-Rate-Limit wurden repariert. Alte 429-Meldungen nicht ohne neue Evidenz zur heutigen Ursache erklären.
|
||||
- Klasse `NetzfahrplanV4Messaufnahme` doppelt aus Staging und Modulpfad geladen: Installer auf eindeutige installierte Bibliothek umgestellt. `require_once` verhindert nicht verschiedene Dateien mit derselben Klasse.
|
||||
- Gemischter 120er-Block: 45 importierte/75 native Aufnahmen, keine doppelten/rückwärtslaufenden Zeiten, aber andere Kennung allein durch `splitToleranceW:100.0` vs `100`. Normalisierung korrigiert; beide vollständigen Konfigurationen exakt geprüft. Kompatibilität nur für diese anlagen- und datensatzgebundene Gleichwertigkeit registriert; Ursprungskennung/Evidence gespeichert. Keine Hashs in Originaljournalen ersetzt, kein Cursor-Sprung.
|
||||
- **Nachweis der Reparatur:** am 03.10. 12:58 Schweizer Zeit 1'782 statt 1'320 Aufnahmen, jüngste Aufnahme 12:58:09, 25 s alt, vollständiger ehemals blockierter Batch vorhanden, Manager `acknowledged`/Fehlerzahl 0. Quelle `[F]` unten. Keine Garantie über den danach liegenden Zeitraum ableiten.
|
||||
- Historische Datenqualität verbesserte sich von 0 brauchbaren Fenstern auf 104/8.66 h, nach Nachversand 130/10.82 h; zuletzt Worker 140/11.65 h. Prozentangaben zur zeitlichen Abdeckung sind **keine Messgenauigkeit und keine Prognosegüte**.
|
||||
|
||||
## 10. Installierter Rückmelde- und begrenzter Stellpfad
|
||||
|
||||
Gemeinsamer Code: `NetzfahrplanV4Rueckmeldung.php`, `BatterieNetzfahrplanV4RueckmeldungTrait.php`, `ManagerNetzfahrplanV4EmpfangTrait.php`, `ManagerNetzfahrplanV4TestTrait.php`, `BatterieNetzfahrplanV4TestTrait.php`, `NetzfahrplanV4Regeltest.php`, Manager-/Batterie-Module.
|
||||
|
||||
- Zweifaches Lesen mit Identitäts- und Originalzeitprüfung; momentan Live-Alter maximal 60 s, Quellenversatz maximal 30 s.
|
||||
- Modus in Lihrenmoos **`virtual_split`**, **`allowEstimatedForTrial=false`**. Auch gültige Rückmeldung oder Vorschau erteilt keine Freigabe.
|
||||
- Wenn SDL-Auftrag exakt null, physische Batteriesumme statt gefilterter virtueller EV-Anzeige verwenden; unveränderter Nullauftrag ist Zustand, kein Messgeräte-Heartbeat. Eine neue EV-Vorgabe bedeutet noch keine sofortige physische Reaktion.
|
||||
- Bei SDL ungleich null: zeitliche Abdeckung von Auftragsänderung, Tracking-Toleranz und explizite Schätzkennzeichnung. Gegenläufige EV-/SDL-Flüsse sind nicht eindeutig identifizierbar und bleiben für diesen Test gesperrt.
|
||||
- Netzziel wird in **Gesamt-Batterieleistung**, nicht zusätzlichen Ladeauftrag übersetzt. Erwartete Änderungen anderer Verbraucher genau einmal berücksichtigen.
|
||||
- Separate interne Server-Testsitzung, leere Default-Allowlist `NETPLAN_V4_CONTROL_TRIAL_PLANTS`, manuelle Familie, Anlagen-/Asset-/Instanz-/Revisionsbindung, beide lokalen Freigaben `NetzfahrplanV4RegeltestErlaubt`, bewusster lokaler Start, gültiger Plan, Bilanz- und Gerätewatchdog-Nachweis.
|
||||
- Test max. 30 min / 5 kW je Richtung; Serverauthority maximal 90 s; Batteriebefehl maximal 10 s; Batterie-Softwarewatchdog 1 s, Manager-Testtakt 2 s. Nicht einfach Limits für Produktion hochsetzen.
|
||||
- Batterie liest vor Ausgabe unabhängig neu und prüft SOC, Reserve, Hysterese, Verfügbarkeit, Sperrzeiten und Netzgrenzen. Unmögliches Netzziel → Testabbruch, keine Erfolgsbehauptung.
|
||||
- Abbruch widerruft Sitzung vor Nullstellversuch; Stoppfehler bleibt sichtbar. Danach maximal eine **frisch berechnete** normale Zuteilung, keine Rückkehr zu gecachtem Test-/Vor-Testwert. Keine Wiederbelebung nach Neustart oder doppeltem Befehl.
|
||||
- `shadow_seen` bedeutet Empfang, **nicht ausgeführt**. `canDispatch=false` in Vorschau bedeutet keine Vorschau-Freigabe. Bei einer separat gestarteten Testsitzung wäre `runMode=shadow` alleine trotzdem kein hinreichender Nachweis für null Stellbefehle.
|
||||
- Softwarewatchdog funktioniert nur bei laufendem Kernel/Kommunikation. Kein Nachweis für Rechnerabsturz, Stromausfall oder unerreichbaren Wechselrichter. Bereits vorhandenes Verhalten prüfen, nicht erfinden und nicht Schutzgates entfernen.
|
||||
|
||||
## 11. Aktueller Blocker: bestätigter Geräteabruf statt falscher Zeitbasis
|
||||
|
||||
Letzter Installationsbericht `[A]`:
|
||||
|
||||
```
|
||||
status: corrected_feedback_installed_trial_disabled
|
||||
feedback.status: unavailable
|
||||
feedback.reason: feedback_source_skew
|
||||
batteryW: null
|
||||
gridW: null
|
||||
usableForTrial: false
|
||||
canDispatch: false
|
||||
```
|
||||
|
||||
Das ist **kein Installationsfehler**. Die Meldung „Modellanteil: nein“ im Installer ist im Fehlerfall irreführend: fehlendes Feld wird als false dargestellt, obwohl die Berechnung gar nicht erfolgreich war. Kleine UI-/Diagnosekorrektur mitnehmen.
|
||||
|
||||
Letzte Zeituntersuchung `[B]` (237 Aufnahmen / 2 h): 48 Aufnahmen mit Versatz >30 s. Netz median 1 s/max 2 s, GoodWe 1 median 1 s/max 48 s, GoodWe 2 median 1 s/max 46 s, SolarEdge median 12 s/max 59 s; SolarEdge in 223 Aufnahmen unter den ältesten Quellen. Keine der vier Quellen in genau diesem Zeitfenster über 60 s. Beispiel 13:58:38: drei Quellen neu, SolarEdge 31 s alt → Ablehnung. Frühere mehrminütige Lücken trotzdem nicht damit wegargumentieren.
|
||||
|
||||
**Nächste Umsetzung, ohne neue Diagnosekategorie:**
|
||||
|
||||
1. Installierten Gerätemodultyp und unterstützten Leseweg verifizieren. Im Gespräch wurde `ModBus_RequestRead()` als möglicher Ansatz genannt. API-/Erfolgs-/Zeitsemantik und Unterschied `ModBus Address` vs `ModBus Device` anhand offizieller aktueller Dokumentation und vorhandenem Code prüfen; nicht voraussetzen, dass ein boolescher Erfolg alle Register atomar aktualisiert oder einen unabhängigen Gerätezeitpunkt liefert.
|
||||
2. Erfolgreiche vollständige Antwort mit genau den daraus stammenden Werten, Zuordnung, Status und Bestätigungszeit binden. `VariableUpdated`, Geräteabrufbestätigung und Sammlerzeit getrennt erhalten.
|
||||
3. Begrenzte Abfragefrequenz, Timeout, Serialisierung, Kommunikationslast und Lesekohärenz berücksichtigen. Keine Netz-I/O in jedem Vorschau-/Regelcallback vervielfachen, keine blockierenden Abfragen unter dem Stellregler-Lock. Existierende erfolgreiche Polls nach Möglichkeit nutzen.
|
||||
4. Wenn keine echte Bestätigung vorliegt, unsicher/gesperrt bleiben. Keine künstlichen `SetValue`-Herzschläge und keine permissive Freigabe bloss wegen des Pollerintervalls.
|
||||
5. In bestehenden Batterie-/Managerpfad integrieren; Tests: unveränderte Werte mit erfolgreichem Abruf, fehlgeschlagene/partielle Antwort, Zeitversatz, Race, Wiederholung, Neustart, erneute unabhängige Batterieprüfung und Rückfall.
|
||||
6. Zusammenhängenden Kandidaten bauen und testen; neue Runtime-Installation nur im bekannten gesicherten Verfahren. Bisherige Installer nicht nochmals ausführen.
|
||||
|
||||
Diese Arbeit wurde am Gesprächsende **noch nicht implementiert oder installiert**. Sie ist nicht durch die vorliegende Dokumentationsübergabe erledigt.
|
||||
|
||||
## 12. Produktionsreife: verbleibende echte Arbeiten
|
||||
|
||||
- Verlässlicher bestätigter Live-Messpfad wie oben; unklares AC/DC/externes SDL nicht durch Labels als verifiziert deklarieren.
|
||||
- Neue Lastprofile mit tatsächlicher ausreichender Datenbasis/Validierung aufbauen und gezielt zur Schattenrechnung schalten. Aktuelle Einstellungen/Modelle vorher lesen, nicht alte Momentaufnahmen fortschreiben.
|
||||
- **Dauerbetrieb** mit sauberem Authority-/Zustands-/Neustart-/Fallback-Verhalten implementieren. Bisher nur begrenzter Pilot, keine fertige kontinuierliche Produktionsfreigabe.
|
||||
- Reale Callback-/Registerstrecke und separat freigegebener Stellversuch; korrekte Vorzeichen, verfügbare Leistungen, Netzziel/Peak-/Exportgrenzen, Abschalten/Verbindungsausfall, lokale Priorität und SDL-Konflikte prüfen.
|
||||
- Modellautomatik über den deklarierten Frozen-Day-v1-Scope hinaus nur nach Erweiterung/Nachweis bewerben; Grenzen transparent halten.
|
||||
- Lebenszyklus/Backups/Recovery/Observability, Mehranlagenisolierung und parametrisierte Inbetriebnahme. Einige Installer sind bewusst auf Lihrenmoos gebunden; für andere Anlagen nicht IDs blind übernehmen.
|
||||
- Git-Authentifizierung/Veröffentlichung lösen; `develop` → geprüfte `beta` → Feldabnahme `main`. Browser-Login, Tailscale oder Portforwarding stellen nicht automatisch Git-Schreibauthentifizierung bereit. Keine Veröffentlichung behaupten, die nicht passiert ist.
|
||||
|
||||
## 13. Wichtige Meilensteine und nicht erneut auszuführende Installer
|
||||
|
||||
| Paket | Status bis Gesprächsende |
|
||||
|---|---|
|
||||
| V4-Schattenserver / Native Operation / Tarife | Installiert, Betrieb historisch belegt |
|
||||
| Passiver Empfänger `/srv/agent/netplan-v4-receiver-stage/install.php` | Installiert, automatische `shadow_seen`-ACKs belegt |
|
||||
| Rohsammler `/srv/agent/netplan-v4-data-capture-stage` | Separater früherer Sammler; Originaldateien erhalten |
|
||||
| Leseexport `/srv/agent/netplan-v4-data-review-stage` | Einmaliger Export wegen restriktiver Originalrechte; heute keine erneute allgemeine Exportpflicht |
|
||||
| Getrennter Beobachter `/srv/agent/netplan-v4-separated-observer-stage` | 23 Quellen, Dateien gezielt lesbar; weiterhin Originalbestand |
|
||||
| Managerdaten `/srv/agent/netplan-v4-application-build/install.php` | Installiert, Klasse-/Kennungsfehler später behoben |
|
||||
| `deploy_history_timing.py` | Installiert, neue Historienversion v2 |
|
||||
| `deploy_unified_release.py --approve-numeric-mapping-compatibility` + `/srv/agent/netplan-v4-unified-release/install.php` | Installiert, Aliasfreigabe exakt registriert, Nachversand aufgeholt |
|
||||
| `/srv/agent/netplan-v4-feedback-stage/install.php` | **Fertig installiert nach einmaliger Registrierungsschleife**, aktuell Zeitversatzprüfung |
|
||||
|
||||
Fachliche Commitfolge (lokales `develop`; nicht als Remote-/Runtime-Gleichstand missverstehen):
|
||||
`b5c1fa6` Empfänger, `9394fe3` begrenzter Test, `e0e52d2` Bilanzierung, `76f8b06` Capture, `44362e4` getrennte Beobachtung, `fe3f46b` Datenanwendung, `6d9aba7` Klassenladen, `5890093` Historienzeit, `6c91d6b` Zahlennormalisierung, `8eb9688` Unified RC1, **`9878197` korrigierte Rückmelde-/Testkette**. Einige weitere Commits können dazwischenliegen; `git log` ist massgeblich.
|
||||
|
||||
## 14. Quellenindex und Tests – zuerst Nachweise lesen, nicht neu installieren
|
||||
|
||||
**[A] Letzte erfolgreiche Rückmeldeinstallation (Testhost):**
|
||||
`/srv/agent/netplan-v4-feedback-stage/INSTALL_RESULT.json` – Ende `2026-10-03T11:59:58Z`. Der frühere `waiting_for_registration`-Bericht ist überholt. Die Sicherung stammt vom ersten Dateidurchlauf, nicht vom zweiten `already_installed`-Durchlauf.
|
||||
|
||||
**[B] Letzte Zeit-/Pipeline-Auswertung (Server):**
|
||||
`/home/agent/services/qa/feedback-timing-20261003/TIMING_REVIEW.json` – `2026-10-03T12:02:13Z`, 140 brauchbare Fenster/11.646 h, Quelle v2 noch `collecting`.
|
||||
|
||||
**[C] Unified-Serverinstallation:**
|
||||
`/home/agent/services/netplan-v4-shadow/unified-releases/20261003T105407.036202Z/REPORT.json`; Runtime `unified-rc1`, Mappingkompatibilität registriert, Einstellungen unverändert.
|
||||
|
||||
**[D] Unified-Managerinstallation (Testhost):**
|
||||
`/srv/agent/netplan-v4-unified-release/INSTALL_RESULT.json`.
|
||||
|
||||
**[E] Historieninstallation:**
|
||||
`/home/agent/services/netplan-v4-shadow/history-timing-releases/20261003T092245.175100Z/REPORT.json`.
|
||||
|
||||
**[F] Erfolgreiches Aufholen des Datenwegs:**
|
||||
`/home/agent/services/qa/unified-release-20261003/FLOW-20261003T105834.324945Z.json`.
|
||||
|
||||
**[G] Installiertes und versioniertes Verhalten:**
|
||||
`docs/netplan-v4-corrected-feedback.md`, `docs/netplan-v4-unified-release.md`, `docs/netplan-v4-history-timing.md`, `docs/netplan-v4-numeric-mapping-fix.md`, `docs/netplan-v4-application.md`, `docs/netplan-v4-controlled-trial.md`, `docs/netplan-v4-accounting.md`. Alte „noch nicht installiert“-Abschnitte dieser Entwicklungsdokumente durch aktuelle Installationsberichte einordnen; nicht mit echten Runtime-Nachweisen verwechseln.
|
||||
|
||||
**[H] Rückmelde-/Testsuite (Testhost):**
|
||||
`/srv/agent/netplan-v4-feedback-stage/PREPARATION.json`, `TEST_RESULTS.txt`, `INSTALLER_TEST_RESULTS.json`, `ORDINARY_OFFER_TEST.json`, `MANIFEST.json`, `PACKAGE_HASHES.json`, `feedback-config.json`.
|
||||
125 isolierte Funktionsprüfungen, 4 vollständige Modul-Linkageprüfungen, 10 Installationsszenarien, 13'824 übereinstimmende normale Leistungsangebotsfälle. Keine Aussage, dass dieselbe Zahl Tests im echten Kernel/Wechselrichter gelaufen sei.
|
||||
|
||||
**[I] Unified-Suite (Server):**
|
||||
`/home/agent/services/qa/unified-release-20261003/PYTHON_TEST_RESULTS.txt`, `NODE_TEST_RESULTS.txt`, `PUBLICATION.json`, `DELIVERY_STATUS.json`.
|
||||
306 Python- und 16 Portaltests; native separate Tests. Spätere Änderungen brauchen erneute relevante Tests, nicht bloss Übernahme dieser Zahlen.
|
||||
|
||||
**[J] Wiederaufbau und Code:**
|
||||
EMS `examples/V4CorrectedFeedback/`, `examples/V4UnifiedRelease/`, `services/netplan-v4/commissioning/`. Staging ist nicht dieselbe Quelle wie das laufende Modul. Dateien, Hashs und Abhängigkeiten vor Änderungen vergleichen; Paralleländerungen abbrechen statt überschreiben.
|
||||
|
||||
**[K] Quellen-/Konfigurationsdiagnose (Testhost):**
|
||||
`/srv/agent/netplan-v4-application-build/inspect_source_update_policy.py`, `inspect_delivery_state.py`, `MAPPING_IDENTITY_DIAGNOSIS.json`, `MAPPING_EQUIVALENCE.json`; `netplan-v4-feedback-stage/review_current_feedback_state.py`. Diese Skripte zuerst lesen; manche erzeugen nur einen Bericht, manche lesen gespeicherte statt live aktuelle Settings. Alte Settings-Snapshots können eine bereits installierte Eigenschaft noch nicht enthalten.
|
||||
|
||||
**Sicherheitsnotiz:** In einem alten Upload-Skript wurden Klartextzugangsdaten bemerkt. Nicht lesen, verbreiten, in Dokumentation/Git übernehmen oder für neue Integration verwenden. Separate autorisierte Rotation/Secret-Auslagerung bleibt Aufgabe; die Werte gehören nicht in diese Übergabe.
|
||||
|
||||
## 15. Übernahmeroutine für einen neuen Agenten
|
||||
|
||||
1. Host-`AGENT_CONTEXT.md`, Repo-`AGENTS.md` und diese Datei vollständig lesen; bei fehlendem Remote-Stand zuerst den Servercheckout prüfen.
|
||||
2. `git status`/Branch/letzte Commits sowie `[A]`, `[B]`, `[C]` lesen. Zeitstempel nennen; keine alten Zähler als heutige Werte ausgeben. Diese Übernahme selbst ändert keine Anlage.
|
||||
3. Nur notwendige gezielte Read-only-Prüfungen. Kein weiterer vollständiger Neustart der Analyse oder Wiederholung bereits erledigter Installer.
|
||||
4. Bei Fortsetzungsauftrag direkt Abschnitt 11 umsetzen und anschliessend echte Restarbeiten aus Abschnitt 12 abarbeiten. Anforderungen, Testfall, Implementierung und Rollout in einem Paket halten.
|
||||
5. Nach jedem Abschluss diese Übergabe mit Datum, Codecommit, tatsächlichem Installations-/Teststatus und nächstem konkreten Arbeitsschritt ergänzen. Keine gescheiterten Schritte in „fertig“ umdeuten. Bei Widerspruch aktuelle geprüfte Laufzeit und ausdrückliche spätere Nutzerkorrektur nennen.
|
||||
|
||||
**Kurzübernahme:** „Netzfahrplan V4 in ENELIX fortsetzen. Datenkennung/Versand repariert, Unified RC1 und korrigierter Feedback-/Testpfad installiert. Aktuell `feedback_source_skew`; bestätigte Geräte-Lesezeitpunkte in die bestehende lokale Rückmeldung integrieren. EV 161.44 kWh/39 kW. Keine neue Diagnosekategorie, keine alten Installer, kein automatischer Stelltest. Dauerproduktion ist noch nicht fertig; Tests/Abnahme/Veröffentlichung sauber getrennt halten.“
|
||||
@@ -0,0 +1,60 @@
|
||||
# Obere Anschlüsse des Managers
|
||||
|
||||
> Status: Lizenzierung, Prognose und Stoerueberwachung sind an
|
||||
> `license.enelix.ch` angebunden. SDL/VGT bleibt als optionaler Anschluss
|
||||
> vorbereitet.
|
||||
|
||||
Die Anschluesse werden ausschliesslich im Manager konfiguriert. Sie erzeugen
|
||||
keine Abhaengigkeit zwischen Enelix EMS und Enelix Utils.
|
||||
|
||||
| Anschluss | Aufgabe | Gegenstelle |
|
||||
| --- | --- | --- |
|
||||
| SDL/VGT | Zeitlich gueltige Leistungsauftraege und Rueckmeldungen | Optional das unabhaengige Utils-Modul VGT-Schnittstelle |
|
||||
| Prognose / Forecast | Topologie, Telemetrie und Netzfahrplan | `license.enelix.ch/api/v1/installations/{id}/prognosis/*` |
|
||||
| Lizenzierung | Pruefung freigeschalteter Manager-Funktionen | `POST https://license.enelix.ch/api/v1/licenses/activate` |
|
||||
| Stoerueberwachung | Vollsnapshot aktiver Manager- und Geraetestoerungen | `PUT https://license.enelix.ch/api/v1/installations/{id}/faults` |
|
||||
|
||||
## Stoerungsdatenfluss
|
||||
|
||||
1. Jedes Geraetemodul meldet seinen Zustand ueber den bestehenden
|
||||
Verbraucher-Nachrichtenvertrag an den Manager.
|
||||
2. Der Manager sammelt aktive Eintraege mit `Art = Stoerung`, fuegt eigene
|
||||
Regelungsstoerungen hinzu und normalisiert sie.
|
||||
3. Der Manager speichert lokal einen deterministisch sortierten Vollsnapshot.
|
||||
4. Bei Zustandsaenderung oder spaetestens im konfigurierten Heartbeat-Intervall
|
||||
sendet er den Snapshot mit dem vorhandenen Geraete-Bearer-Token.
|
||||
5. Ein leerer Snapshot loest zuvor aktive Stoerungen im Portal auf.
|
||||
|
||||
Jede Meldung besteht aus `sourceType`, `sourceId`, `sourceName`, `code`,
|
||||
`severity` und `message`. Die stabile Kombination aus `sourceId` und
|
||||
`code` identifiziert ein Ereignis ueber mehrere Snapshots hinweg.
|
||||
|
||||
Fehler der externen Uebertragung blockieren die lokale EMS-Regelung nicht. Der
|
||||
Manager wiederholt mit exponentiellem Abstand zwischen 30 und 900 Sekunden.
|
||||
Nach HTTP 401 oder 403 verwirft er den Geraetezugang und fordert ihn bei der
|
||||
naechsten Lizenzaktivierung neu an.
|
||||
|
||||
## Lizenzvorbereitung
|
||||
|
||||
Die Stoerueberwachung ist technisch als separat schaltbare Manager-Funktion
|
||||
gekapselt. Das Portal kann spaeter eine Berechtigung mit dem Katalogschluessel
|
||||
`fault_monitoring`, einer Laufzeit von einem Jahr und einem erneuerten
|
||||
`validUntil` ausliefern. In der ersten Ausbaustufe wird diese zusaetzliche
|
||||
Jahresberechtigung noch nicht erzwungen; Voraussetzung bleibt eine gueltige
|
||||
Managerlizenz samt Geraetezugang.
|
||||
|
||||
## Gemeinsame Grundsaetze
|
||||
|
||||
- Anbieterformate werden am Manageranschluss uebersetzt und gelangen nicht in
|
||||
die Verbraucher-Schnittstelle.
|
||||
- Jeder Anschluss meldet `NichtVerwendet`, `Wartet`, `Verbunden` oder
|
||||
`Fehler`.
|
||||
- Fehlende optionale Anschluesse duerfen die lokale EMS-Grundfunktion nicht
|
||||
blockieren.
|
||||
- Zugangstoken werden weder als Property noch in Diagnosevariablen oder Logs
|
||||
ausgegeben.
|
||||
## Noch festzulegen
|
||||
|
||||
- Aufbau, Gueltigkeitszeitraum und Rueckmeldung eines SDL-Auftrags
|
||||
- Produkt- und Zahlungsmodell der jaehrlichen Stoerueberwachungslizenz
|
||||
- Verhalten nach Ablauf einer spaeter aktivierten Stoerueberwachungslizenz
|
||||
@@ -0,0 +1,122 @@
|
||||
# Offene Punkte
|
||||
|
||||
Diese Datei ist die zentrale, repositoryuebergreifende Liste fuer Themen, die das
|
||||
Enelix-Team noch entscheiden muss. Sie dient als Arbeitsgrundlage fuer
|
||||
Sprintmeetings. Modulinterne Implementierungsdetails bleiben in der jeweiligen
|
||||
Moduldokumentation.
|
||||
|
||||
## Arbeitsweise
|
||||
|
||||
- Neue Punkte erhalten fortlaufende IDs im Format `OP-001`.
|
||||
- Erlaubte Status sind `Offen`, `In Klaerung`, `Entschieden` und `Zurueckgestellt`.
|
||||
- Ein Eintrag beschreibt die konkrete Entscheidungsfrage, nicht nur ein Stichwort.
|
||||
- Entscheidungen bleiben in dieser Datei erhalten und verweisen auf ADR, Issue oder Commit.
|
||||
- Neue Punkte werden standardmaessig ohne Verantwortlichen und Ziel-Sprint erfasst.
|
||||
- Im Sprintmeeting werden Status, Verantwortlicher und Ziel-Sprint gepflegt.
|
||||
|
||||
## Aufnahmeschranke fuer offene Punkte und Issues
|
||||
|
||||
Ein neuer Eintrag wird nur angelegt, wenn er konkret, abgrenzbar und pruefbar ist.
|
||||
|
||||
- Eine offene Entscheidung nennt eine konkrete Entscheidungsfrage, ihren Kontext und die Auswirkung der Entscheidung.
|
||||
- Ein Bug beschreibt betroffenes Verhalten, Ist- und Sollzustand sowie eine nachvollziehbare Ausloesung.
|
||||
- Ein Feature oder Task nennt Anwendungsfall, Ausloeser oder Eingabe, erwartetes Verhalten und ein pruefbares Ergebnis.
|
||||
- Ein Eintrag behandelt genau ein zusammenhaengendes Thema und kann unabhaengig umgesetzt oder entschieden werden.
|
||||
- Zu grosse oder gebuendelte Anforderungen werden vor der Aufnahme in kleinere, einzeln pruefbare Punkte zerlegt.
|
||||
|
||||
Abstrakte Ziele, Visionen und Loesungsideen ohne konkretes Verhalten werden nicht aufgenommen.
|
||||
Die Rueckmeldung lautet in diesem Fall sinngemaess:
|
||||
|
||||
> Nicht aufgenommen: Der Punkt ist noch zu abstrakt. Bitte in konkrete,
|
||||
> einzeln ausfuehrbare und pruefbare Probleme, Entscheidungen oder Schritte unterteilen.
|
||||
|
||||
Beispiel fuer einen ausreichend konkreten Ausgangspunkt:
|
||||
|
||||
> Die Temperatureinheit des Boilers kann zwischen Celsius und Fahrenheit
|
||||
> eingestellt werden; Anzeige und Grenzwerte verwenden die gewaehlte Einheit.
|
||||
|
||||
Beispiel fuer einen noch zu abstrakten Punkt:
|
||||
|
||||
> Der Manager soll aufgrund einer Prognose regeln.
|
||||
|
||||
Hier fehlen unter anderem Prognoseart, geregelte Verbraucher, konkretes Regelziel,
|
||||
Prioritaeten, Fehlerverhalten und ein pruefbares Ergebnis.
|
||||
|
||||
## Uebersicht
|
||||
|
||||
| ID | Status | Bereich | Kurzthema |
|
||||
| --- | --- | --- | --- |
|
||||
| OP-001 | Entschieden | Lizenzierung | Betriebsort des Lizenzservers |
|
||||
| OP-002 | Erledigt | Grundeinrichtung | Generator und kostenpflichtige Ersteinrichtung |
|
||||
| OP-003 | Offen | Lizenzportal | VGT-Integration in license.enelix.ch |
|
||||
| OP-004 | Entschieden | Lizenzierung | Lizenzmodell des Verbrauchskostenreports |
|
||||
| OP-005 | Offen | Teststrategie | Standort fuer Feldtests |
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
### OP-003: VGT-Integration in license.enelix.ch
|
||||
|
||||
- **Entscheidungsfrage:** Soll die VGT-Anwendung als Bestandteil in die
|
||||
Oberflaeche von `license.enelix.ch` integriert werden?
|
||||
- **Kontext:** Zu klaeren ist, ob die VGT-Anwendung innerhalb der Oberflaeche von
|
||||
`license.enelix.ch` oder getrennt davon bereitgestellt wird.
|
||||
- **Auswirkung:** Die Entscheidung legt fest, ob die VGT-Anwendung Teil dieser
|
||||
Oberflaeche oder eine eigenstaendige Anwendung bleibt.
|
||||
- **Verantwortlich:** Offen
|
||||
- **Ziel-Sprint:** Offen
|
||||
- **Ergebnis/Verweis:** Offen
|
||||
|
||||
### OP-005: Standort fuer Feldtests
|
||||
|
||||
- **Entscheidungsfrage:** An welchem Standort koennen die Module aus Enelix EMS
|
||||
und Enelix Utils unter realen Einsatzbedingungen getestet werden?
|
||||
- **Kontext:** Fuer die Module aus beiden Repositories soll ein geeigneter
|
||||
Standort fuer Feldtests festgelegt werden.
|
||||
- **Auswirkung:** Die Entscheidung legt fest, wo die praktische Validierung der
|
||||
Module unter realen Einsatzbedingungen stattfindet.
|
||||
- **Verantwortlich:** Offen
|
||||
- **Ziel-Sprint:** Offen
|
||||
- **Ergebnis/Verweis:** Offen
|
||||
|
||||
## Entschiedene Punkte
|
||||
|
||||
### OP-001: Betriebsort des Lizenzservers
|
||||
|
||||
- **Entscheidung:** Der Entwicklungs-Lizenzdienst wird unter
|
||||
`https://license.enelix.ch` betrieben.
|
||||
- **Manager-Endpunkt:** `POST /api/v1/licenses/activate`
|
||||
- **Ergebnis/Verweis:** [Obere Anschluesse](Obere-Anschluesse.md) und
|
||||
[Manager](module/Manager/README.md)
|
||||
|
||||
### OP-002: Generator und kostenpflichtige Ersteinrichtung
|
||||
|
||||
- **Entscheidung:** Der automatische Systemgenerator und die kostenpflichtige
|
||||
Ersteinrichtung sind derselbe Vorgang.
|
||||
- **Abrechnung:** Eine Bestellung aus dem Systemkonfigurator enthaelt
|
||||
automatisch die noch nicht bezahlten Einrichtungskosten fuer den Manager und
|
||||
die konfigurierten Verbrauchermodule. Bereits bezahlte Einrichtungsmengen
|
||||
werden je Anlage angerechnet. Direkte Lizenzbestellungen enthalten keine
|
||||
Einrichtungskosten.
|
||||
- **Status:** Erledigt
|
||||
- **Ergebnis/Verweis:** Umsetzung im Lizenzportal unter
|
||||
`https://license.enelix.ch`.
|
||||
|
||||
### OP-004: Lizenzmodell des Verbrauchskostenreports
|
||||
|
||||
- **Entscheidung:** Der Verbrauchskostenreport verwendet eine Grundlizenz sowie
|
||||
getrennte Kontingente fuer Stromzaehler und Nebenzaehler. Die Anzahl der
|
||||
konfigurierten Zaehler wird bei der Lizenzpruefung beruecksichtigt.
|
||||
- **Status:** Entschieden und implementiert
|
||||
- **Ergebnis/Verweis:** Enelix Utils, Commit `1f945be` und
|
||||
`Verbrauchskostenreport/module.php`.
|
||||
|
||||
## Vorlage fuer neue Punkte
|
||||
|
||||
```markdown
|
||||
### OP-NNN: Kurztitel
|
||||
|
||||
- **Entscheidungsfrage:** Welche konkrete Entscheidung muss das Team treffen?
|
||||
- **Verantwortlich:** Offen
|
||||
- **Ziel-Sprint:** Offen
|
||||
- **Ergebnis/Verweis:** Offen
|
||||
```
|
||||
@@ -0,0 +1,112 @@
|
||||
# Schnittstelle Batterie
|
||||
|
||||
Diese Beschreibung ergaenzt den allgemeinen
|
||||
[Manager-Verbraucher-Vertrag](Schnittstelle.md) fuer das Batteriemodul.
|
||||
Vertragsversion ist 4.0.
|
||||
|
||||
## Manager an Batterie
|
||||
|
||||
Die Batterie empfaengt den unveraenderten gemeinsamen Datensatz:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"Kopf": {
|
||||
"Version": "4.0",
|
||||
"AbsenderID": 10001,
|
||||
"EmpfaengerID": 20001,
|
||||
"Zeitpunkt": 1788825600
|
||||
},
|
||||
"Betriebsart": "PV",
|
||||
"Sollleistung_W": -1500
|
||||
}
|
||||
~~~
|
||||
|
||||
Sollleistung_W verwendet folgende Semantik:
|
||||
|
||||
- positiver Wert: Batterie laden
|
||||
- negativer Wert: Batterie entladen
|
||||
- 0: Leistungsregister auf 0 setzen
|
||||
- null: nur Betriebsart synchronisieren und neues Angebot anfordern
|
||||
|
||||
Ein konkreter Wert muss im zuletzt fuer dieselbe Betriebsart gemeldeten
|
||||
Einzelwert oder Leistungsbereich enthalten sein. Waehrend der Aenderungssperre
|
||||
wird nur die Wiederholung des aktuellen Sollwerts akzeptiert.
|
||||
|
||||
## Batterie an Manager
|
||||
|
||||
Die Batterie verwendet alle Pflichtfelder des gemeinsamen Vertrags und
|
||||
ergaenzt folgende Zustandseintraege:
|
||||
|
||||
| Kennung | Art | Typ | Einheit | Bedeutung |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Sollleistung_W | Sollwert | Integer oder null | W | Aktuell gueltige Manager-Vorgabe |
|
||||
| Ladezustand_Prozent | Istwert | Float oder null | % | Physischer SoC |
|
||||
| HystereseAktiv | Status | Boolean | - | Reserve-Hysterese ist aktiv |
|
||||
| Batteriesteuerung | Status | Integer | - | 1 Wechselrichter, 2 Enelix |
|
||||
| Messwertfehler | Stoerung | Boolean | - | Pflichtmesswert fehlt oder ist ungueltig |
|
||||
| Registerfehler | Stoerung | Boolean | - | Mindestens ein Schreibbefehl ist fehlgeschlagen |
|
||||
|
||||
Beispiel:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"Kopf": {
|
||||
"Version": "4.0",
|
||||
"AbsenderID": 20001,
|
||||
"EmpfaengerID": 10001,
|
||||
"Zeitpunkt": 1788825602
|
||||
},
|
||||
"Betriebsart": "PV",
|
||||
"PrioritaetPV": 0,
|
||||
"PrioritaetPeak": 0,
|
||||
"Leistungswerte_W": [{"Von_W": -5000, "Bis_W": 5000}],
|
||||
"AenderungMoeglich": true,
|
||||
"Verfuegbar": true,
|
||||
"Istleistung_W": -250.0,
|
||||
"Leistungsquelle": 2,
|
||||
"Zustand": [
|
||||
{
|
||||
"Kennung": "Sollleistung_W",
|
||||
"Art": "Sollwert",
|
||||
"Wert": -537,
|
||||
"Einheit": "W"
|
||||
},
|
||||
{
|
||||
"Kennung": "Ladezustand_Prozent",
|
||||
"Art": "Istwert",
|
||||
"Wert": 54.2,
|
||||
"Einheit": "%"
|
||||
}
|
||||
]
|
||||
}
|
||||
~~~
|
||||
|
||||
Ist die Leistungsmessung ungueltig, wird Istleistung_W als null und
|
||||
Leistungsquelle als 0 gemeldet. Bei einem gueltigen Messwert ist
|
||||
Leistungsquelle 2. Frei regelbare Lade- und Entladeangebote werden als
|
||||
inklusive Bereiche in ganzen Watt gemeldet. Feste Schutz- und Peakvorgaben
|
||||
bleiben einzelne Leistungswerte.
|
||||
|
||||
## Ereignisse
|
||||
|
||||
Das Modul registriert VM_UPDATE fuer:
|
||||
|
||||
- maximale Ladeleistung
|
||||
- maximale Entladeleistung
|
||||
- Ladezustand
|
||||
- Netzleistung
|
||||
- aktuelle Batterieleistung
|
||||
|
||||
Jede Aktualisierung berechnet Zustand und Angebot neu und plant eine
|
||||
gebuendelte Rueckmeldung. Registerwerte werden nur geschrieben, wenn sich der
|
||||
resultierende Befehl geaendert hat.
|
||||
|
||||
## Registerausgang
|
||||
|
||||
Der technische Ausgang besteht aus ausgewaehlten numerischen
|
||||
IP-Symcon-Variablen. Das Modul verwendet RequestAction und setzt diese
|
||||
Variablen nicht mit SetValue. Damit bleibt der jeweilige Modbus-, Skript- oder
|
||||
Geraeteadapter fuer die konkrete Registerkommunikation verantwortlich.
|
||||
|
||||
Die genaue Herstellerabbildung steht in der
|
||||
[Moduldokumentation](module/Batterie/README.md).
|
||||
@@ -0,0 +1,103 @@
|
||||
# Easee-Gateway-Schnittstelle
|
||||
|
||||
Stand: 2026-09-22
|
||||
|
||||
Die Schnittstelle verbindet das kontobezogene Splittermodul `EaseeGateway`
|
||||
mit beliebig vielen Kindinstanzen `LadestationGateway`.
|
||||
|
||||
## IP-Symcon-Daten-IDs
|
||||
|
||||
| Richtung | DataID |
|
||||
| --- | --- |
|
||||
| Ladestation an Gateway | `{7AEF3DF7-DA5B-47C5-BCDC-0110D06DDC04}` |
|
||||
| Gateway an Ladestation | `{107D5CFA-8F3D-4E38-8643-90DDFE6A3B4D}` |
|
||||
|
||||
Die aeussere IP-Symcon-Nachricht enthaelt `DataID` und `Buffer`. `Buffer`
|
||||
ist wiederum ein JSON-Objekt.
|
||||
|
||||
## Anfragen an das Gateway
|
||||
|
||||
### Subscribe und GetState
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "Subscribe",
|
||||
"serialNumber": "EH123456"
|
||||
}
|
||||
```
|
||||
|
||||
`GetState` besitzt dasselbe Format. Beide Aktionen registrieren die
|
||||
Seriennummer und liefern den Cache:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"connected": true,
|
||||
"state": {
|
||||
"109": 3,
|
||||
"110": 30,
|
||||
"120": 11.0,
|
||||
"updated": 1790053200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SetDynamicChargerCurrent
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "SetDynamicChargerCurrent",
|
||||
"serialNumber": "EH123456",
|
||||
"amps": 13
|
||||
}
|
||||
```
|
||||
|
||||
Zulaessig sind `0 A` oder ganzzahlige Werte von `6 A` bis `32 A`.
|
||||
Das Gateway ruft
|
||||
`POST /api/chargers/{serialNumber}/commands/set_dynamic_charger_current`
|
||||
mit `{"amps":13,"minutes":0}` auf.
|
||||
|
||||
## Ereignisse an Kindinstanzen
|
||||
|
||||
### Observation
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Observation",
|
||||
"serialNumber": "EH123456",
|
||||
"id": 110,
|
||||
"value": 30,
|
||||
"timestamp": 1790053200
|
||||
}
|
||||
```
|
||||
|
||||
Verteilt werden die IDs `47`, `48`, `100`, `104`, `109`, `110`,
|
||||
`119`, `120`, `182` bis `185` und `250`. Jede Kindinstanz verwirft
|
||||
Ereignisse anderer Seriennummern.
|
||||
|
||||
### GatewayStatus
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "GatewayStatus",
|
||||
"connected": false
|
||||
}
|
||||
```
|
||||
|
||||
Bei `false` setzt die Ladestation Verfuegbarkeit und Aenderbarkeit sofort
|
||||
zurueck. Bei `true` fordert sie den aktuellen Zustand erneut an.
|
||||
|
||||
## Zustandsabbildung
|
||||
|
||||
Observation `109` wird gemaess Easee OpMode abgebildet: `0` offline,
|
||||
`1` getrennt, `2/6/7/8` bereit, `3` laedt, `4` geladen und `5` Fehler.
|
||||
Unbekannte Werte geben die Regelung nicht frei. Observation `110` liefert die
|
||||
aktive Ausgangsphase: `10..15` einphasig und `30` dreiphasig. Es gibt keine
|
||||
leistungsbasierte Phasenschaetzung.
|
||||
|
||||
## Fehlervertrag
|
||||
|
||||
Gateway-Antworten enthalten immer `success`. Bei `false` folgt ein
|
||||
menschenlesbares Feld `error`; optional wird `httpCode` ergaenzt.
|
||||
Zugangsdaten, Access Token und Refresh Token duerfen weder in Antworten noch
|
||||
in Ereignissen oder Diagnosevariablen vorkommen.
|
||||
+131
-25
@@ -1,6 +1,6 @@
|
||||
# EMS-Schnittstelle
|
||||
|
||||
Vertragsversion: `3.0`
|
||||
Vertragsversion: `4.0`
|
||||
|
||||
Es gibt genau eine fachliche Empfangsmethode je Richtung:
|
||||
|
||||
@@ -13,7 +13,7 @@ public function VerbraucherdatenEmpfangen(array $daten): void;
|
||||
|
||||
| Feld | Typ | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `Version` | Text | Vertragsversion `3.0` |
|
||||
| `Version` | Text | Vertragsversion `4.0` |
|
||||
| `AbsenderID` | Ganzzahl | Sendende Symcon-Instanz |
|
||||
| `EmpfaengerID` | Ganzzahl | Empfangende Symcon-Instanz |
|
||||
| `Zeitpunkt` | Ganzzahl | Unixzeit in UTC |
|
||||
@@ -23,61 +23,167 @@ public function VerbraucherdatenEmpfangen(array $daten): void;
|
||||
```json
|
||||
{
|
||||
"Kopf": {
|
||||
"Version": "3.0",
|
||||
"Version": "4.0",
|
||||
"AbsenderID": 10001,
|
||||
"EmpfaengerID": 20001,
|
||||
"Zeitpunkt": 1788825600
|
||||
},
|
||||
"Betriebsart": "PV",
|
||||
"Sollleistung_W": 1501
|
||||
}
|
||||
```
|
||||
|
||||
`Sollleistung_W` ist immer eine Ganzzahl und muss im aktuell gemeldeten Leistungsangebot liegen.
|
||||
`Betriebsart` ist `PV` oder `Peak`. `Sollleistung_W` ist eine Ganzzahl
|
||||
aus dem fuer diese Betriebsart gemeldeten Leistungsangebot oder `null`.
|
||||
|
||||
`null` kuendigt nur die Betriebsart an. Der Verbraucher uebernimmt sie,
|
||||
berechnet sein Leistungsangebot neu und meldet es zurueck. Eine vorhandene
|
||||
Sollleistung wird dabei nur verworfen, wenn sie im neuen Angebot nicht mehr
|
||||
zulaessig ist.
|
||||
|
||||
### Verworfene Sollwerte und Anzeige
|
||||
|
||||
Beim Verwerfen einer gueltigen Vorgabe setzen alle Verbrauchermodule sowohl
|
||||
`SollwertGueltig=false` als auch den gespeicherten und gegebenenfalls sichtbaren
|
||||
Wert `Sollleistung=0`. An den Manager geht weiterhin `Sollleistung_W=null`;
|
||||
die Anzeige 0 W ist keine neue gueltige Manager-Vorgabe und kein Nachweis einer
|
||||
physisch ausgeschalteten Last.
|
||||
|
||||
Lokale Schutzprogramme, Mindestlaufzeiten und bestehende Lade-Uebergaenge
|
||||
bleiben unveraendert. Berechnet ein Modul danach eine lokale Sollleistung,
|
||||
darf es diese weiterhin anzeigen. Bei Ladestationen bleibt waehrend eines
|
||||
Regeluebergangs insbesondere der letzte Geraetebefehl erhalten, auch wenn die
|
||||
alte Manager-Vorgabe bereits verworfen und ihre Anzeige auf 0 gesetzt wurde.
|
||||
Wiederholte Verwerfungen einer bereits ungueltigen Manager-Vorgabe lassen eine
|
||||
inzwischen neu berechnete lokale Schutzleistung unveraendert.
|
||||
|
||||
Nach einem Modulupdate bereinigt `ApplyChanges` bereits gespeicherte ungueltige
|
||||
Altwerte. Gueltige Vorgaben einschliesslich 0 W und negativer Batterieleistung
|
||||
bleiben erhalten. Keine neuen Properties, Variablen-IDs oder Vertragsversion;
|
||||
es ist keine manuelle Konfigurationsmigration erforderlich. Das Update ist
|
||||
kontrolliert anzuwenden, da die regulaere Instanzinitialisierung wie bisher
|
||||
Geraeteaktionen ausloesen kann.
|
||||
|
||||
## Verbraucher an Manager
|
||||
|
||||
```json
|
||||
{
|
||||
"Kopf": {
|
||||
"Version": "3.0",
|
||||
"Version": "4.0",
|
||||
"AbsenderID": 20001,
|
||||
"EmpfaengerID": 10001,
|
||||
"Zeitpunkt": 1788825602
|
||||
},
|
||||
"Betriebsart": "Peak",
|
||||
"PrioritaetPV": 0,
|
||||
"PrioritaetPeak": 0,
|
||||
"Leistungswerte_W": [
|
||||
-3000,
|
||||
-2000,
|
||||
{"Von_W": -1000, "Bis_W": -500},
|
||||
0,
|
||||
100,
|
||||
{"Von_W": 1000, "Bis_W": 2000},
|
||||
3000
|
||||
],
|
||||
"AenderungMoeglich": true,
|
||||
"Leistungswerte_W": [0],
|
||||
"AenderungMoeglich": false,
|
||||
"Verfuegbar": true,
|
||||
"Istleistung_W": 1498.5,
|
||||
"Leistungsquelle": 2,
|
||||
"Istleistung_W": 0,
|
||||
"Leistungsquelle": 1,
|
||||
"Zustand": [
|
||||
{
|
||||
"Kennung": "Sollleistung_W",
|
||||
"Art": "Sollwert",
|
||||
"Wert": 1501,
|
||||
"Wert": 0,
|
||||
"Einheit": "W"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Feste Regeln
|
||||
## Betriebsart-Synchronisation
|
||||
|
||||
- Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet eine hoehere Prioritaet.
|
||||
1. Der Manager bestimmt `PV` oder `Peak`.
|
||||
2. Meldungen einer anderen Betriebsart werden nicht zur Verteilung verwendet.
|
||||
3. Der Manager sendet diesen Verbrauchern eine Betriebsart-Ankuendigung mit
|
||||
`Sollleistung_W=null`.
|
||||
4. Jeder Verbraucher berechnet und meldet seine PowerSteps fuer diese
|
||||
Betriebsart.
|
||||
5. Der Manager verteilt Sollleistungen an alle bereits synchronisierten
|
||||
Verbraucher.
|
||||
6. Fehlende, veraltete oder noch nicht umgeschaltete Verbraucher werden nicht
|
||||
angesteuert und als Stoerung ausgewiesen; sie blockieren die aktuellen
|
||||
Verbraucher nicht.
|
||||
|
||||
Damit kann jeder Verbrauchertyp unterschiedliche Angebote fuer PV und Peak
|
||||
melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
|
||||
|
||||
## Feste Regeln
|
||||
|
||||
- Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet hoehere Prioritaet.
|
||||
- Jeder verfuegbare, synchronisierte Verbraucher mit einem nicht leeren
|
||||
`Leistungswerte_W`-Angebot erhaelt einen Sollwert aus genau diesem Angebot.
|
||||
- `AenderungMoeglich=false` kennzeichnet ein fixes Angebot und ist kein
|
||||
Ausschlussgrund. Ein Angebot `[11000]` fuehrt deshalb zwingend zu `11000 W`.
|
||||
- Prioritaeten verteilen nur die ueber den jeweiligen Mindestwert hinaus
|
||||
verfuegbare Leistung; sie duerfen keinen angebotenen Mindestwert verdrängen.
|
||||
- `Leistungsquelle`: 0 nicht vorhanden, 1 berechnet, 2 gemessen.
|
||||
- Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`.
|
||||
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W` einschliesslich.
|
||||
- Die Leistungswerte sind aufsteigend, eindeutig und ueberschneiden sich nicht.
|
||||
- `Zustand` enthaelt immer den gemeinsamen Eintrag `Sollleistung_W` und daneben nur benoetigte geraetespezifische Eintraege.
|
||||
- Eine aktive Stoerung wird mit `Art=Stoerung` und `Wert=true` gemeldet; eine behobene mit `false`.
|
||||
- Verbraucher werden ausschliesslich im Manager zugeordnet. Der Verbraucher besitzt keine Manager-ID-Property.
|
||||
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W`.
|
||||
- Leistungswerte sind aufsteigend, eindeutig und ueberschneiden sich nicht.
|
||||
- `Zustand` enthaelt immer `Sollleistung_W`.
|
||||
- Verbraucher werden ausschliesslich im Manager zugeordnet.
|
||||
- Der Verbraucher besitzt keine Manager-ID-Property.
|
||||
|
||||
## Technische Umsetzung in IP-Symcon
|
||||
|
||||
Der Transport erfolgt ueber `IPS_RequestAction` mit JSON. `MessageSink`
|
||||
erkennt registrierte Aenderungen. Empfang und Neuberechnung sind intern
|
||||
entkoppelt, damit keine gegenseitige Endlosschleife entsteht.
|
||||
|
||||
Das PHP-Interface legt nur die Empfangsmethode fest. Die gemeinsamen
|
||||
Symcon-Datenpunkte registriert `VerbraucherBasisTrait`.
|
||||
|
||||
### Gemeinsame Properties aller Verbraucher
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | --- | --- |
|
||||
| `PrioritaetPV` | Integer | `0` | Prioritaet in der Betriebsart PV |
|
||||
| `PrioritaetPeak` | Integer | `0` | Prioritaet in der Betriebsart Peak |
|
||||
| `Meldeintervall` | Integer | `10` | Vollstaendige Rueckmeldung in Sekunden |
|
||||
| `VorgabeTimeout` | Integer | `120` | Ablaufzeit einer Sollleistung |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Lokale Einstellungen in der Visualisierung |
|
||||
| `LoggingEin` | Boolean | `false` | Laufendes Diagnoseprotokoll |
|
||||
|
||||
### Gemeinsame Variablen aller Verbraucher
|
||||
|
||||
| Ident | Typ / Zugriff | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Start `false` |
|
||||
| `Istleistung` | Float / Anzeige | Aktuelle Leistung in W |
|
||||
| `Leistungsquelle` | Integer / Anzeige | 0 nicht vorhanden, 1 berechnet, 2 gemessen |
|
||||
| `Sollleistung` | Integer / Anzeige | Angenommene oder lokal erzwungene Vorgabe |
|
||||
| `SollwertGueltig` | Boolean / Anzeige | Aktuelle, nicht abgelaufene Vorgabe |
|
||||
| `Verfuegbar` | Boolean / Anzeige | Verbraucher grundsaetzlich verfuegbar |
|
||||
| `AenderungMoeglich` | Boolean / Anzeige | Neue Vorgabe darf uebernommen werden |
|
||||
| `Stoerung` | Boolean / Anzeige | Mindestens eine Stoerung aktiv |
|
||||
| `Stoertext` | String / Anzeige | Zusammengefasste Stoerbeschreibung |
|
||||
|
||||
`Leistungswerte_W`, `Betriebsart` und `Zustand` werden intern gehalten
|
||||
und direkt in die Nachricht geschrieben.
|
||||
|
||||
## Batteriespezifische Erweiterung
|
||||
|
||||
Die Batterie verwendet denselben Vertrag 4.0 und ergaenzt Zustandseintraege
|
||||
fuer Ladezustand, Hysterese, Steuerungsmodus, Messwertfehler und
|
||||
Registerfehler. Positive Leistung bedeutet Laden, negative Leistung
|
||||
Entladen. Die vollstaendige Semantik und Registeranbindung beschreibt die
|
||||
[Schnittstelle Batterie](Schnittstelle-Batterie.md).
|
||||
|
||||
## Easee-Gateway-Transport
|
||||
|
||||
Der technische JSON-Vertrag zwischen `EaseeGateway` und
|
||||
`LadestationGateway` ist getrennt vom fachlichen Managervertrag dokumentiert:
|
||||
[Easee-Gateway-Schnittstelle](Schnittstelle-Easee-Gateway.md). Die
|
||||
Ladestation uebersetzt Gateway-Ereignisse in den hier beschriebenen
|
||||
Verbrauchervertrag `4.0`.
|
||||
|
||||
## Zeitverhalten
|
||||
|
||||
- Rueckmeldung nach Start, relevanten Aenderungen und alle `Meldeintervall`
|
||||
Sekunden.
|
||||
- Laufende Vorgaben werden vom Manager standardmaessig erneuert.
|
||||
- Nach `VorgabeTimeout` ist eine nicht erneuerte Vorgabe ungueltig.
|
||||
- Nach einem Neustart wird keine alte Vorgabe ungeprueft aufgenommen.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# ADR 0001: Warmwassererwaermer verwendet ausschliesslich Vertrag 3.0
|
||||
|
||||
## Status
|
||||
|
||||
Akzeptiert.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
Der neue `VerbraucherWarmwassererwaermer` implementiert die bestehende
|
||||
`VerbraucherSchnittstelle` und den `Nachrichtenvertrag` in Version `3.0`.
|
||||
Die Enelix-1-Variablen `Power`, `PowerSteps`, `PV_Prio`, `Sperre_Prio`,
|
||||
`Is_Peak_Shaving` und `Leistung_Delta` werden nicht als zweite parallele
|
||||
Manager-Schnittstelle weitergefuehrt.
|
||||
|
||||
Der Manager bleibt allein fuer die Betriebsart und Leistungsverteilung
|
||||
verantwortlich. Lokale Temperatur- und Hygienesicherheit wird als eingeschraenktes
|
||||
Leistungsangebot mit `AenderungMoeglich=false` gemeldet.
|
||||
|
||||
## Folgen
|
||||
|
||||
- Es gibt genau eine fachliche Empfangsmethode pro Richtung.
|
||||
- Der Verbraucher hat keine Manager-ID-Property.
|
||||
- Alte Instanzen werden anhand der Migrationstabelle neu konfiguriert.
|
||||
- Manager und Verbraucher koennen unabhaengig getestet werden.
|
||||
@@ -0,0 +1,74 @@
|
||||
# ADR 0002: Verbraucher 1-Stufig arbeitet ereignisbasiert
|
||||
|
||||
## Status
|
||||
|
||||
Akzeptiert, am 17. September 2026 um getrennte Mindestzeiten erweitert.
|
||||
|
||||
## Kontext
|
||||
|
||||
Das Enelix-1-Modul berechnete seinen Zustand in einem festen Intervall und
|
||||
bildete Lastwechselsperren ueber `Interval`, `IdleCounterMax` und weitere
|
||||
zyklusabhaengige Zaehler ab. Ein erster Enelix-2-Stand ersetzte dies durch
|
||||
einen allgemeinen `Umschaltabstand`. Dieser konnte unterschiedliche
|
||||
Geraeteanforderungen fuer Ein- und Aus-Zustand nicht ausdruecken.
|
||||
|
||||
Bei einem asynchron schaltenden Geraet kann ausserdem der Aktorbefehl nicht als
|
||||
physische Zustandsbestaetigung gelten.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
Der `VerbraucherEinStufig` verwendet keine zyklische Regelberechnung.
|
||||
Schaltkontakt, Rueckmeldung, Manager-Vorgabe, Freigabe, Vorgabeablauf und
|
||||
Tagesplanung loesen die Regelung direkt aus.
|
||||
|
||||
Der allgemeine `Umschaltabstand` entfaellt. An seine Stelle treten:
|
||||
|
||||
- `Mindesteinschaltdauer` in Sekunden, Standard 5,
|
||||
- `Mindestausschaltdauer` in Sekunden, Standard 5.
|
||||
|
||||
Die passende Mindestzeit beginnt mit dem bestaetigten Zustandswechsel. Ohne
|
||||
separate Rueckmeldung bestaetigt die Aktorvariable den Wechsel unmittelbar.
|
||||
Mit separater Rueckmeldung beginnt die Mindestzeit erst, wenn deren Wert den
|
||||
Zielzustand erreicht.
|
||||
|
||||
Bis dahin bleiben Istleistung und Leistungsangebot beim rueckgemeldeten
|
||||
Zustand. `SchaltbefehlAusstehend=true`,
|
||||
`AenderungMoeglich=false` und `Schaltbereit=false` machen die laufende
|
||||
Umschaltung fuer den Manager sichtbar.
|
||||
|
||||
Das gemeinsame `Meldeintervall` bleibt erhalten, weil Vertrag `3.0`
|
||||
zusaetzlich zu Ereignismeldungen eine periodische Vollmeldung fordert. Dieser
|
||||
Timer ist kein Regelzyklus.
|
||||
|
||||
Die Tagesmindestlaufzeit wird in realen Sekunden und lokaler Symcon-Zeitzone
|
||||
gezaehlt. Bei separater Rueckmeldung zaehlt ausschliesslich der bestaetigte
|
||||
Ein-Zustand.
|
||||
|
||||
## Alternativen
|
||||
|
||||
- Ein fester Regelzyklus wurde verworfen, weil Reaktionszeit und Zeitregeln
|
||||
wieder voneinander abhaengen wuerden.
|
||||
- Ein allgemeiner Umschaltabstand wurde verworfen, weil Ein- und Aus-Zustand
|
||||
unterschiedliche Mindestzeiten benoetigen koennen.
|
||||
- Der Aktorbefehl als sofortige physische Bestaetigung wurde bei vorhandener
|
||||
Rueckmeldung verworfen.
|
||||
- Ein zusaetzlicher Rueckmelde-Timeout wurde nicht eingefuehrt. Ein
|
||||
ausstehender Befehl bleibt transparent sichtbar, bis ein neues Ereignis den
|
||||
Zustand klaert.
|
||||
|
||||
## Folgen
|
||||
|
||||
- Lastwechsel reagieren ohne Polling auf relevante Ereignisse.
|
||||
- `Interval`, `IdleCounterMax` und `Umschaltabstand` entfallen.
|
||||
- Waerend Mindestzeiten und ausstehenden Rueckmeldungen wird nur die
|
||||
bestaetigte Istleistung angeboten.
|
||||
- Der Manager erhaelt Status und Restmindestzeit als Zustandseintraege.
|
||||
- Eine unerwartete Abweichung zwischen Aktor und Rueckmeldung ausserhalb eines
|
||||
laufenden Schaltvorgangs wird als Stoerung gemeldet.
|
||||
- Tageslaufzeit und Mindestzeiten sind unabhaengig von einer Zyklusdauer.
|
||||
- Das Modul bleibt vollstaendig im Repository Enelix EMS.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Eine spaetere sperrbare Variante wird separat spezifiziert.
|
||||
- Ein konfigurierbarer Rueckmelde-Timeout benoetigt eine eigene Entscheidung.
|
||||
@@ -0,0 +1,43 @@
|
||||
# ADR 0003: Standardisierte Teststrategie
|
||||
|
||||
## Kontext
|
||||
|
||||
Die Module besitzen PHPUnit-Tests, ihre Prüfungen in einer echten
|
||||
IP-Symcon-Installation waren jedoch unterschiedlich aufgebaut. Dadurch fehlten
|
||||
ein einheitlicher Aufruf, sicherer Cleanup, maschinenlesbare Berichte und eine
|
||||
verbindliche Regel für neue Module.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
Enelix verwendet zwei Testebenen. PHPUnit bleibt die schnelle Pflichtprüfung bei
|
||||
jedem Push. Zusätzlich erhält jedes Modul einen registrierten Symcon-Modultest
|
||||
mit dem gemeinsamen `TestContext`. Der Runner unterstützt `single`,
|
||||
`affected` und `all`, isoliert jeden Testlauf und liefert Konsole, JSON und
|
||||
JUnit XML.
|
||||
|
||||
Der Manager-Test erzeugt jeden implementierten Verbrauchertyp und prüft ihn
|
||||
einzeln sowie in einer gemeinsamen Konstellation. Neue Verbrauchertypen müssen
|
||||
diese Matrix erweitern.
|
||||
|
||||
## Alternativen
|
||||
|
||||
- Nur PHPUnit: verworfen, weil das reale Objektmodell, Actions und
|
||||
Modulinteraktionen nicht abgedeckt werden.
|
||||
- Freie Schnellausführungs-Skripte pro Modul: verworfen, weil Aufbau, Cleanup
|
||||
und Berichte erneut auseinanderlaufen würden.
|
||||
- Ein drittes Test-Repository: vorerst verworfen, weil Tests zusammen mit dem
|
||||
jeweiligen Modul versioniert und atomar geändert werden sollen.
|
||||
|
||||
## Folgen
|
||||
|
||||
Jedes neue Modul benötigt zusätzlich zu Unit-Tests einen Manifest-Eintrag und
|
||||
einen Symcon-Test. Der Vertrags-Unit-Test verhindert unregistrierte Module.
|
||||
Integrationstests benötigen einen isolierten IP-Symcon-8.x-Runner. Testobjekte
|
||||
dürfen ausschließlich innerhalb der vom Framework erzeugten Kategorie liegen.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Bereitstellung und Registrierung des Gitea-Runners mit Label `symcon-8`.
|
||||
- Festlegung der Aufbewahrungsdauer für JSON- und JUnit-Artefakte.
|
||||
- Erweiterung der Manager-Matrix, sobald weitere Verbrauchertypen umgesetzt
|
||||
werden.
|
||||
@@ -0,0 +1,76 @@
|
||||
# ADR 0004: Betriebsartabhaengige Leistungsangebote
|
||||
|
||||
## Kontext
|
||||
|
||||
Der Nachrichtenvertrag 3.0 uebermittelte vom Manager nur
|
||||
`Sollleistung_W`. Verbraucher meldeten ein einziges Leistungsangebot und
|
||||
kannten die aktuelle Betriebsart nicht. Damit konnten Module keine
|
||||
unterschiedlichen PowerSteps fuer PV- und Peakbetrieb bereitstellen.
|
||||
|
||||
Eine einfache Ergaenzung des Sollwertpakets reicht nicht aus: Beim Wechsel der
|
||||
Betriebsart besitzt der Manager zunaechst noch das Angebot der vorherigen
|
||||
Betriebsart. Eine sofortige Verteilung koennte deshalb einen Sollwert erzeugen,
|
||||
den der Verbraucher im neuen Modus ablehnen muss.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
Der Vertrag wird inkompatibel auf Version `4.0` angehoben.
|
||||
|
||||
- Managerdaten und Verbraucherdaten enthalten verpflichtend `Betriebsart`
|
||||
mit dem Wert `PV` oder `Peak`.
|
||||
- `Sollleistung_W` in Managerdaten darf `null` sein.
|
||||
- `null` ist eine reine Betriebsart-Ankuendigung und kein Schaltbefehl.
|
||||
- Der Verbraucher uebernimmt die Betriebsart, berechnet sein Leistungsangebot
|
||||
neu und meldet dieses mit derselben Betriebsart zurueck.
|
||||
- Der Manager verwendet nur Angebote seiner aktuellen Betriebsart.
|
||||
- Solange mindestens ein aktiver Verbraucher nicht synchronisiert ist, erfolgt
|
||||
keine Verteilung.
|
||||
- Nach einem Betriebsartwechsel verwirft der Manager seinen Sollwertcache.
|
||||
|
||||
Die konkrete Differenz zwischen PV- und Peakangebot bleibt Verantwortung des
|
||||
Verbrauchermoduls. Die derzeit implementierten Verbraucher uebernehmen die
|
||||
zustandsabhaengigen Enelix-1-Angebote gezielt:
|
||||
|
||||
- Der einstufige Verbraucher bietet in Peak normalerweise `[0]`. Bei faelliger
|
||||
Tagesmindestlaufzeit ist konfigurierbar, ob `[0, Nennleistung]` angeboten
|
||||
oder nur die Nennleistung erzwungen wird.
|
||||
- Die Ladestation bietet in Peak mit Solarladen `[0]`, ohne Solarladen dagegen
|
||||
`[0, ...Ladestufen]` an.
|
||||
- Der Warmwassererwaermer bietet unter seiner wirksamen Mindesttemperatur auch
|
||||
in Peak `[0, ...Leistungsstufen]` an.
|
||||
- Der Pufferspeicher bietet im Peakbetrieb unabhaengig vom Zustand `[0]` an.
|
||||
- Technische Schaltsperren und ausstehende Rueckmeldungen duerfen das Angebot
|
||||
weiterhin auf die aktuell gehaltene Leistung begrenzen.
|
||||
|
||||
## Alternativen
|
||||
|
||||
### Zwei Angebote gleichzeitig melden
|
||||
|
||||
Separate Felder fuer PV- und Peak-PowerSteps wuerden den Umschalt-Handshake
|
||||
vermeiden. Sie verdoppeln jedoch alle Angebotsdaten und zwingen jedes Modul,
|
||||
beide Zustaende jederzeit parallel zu berechnen.
|
||||
|
||||
### Betriebsart ohne Synchronisation senden
|
||||
|
||||
Der Manager koennte Betriebsart und Sollleistung in einem Paket senden. Das
|
||||
erste Kommando nach einem Wechsel waere dann aus dem alten Angebot berechnet
|
||||
und koennte ungueltig sein.
|
||||
|
||||
### Betriebsart aus einer Manager-Variable lesen
|
||||
|
||||
Eine direkte Objektkopplung wuerde die definierte Schnittstelle umgehen,
|
||||
mehrere Manager erschweren und Verbraucher unnoetig an die Managerinstanz
|
||||
binden.
|
||||
|
||||
## Folgen
|
||||
|
||||
- Alle Manager und Verbraucher einer Installation muessen gemeinsam auf
|
||||
Vertrag 4.0 aktualisiert werden.
|
||||
- Version 3.0 und 4.0 koennen nicht innerhalb derselben Managerzuordnung
|
||||
gemischt werden.
|
||||
- Ein Betriebsartwechsel benoetigt mindestens einen zusaetzlichen
|
||||
Nachrichtenumlauf.
|
||||
- Tests pruefen Vertragsvalidierung, Synchronisation und modulspezifische
|
||||
Angebote fuer PV und Peak.
|
||||
- ADR 0001 bleibt als historische Entscheidung erhalten, wird fuer den
|
||||
aktuellen Vertrag aber durch diese Entscheidung abgeloest.
|
||||
@@ -0,0 +1,76 @@
|
||||
# ADR 0005: Anlagentopologie als Manager-Stammdaten
|
||||
|
||||
## Kontext
|
||||
|
||||
Die Prognose benoetigt technische Stammdaten zu PV-Flaechen,
|
||||
Wechselrichtern und Batteriespeichern. Diese Daten beschreiben die reale
|
||||
Installation und werden auch von der lokalen Regelung benoetigt. Eine
|
||||
unabhaengige Pflege im Prognoseportal wuerde zwei konkurrierende Wahrheiten
|
||||
erzeugen.
|
||||
|
||||
Hybridwechselrichter benoetigen eine ausdrueckliche Topologie. PV und Batterie
|
||||
duerfen nicht als zwei unabhaengige AC-Quellen mit jeweils voller
|
||||
Wechselrichterleistung behandelt werden.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
Der Enelix-Manager ist die fuehrende Quelle fuer die technische
|
||||
Anlagentopologie. Er speichert drei getrennte Listen:
|
||||
|
||||
- Wechselrichter mit Typ, AC-Nennleistung und optionalen AC-Grenzen,
|
||||
- PV-Flaechen mit DC-Leistung, Ausrichtung und Wechselrichterbezug,
|
||||
- Batteriespeicher mit Kapazitaeten, Leistungen, Kopplung und
|
||||
Wechselrichterbezug.
|
||||
|
||||
Jede Komponente besitzt eine innerhalb ihrer Liste eindeutige, stabile ID.
|
||||
PV-Flaechen und Batterien referenzieren einen Wechselrichter ueber diese ID.
|
||||
|
||||
Die AC-Nennleistung gehoert zum Wechselrichter. Die DC-Leistung gehoert zur
|
||||
PV-Flaeche. Modulanzahl und Modulleistung sind optionale Detailangaben und
|
||||
muessen gemeinsam gepflegt werden.
|
||||
|
||||
Bei einem Hybridwechselrichter bleiben PV-Flaechen und Batterie getrennte
|
||||
Komponenten. Der Exportvertrag liefert zusaetzlich eine gemeinsame AC-Grenze,
|
||||
die beide Seiten demselben Wechselrichter zuordnet. Damit darf ihre kombinierte
|
||||
AC-Leistung die Wechselrichtergrenze nicht unabhaengig mehrfach ausschoepfen.
|
||||
|
||||
Der Manager exportiert die normalisierte Struktur mit Vertragsversion '1.0'
|
||||
und seiner stabilen Lizenz-Installations-ID. Eine aggregierte Zusammenfassung
|
||||
erleichtert die schrittweise Anbindung bestehender Prognoseberechnungen.
|
||||
|
||||
Tarife, Prognosevarianten und rein prognosespezifische Annahmen bleiben im
|
||||
Prognoseportal. Dort werden die vom Manager gelieferten technischen Stammdaten
|
||||
standardmaessig nur angezeigt.
|
||||
|
||||
## Alternativen
|
||||
|
||||
### Vollstaendige Pflege im Prognoseportal
|
||||
|
||||
Diese Variante waere fuer den Prognosedienst einfach, erzeugt aber
|
||||
Doppelpflege und kann von der lokal tatsaechlich installierten Anlage
|
||||
abweichen.
|
||||
|
||||
### Batterie und PV als ein Hybridobjekt speichern
|
||||
|
||||
Ein einzelnes Objekt waere kompakt, bildet mehrere PV-Flaechen, MPPT-Eingaenge
|
||||
und spaetere Erweiterungen jedoch schlecht ab. Ausserdem gingen die getrennten
|
||||
Kapazitaets- und Leistungsgrenzen verloren.
|
||||
|
||||
### AC- und DC-Leistung an jeder PV-Flaeche speichern
|
||||
|
||||
Das wuerde bei mehreren Flaechen an einem Wechselrichter die AC-Leistung
|
||||
mehrfach zaehlen. Deshalb wird AC am Wechselrichter und DC an der Flaeche
|
||||
gespeichert.
|
||||
|
||||
## Folgen
|
||||
|
||||
- Bestehende Installationen bleiben mit drei leeren Listen gueltig.
|
||||
- Fehlerhafte Referenzen oder unplausible technische Grenzen sperren die
|
||||
Manager-Konfiguration kontrolliert.
|
||||
- Ein Hybridwechselrichter kann mehrere PV-Flaechen und Batteriespeicher
|
||||
verbinden, besitzt aber nur eine gemeinsame AC-Nennleistung.
|
||||
- Das Prognoseportal muss die technischen Stammdaten aus dem Managervertrag
|
||||
lesen und eigene Eingaben dafuer als schreibgeschuetzt behandeln.
|
||||
- Fuer den automatischen Upload ist ein eigener, widerrufbarer
|
||||
Installationszugang erforderlich. Der Lizenzcode selbst wird nicht als
|
||||
dauerhaftes API-Secret verwendet.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Migration Batterie aus Enelix 1
|
||||
|
||||
## Ziel
|
||||
|
||||
Die bisherige Batterie wird als Enelix-2-Verbraucher auf Vertrag 4.0
|
||||
umgestellt. Das alte Repository bleibt unveraendert und dient nur als
|
||||
Verhaltensreferenz.
|
||||
|
||||
## Uebernommen
|
||||
|
||||
- positive Sollleistung fuer Laden und negative Sollleistung fuer Entladen
|
||||
- dynamische maximale Lade- und Entladeleistung aus Variablen
|
||||
- Ladezustand, Netzleistung und aktuelle Batterieleistung als Eingaben
|
||||
- Reserve-, Mindestladezustands- und Hystereselogik
|
||||
- getrennte Angebote fuer PV und Peak
|
||||
- Wechselrichter- oder Enelix-Steuerung
|
||||
- Registercodes fuer generisch, GoodWe, SolarEdge und Sigenergy
|
||||
- Aufsummierung der bezogenen Energie
|
||||
|
||||
## Angepasst
|
||||
|
||||
| Enelix 1 | Enelix 2 |
|
||||
| --- | --- |
|
||||
| zyklischer Timer Do_UserCalc | Messwert- und Managerereignisse |
|
||||
| IdleCounter in Zyklen | Aenderungssperre in Sekunden mit Einmaltimer |
|
||||
| PowerSteps als Modulvariable | Leistungswerte_W direkt im Vertrag |
|
||||
| 250-W-Grundraster mit 50-W-Feinwerten | inklusive Leistungsbereiche in ganzen Watt |
|
||||
| interne Stellwertvariablen | ausgewaehlte Registervariablen mit RequestAction |
|
||||
| fest verdrahtete Herstellerhilfsvariablen | dynamisch sichtbare Registerauswahl |
|
||||
| fest codierte 2-%-Hysterese | Property LadezustandHysterese |
|
||||
| berechnete Istleistung | gemessene Istleistung, Leistungsquelle 2 |
|
||||
| Interval-basierte Energie | Zeitintegration zwischen Ereignissen und Vollmeldungen |
|
||||
| Is_Peak_Shaving als Variable | Betriebsart PV oder Peak im Vertrag 4.0 |
|
||||
|
||||
## Neu implementiert
|
||||
|
||||
- Validierung aller Mess- und Registervariablen
|
||||
- Pflichtaktion auf jeder Zielregistervariable
|
||||
- Messwertalter und sicherer Zustand bei ungueltigen Messwerten
|
||||
- Sollwert-Timeout
|
||||
- Diagnosevariablen, Registerfehlerstatus und Debug-Logging
|
||||
- standardisierte PHPUnit- und Symcon-8.0-Laufzeittests
|
||||
- explizite Schnittstellenbeschreibung
|
||||
|
||||
## Verworfen
|
||||
|
||||
- direkter SetValue-Aufruf auf internen Leistungs- und Modusvariablen
|
||||
- zyklische Regelung mit frei konfigurierbarem Interval
|
||||
- doppelte Peak-/PV-Codepfade mit identischer Registerabbildung
|
||||
- ungenutzte Batteriespannungsproperty
|
||||
- CheckIdle mit globalem GetValue ohne Objekt-ID
|
||||
|
||||
## Umstellung einer Anlage
|
||||
|
||||
1. Bestehende Batterieinstanz und alle bisherigen Variablen-IDs dokumentieren.
|
||||
2. Sicherung der IP-Symcon-Konfiguration erstellen.
|
||||
3. Neue Enelix-2-Batterieinstanz anlegen.
|
||||
4. Fuenf Messwertvariablen zuordnen.
|
||||
5. Batterietyp waehlen und die eingeblendeten Registervariablen zuordnen.
|
||||
6. Reserve, Mindestladezustand und Aenderungssperre uebertragen.
|
||||
7. Neue Batterie im Enelix-2-Manager zuordnen.
|
||||
8. Diagnose und Logging aktivieren.
|
||||
9. Laden, Entladen, 0 W und Wechselrichtermodus unter Aufsicht testen.
|
||||
10. Alte Instanz erst nach erfolgreicher Feldpruefung deaktivieren.
|
||||
|
||||
Ein automatisches Loeschen oder Ueberschreiben der alten Instanz findet nicht
|
||||
statt.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Migration von Boiler_x_Stufig
|
||||
|
||||
## Eigenschaften
|
||||
|
||||
| Enelix 1 | Enelix 2 | Hinweis |
|
||||
| --- | --- | --- |
|
||||
| `LeistungsStufen` | `LeistungsStufen` | Felder `Stufe`, `Leistung`, `Schaltkontakt_Stufe` bleiben erhalten. |
|
||||
| `ZeitKonstante` | `ZeitKonstante` | PT1-Berechnung verwendet die tatsaechlich verstrichene Zeit. |
|
||||
| `Boilerfuehler_PT1` | `Boilerfuehler_PT1` | Alter und Datentyp werden geprueft. |
|
||||
| `Boilertemperatur_glätten` | `BoilertemperaturGlaetten` | ASCII-sicherer Ident. |
|
||||
| `Boilervolumen` | `Boilervolumen` | Wird fuer die Zeitplanprognose verwendet. |
|
||||
| `Zeitplan` | `Zeitplan` | JSON-Liste mit `Uhrzeit` und `Solltemperatur`. |
|
||||
| `Interval` | entfaellt | Die Regelung wird ereignisbasiert ausgeloest. |
|
||||
| `IdleCounterMax` | `LastwechselSperrzeit` | Zykluszaehler wird durch eine Sperrzeit in Sekunden ersetzt; Standard `5`. |
|
||||
|
||||
Die neuen Properties `TemperaturMaxAlter`, `TemperaturUntergrenze`,
|
||||
`TemperaturObergrenze`, `Hysterese`, die Parameter der Legionellenfunktion und
|
||||
`DiagnosevariablenAnzeigen` ersetzen feste Werte und nicht pruefbare Annahmen
|
||||
des Altmoduls. Die Stoergrenzen liegen standardmaessig bei 0 und 100 Grad C und
|
||||
sind von den Regel-Sollwerten `Mindesttemperatur` und `Maximaltemperatur` getrennt.
|
||||
|
||||
## Variablen
|
||||
|
||||
| Enelix 1 | Enelix 2 |
|
||||
| --- | --- |
|
||||
| `Mindesttemperatur` | `Mindesttemperatur` |
|
||||
| `Maximaltemperatur` | `Maximaltemperatur` |
|
||||
| `Legionellentemperatur` | `Legionellentemperatur` |
|
||||
| `Boilertemperatur` | `Boilertemperatur` |
|
||||
| `LegioCounter` | `LegioCounter` |
|
||||
| `Idle` | entfaellt; Freigabe ueber `AenderungMoeglich` |
|
||||
| `IdleCounter` | entfaellt; Sperre ueber `Leistungswerte_W` und `AenderungMoeglich` |
|
||||
| `Aktuelle_Leistung` | gemeinsames `Istleistung` |
|
||||
| `Bezogene_Energie` | `BezogeneEnergie` in kWh |
|
||||
| `PV_Prio` | Property `PrioritaetPV` |
|
||||
| `Sperre_Prio` | Property `PrioritaetPeak` |
|
||||
| `Power` | gemeinsames `Sollleistung` |
|
||||
| `PowerSteps` | Vertragsfeld `Leistungswerte_W`, keine Variable |
|
||||
| `Is_Peak_Shaving` | entfaellt; Betriebsart liegt im Manager |
|
||||
| `Leistung_Delta` | entfaellt; Leistungsverteilung liegt im Manager |
|
||||
|
||||
## Inbetriebnahme
|
||||
|
||||
1. Neue Instanz `VerbraucherWarmwassererwaermer` anlegen.
|
||||
2. Fuehler, Leistungsstufen und Schaltkontakte uebertragen.
|
||||
3. Temperaturvariablen einmalig mit den bisherigen Sollwerten setzen.
|
||||
4. Die Instanz im Enelix-2-Manager unter `VerbraucherZuordnung` aktiv zuordnen.
|
||||
5. Erst danach `Aktiv` einschalten und jede Stufe beaufsichtigt pruefen.
|
||||
|
||||
Das Modul verwendet eine neue Modul-ID. Eine automatische Umwandlung der alten
|
||||
Instanz findet deshalb nicht statt.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Migration von Verbraucher_1_Stufig
|
||||
|
||||
Das Enelix-1-Modul wird nicht in-place aktualisiert. Das neue Modul verwendet
|
||||
eine neue Modul-ID, Vertrag `3.0` und eine ereignisbasierte Regelung.
|
||||
|
||||
## Eigenschaften
|
||||
|
||||
| Enelix 1 / bisheriges Enelix 2 | Aktuelles Enelix 2 | Migration |
|
||||
| --- | --- | --- |
|
||||
| `BoilerLeistung` | `Nennleistung` | Wert in ganzen Watt uebernehmen. |
|
||||
| `Schaltkontakt1` | `SchaltkontaktVariableID` | Boolean-Aktor uebernehmen und Aktion pruefen. |
|
||||
| nicht vorhanden | `SchaltkontaktInvertiert` | Nur bei umgekehrter Aktorlogik aktivieren. |
|
||||
| nicht vorhanden | `RueckmeldungVariableID` | Optional eine echte Boolean-Rueckmeldung zuordnen. |
|
||||
| `Mindesttlaufzeit` | `Mindestlaufzeit` | Alten Minutenwert fuer gleiches Verhalten mit 60 multiplizieren. |
|
||||
| `Zeit_Zwischen_Zustandswechseln` | `Mindesteinschaltdauer`, `Mindestausschaltdauer` | Alte Minuten nicht ungeprueft uebernehmen; beide Sekundenwerte geraetespezifisch setzen. |
|
||||
| `Umschaltabstand` aus fruehem Enelix 2 | `Mindesteinschaltdauer`, `Mindestausschaltdauer` | Der alte Wert wird nicht automatisch uebertragen; beide Werte haben Standard 5 s. |
|
||||
| `Interval` | entfaellt | Keine zyklische Regelberechnung. |
|
||||
| `IdleCounterMax` | entfaellt | Keine zyklusbasierte Idle-Erkennung. |
|
||||
|
||||
## Variablen und Verhalten
|
||||
|
||||
| Alt | Aktuell |
|
||||
| --- | --- |
|
||||
| `DailyOnTime` | `Tageslaufzeit` in bestaetigten echten Sekunden |
|
||||
| `IstNacht` | entfaellt; Tagesmindestlaufzeit wird spaetestmoeglich erzwungen |
|
||||
| `IsTimerActive` | entfaellt; interne einmalige Timer |
|
||||
| `Aktuelle_Leistung` | gemeinsames `Istleistung` |
|
||||
| `Power` | gemeinsames `Sollleistung` |
|
||||
| `PowerSteps` | Vertragsfeld `Leistungswerte_W` |
|
||||
| `PV_Prio` | Property `PrioritaetPV` |
|
||||
| `Sperre_Prio` | Property `PrioritaetPeak` |
|
||||
| `Is_Peak_Shaving` | entfaellt; Betriebsart liegt im Manager |
|
||||
| `Leistung_Delta` | entfaellt; Leistungsverteilung liegt im Manager |
|
||||
| `Idle`, `IdleCounter` | entfallen |
|
||||
|
||||
Bei konfigurierter Rueckmeldung bestimmt nicht mehr der ausgegebene
|
||||
Aktorbefehl, sondern erst der rueckgemeldete Zustand die Istleistung, die
|
||||
Tageslaufzeit und den Start der jeweiligen Mindestzeit.
|
||||
|
||||
## Inbetriebnahme
|
||||
|
||||
1. Bestehende Instanzkonfiguration und Aktorlogik dokumentieren.
|
||||
2. Neue Instanz **Verbraucher 1-Stufig** anlegen oder die bestehende
|
||||
Enelix-2-Instanz aktualisieren.
|
||||
3. Leistung, Aktor, Invertierung und optional Rueckmeldung konfigurieren.
|
||||
4. Tagesmindestlaufzeit gegebenenfalls von Minuten in Sekunden umrechnen.
|
||||
5. Mindest-Ein- und Mindest-Aus-Zeit getrennt anhand der Geraeteanforderungen
|
||||
setzen.
|
||||
6. Neue Instanz im Enelix-2-Manager aktiv zuordnen.
|
||||
7. Ein- und Ausschalten zuerst ohne reale Last oder unter Aufsicht pruefen.
|
||||
8. Bei separater Rueckmeldung kontrollieren, dass
|
||||
`SchaltbefehlAusstehend` erst mit deren Zustandswechsel verschwindet.
|
||||
9. Mindestens je einen verhinderten Ein- und Ausschaltvorgang testen.
|
||||
10. Tageslaufzeit und Tageswechsel kontrollieren.
|
||||
|
||||
## Rueckkehr
|
||||
|
||||
Solange die alte Instanz nicht geloescht wurde, kann zurueckgekehrt werden,
|
||||
indem die neue Instanz im Manager deaktiviert und ausgeschaltet wird. Danach
|
||||
darf die alte Instanz wieder aktiviert werden. Beide Module duerfen nie
|
||||
gleichzeitig denselben Aktor steuern.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Batterie
|
||||
|
||||
> Status: implementiert. Zielplattform ist IP-Symcon ab Version 8.0,
|
||||
> Nachrichtenvertrag 4.0.
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Das Modul bildet einen Batteriespeicher als bidirektionalen Enelix-Verbraucher
|
||||
ab. Es berechnet das betriebsart- und ladezustandsabhaengige
|
||||
Leistungsangebot, empfaengt Sollleistungen vom Manager und uebersetzt sie in
|
||||
herstellerspezifische Registerwerte.
|
||||
|
||||
Positive Leistung bedeutet Laden, negative Leistung Entladen. Die
|
||||
Geraeteanbindung erfolgt ausschliesslich ueber vom Benutzer ausgewaehlte
|
||||
numerische IP-Symcon-Variablen. Schreibziele muessen eine Aktion besitzen.
|
||||
|
||||
## Ereignismodell
|
||||
|
||||
Fachliche Neuberechnungen werden ausgeloest durch:
|
||||
|
||||
- Aktualisierung eines der fuenf Messwerte
|
||||
- Managerdaten mit Betriebsart oder Sollleistung
|
||||
- lokale Aenderung der Variablen Aktiv
|
||||
- Ablauf von Vorgabe-Timeout oder Aenderungssperre
|
||||
- manuelle Aktualisierung in der Konfigurationsmaske
|
||||
|
||||
Der Meldezyklus verschickt eine Vollmeldung und dient nicht als Regelzyklus.
|
||||
Ein periodischer Berechnungstimer oder Idle-Counter existiert nicht.
|
||||
|
||||
## Properties
|
||||
|
||||
### Gemeinsame Verbraucherproperties
|
||||
|
||||
PrioritaetPV, PrioritaetPeak, Meldeintervall, VorgabeTimeout,
|
||||
EinstellungenInVisu und LoggingEin stammen aus VerbraucherBasisTrait.
|
||||
|
||||
### Batteriespezifische Properties
|
||||
|
||||
| Property | Typ | Standard | Bedeutung |
|
||||
| --- | --- | ---: | --- |
|
||||
| Batterietyp | Integer | 0 | 0 unkonfiguriert, 1 generisch, 2 GoodWe, 3 SolarEdge, 4 Sigenergy |
|
||||
| Batteriemanagement | Integer | 1 | 1 Wechselrichter, 2 Enelix |
|
||||
| MaxLadeleistungVariableID | Integer | 0 | Dynamische Ladegrenze in W |
|
||||
| MaxEntladeleistungVariableID | Integer | 0 | Dynamische Entladegrenze in W |
|
||||
| LadezustandVariableID | Integer | 0 | SoC in Prozent |
|
||||
| NetzleistungVariableID | Integer | 0 | Positiv Netzbezug, negativ Einspeisung |
|
||||
| IstleistungVariableID | Integer | 0 | Positiv Laden, negativ Entladen |
|
||||
| ManagementRegisterVariableID | Integer | 0 | Steuerungsquelle des Wechselrichters |
|
||||
| ModusRegisterVariableID | Integer | 0 | Laden-/Entladen-Code |
|
||||
| LeistungsRegisterVariableID | Integer | 0 | GoodWe-Leistungsregister in W |
|
||||
| LadeleistungRegisterVariableID | Integer | 0 | Ladeleistung in W beziehungsweise kW |
|
||||
| EntladeleistungRegisterVariableID | Integer | 0 | Entladeleistung in W beziehungsweise kW |
|
||||
| ReserveLadezustand | Float | 20 | Peakshaving-Reserve in Prozent |
|
||||
| MindestLadezustand | Float | 10 | Untere Entladegrenze in Prozent |
|
||||
| LadezustandHysterese | Float | 2 | Hysterese oberhalb der Reserve |
|
||||
| NachladenMitMaximalleistung | Boolean | true | Verwendet fuer schutzbedingtes Nachladen die dynamische maximale Ladeleistung |
|
||||
| MaximaleNachladeleistung | Integer | 0 | Benutzerdefinierte Nachladegrenze in W; bei deaktivierter Maximalleistung muss der Wert groesser als 0 sein |
|
||||
| MesswertMaxAlter | Integer | 60 | Maximales Messwertalter in Sekunden |
|
||||
| Aenderungssperre | Integer | 4 | Sperrzeit nach Sollwertaenderung |
|
||||
| DiagnosevariablenAnzeigen | Boolean | false | Technische Variablen einblenden |
|
||||
|
||||
## Registeradapter
|
||||
|
||||
| Typ | WR-Management | Enelix-Management | Laden | Entladen |
|
||||
| --- | ---: | ---: | --- | --- |
|
||||
| Generisch | 0 | 1 | Modus 0, Ladeleistung W | Modus 1, Entladeleistung W |
|
||||
| GoodWe | 1 | 2 | Modus 11, Betrag W | Modus 12, Betrag W |
|
||||
| SolarEdge | 1 | 4 | Modus 3, Ladeleistung W | Modus 4, Entladeleistung W |
|
||||
| Sigenergy | 0 | 1 | Modus 3, Ladeleistung kW | Modus 6, Entladeleistung kW |
|
||||
|
||||
Beim Wechsel in die Wechselrichtersteuerung werden zuerst die zum Typ
|
||||
gehoerenden Leistungsregister auf 0 gesetzt und danach der Automatikcode
|
||||
geschrieben. Im Enelix-Modus werden zuerst die Leistungswerte, dann Modus und
|
||||
zuletzt Management geschrieben.
|
||||
|
||||
## Leistungsangebot
|
||||
|
||||
Die Batterie meldet innerhalb der dynamischen Lade- und Entladegrenzen
|
||||
inklusive Leistungsbereiche in ganzen Watt. Der Manager kann dadurch jeden
|
||||
ganzzahligen Sollwert innerhalb des aktuell erlaubten Bereichs vorgeben. Die
|
||||
Grenzen werden abgerundet, damit keine dynamische Maximalleistung ueberschritten
|
||||
wird.
|
||||
|
||||
PV und Peak verwenden weiterhin die bisherige SoC-Logik. Feste Schutz- oder
|
||||
Peakvorgaben bleiben Einzelwerte; frei regelbare Angebote werden als Bereiche
|
||||
gemeldet. Bei schutzbedingtem Nachladen innerhalb oder unterhalb der Reserve
|
||||
begrenzt MaximaleNachladeleistung die positive Angebotsgrenze, wenn
|
||||
NachladenMitMaximalleistung deaktiviert ist. Der normale Ladebereich oberhalb
|
||||
der Reserve bleibt unveraendert. Die Hysterese wird als persistenter Modulzustand
|
||||
gefuehrt. Bei Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen
|
||||
Messwerten lautet das Angebot [0].
|
||||
|
||||
## Variablen
|
||||
|
||||
Immer sichtbar sind Aktiv und Ladestatus. Ladestatus verwendet:
|
||||
|
||||
| Wert | Bedeutung |
|
||||
| ---: | --- |
|
||||
| 0 | Messwert ungueltig oder unbekannt |
|
||||
| 1 | Ruhezustand |
|
||||
| 2 | Laden |
|
||||
| 3 | Entladen |
|
||||
|
||||
Die Diagnoseoption ergaenzt die gemeinsamen Verbraucherdiagnosen sowie
|
||||
Ladezustand, Netzleistung, Leistungsgrenzen, Hysterese, Steuerungsmodus,
|
||||
BezogeneEnergie, LeistungsangebotDiagnose und LetzterRegisterbefehl.
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
- Status 201: Konfiguration oder Variablentyp ungueltig
|
||||
- Status 202: Messwert fehlt, ist veraltet oder fachlich ungueltig
|
||||
- Status 203: RequestAction auf mindestens ein Register ist fehlgeschlagen
|
||||
|
||||
Ein ungueltiger oder abgelaufener Sollwert wird verworfen und durch 0 ersetzt.
|
||||
Die konkrete Stoerung wird im Vertrag und bei aktivierter Diagnose in
|
||||
Stoerung und Stoertext gemeldet.
|
||||
|
||||
## Managerkommunikation
|
||||
|
||||
Die fachliche Schnittstelle ist in
|
||||
[Schnittstelle Batterie](../../Schnittstelle-Batterie.md) beschrieben.
|
||||
Transport und gemeinsame Felder folgen
|
||||
[EMS-Schnittstelle](../../Schnittstelle.md).
|
||||
|
||||
## Tests
|
||||
|
||||
- BatterieReglerTest: Leistungsbereiche, Hysterese, PV/Peak und Herstellerabbildung
|
||||
- BatterieModulstrukturTest: Metadaten, Formular, Ereignismodell und Manager-ID
|
||||
- Symcon/modules/Batterie.php: reale Modulinstanz und Registeraktionen unter
|
||||
IP-Symcon 8.0
|
||||
@@ -0,0 +1,77 @@
|
||||
# Easee Gateway
|
||||
|
||||
> Status: implementiert fuer IP-Symcon 8 und die Easee Cloud API.
|
||||
|
||||
Das Modul stellt pro Easee-Nutzerkonto genau eine gemeinsame Verbindung bereit.
|
||||
Benutzername, Passwort, Access Token und Refresh Token verbleiben im Gateway.
|
||||
Mehrere Instanzen von **Ladestation Gateway** koennen denselben Elternknoten
|
||||
verwenden.
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- Anmeldung mit Easee-Benutzerkonto und automatische Token-Erneuerung,
|
||||
- gemeinsame Ereignisverbindung zu `streams.easee.com`,
|
||||
- Abonnement mehrerer Ladestationen mit aktuellem Zustand,
|
||||
- Verteilung der Easee-Observations an die passenden Kindinstanzen,
|
||||
- Stromvorgabe ueber `set_dynamic_charger_current`,
|
||||
- Wiederverbindung und erneute Anmeldung aller Stationen nach Unterbrechungen,
|
||||
- TLS-Zertifikatspruefung standardmaessig aktiv.
|
||||
|
||||
## Properties
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | --- | --- |
|
||||
| `Active` | Boolean | `true` | Aktiviert Anmeldung und Ereignisverbindung. |
|
||||
| `Username` | String | leer | E-Mail-Adresse oder Telefonnummer des Easee-Kontos. |
|
||||
| `Password` | Passwort | leer | Passwort, nur im Gateway gespeichert. |
|
||||
| `VerifyCertificate` | Boolean | `true` | Prueft TLS-Zertifikate fuer REST und WebSocket. |
|
||||
|
||||
Die interne Property `Testmodus` ist nicht im Formular sichtbar und wird nur
|
||||
vom automatisierten IP-Symcon-Test verwendet.
|
||||
|
||||
## Variablen
|
||||
|
||||
| Ident | Typ | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Connected` | Boolean | Ereignisverbindung ist betriebsbereit. |
|
||||
| `SubscriptionCount` | Integer | Anzahl angemeldeter Seriennummern. |
|
||||
| `LastError` | String | Letzter Fehler ohne Zugangsdaten oder Tokens. |
|
||||
|
||||
## API und Ereignisse
|
||||
|
||||
Der Gateway-Transport ist in
|
||||
[Easee-Gateway-Schnittstelle](../../Schnittstelle-Easee-Gateway.md)
|
||||
vollstaendig beschrieben. Fuer Fahrzeug- und Phasenstatus werden insbesondere
|
||||
Observation `109` und `110` verteilt. Nach jeder Wiederverbindung wird der
|
||||
Cache verworfen und durch `SubscribeWithCurrentState` neu aufgebaut.
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
| Status | Bedeutung |
|
||||
| ---: | --- |
|
||||
| `102` | Verbindung aktiv. |
|
||||
| `104` | Gateway deaktiviert. |
|
||||
| `201` | Benutzername oder Passwort fehlt. |
|
||||
| `202` | Anmeldung oder Token-Erneuerung fehlgeschlagen. |
|
||||
| `203` | Ereignisverbindung fehlgeschlagen. |
|
||||
|
||||
REST-Aufrufe verwenden 5 Sekunden Verbindungs- und 30 Sekunden
|
||||
Gesamt-Timeout. HTTP-401 fuehrt einmalig zu einer Token-Erneuerung und
|
||||
Wiederholung. Tokens werden nie als Variable oder Debugtext ausgegeben.
|
||||
|
||||
## Inbetriebnahme
|
||||
|
||||
1. Eine Instanz **Easee Gateway** erstellen.
|
||||
2. Benutzername und Passwort des Easee-Kontos eintragen.
|
||||
3. TLS-Pruefung aktiviert lassen.
|
||||
4. Speichern und Status `102` sowie `Connected=true` abwarten.
|
||||
5. Fuer jede Station eine Kindinstanz **Ladestation Gateway** anlegen.
|
||||
6. Bei mehreren Konten je Konto eine eigene Gateway-Instanz verwenden.
|
||||
|
||||
## Tests
|
||||
|
||||
`composer check` prueft Syntax und Unit-Tests. Der Funktionstest laeuft mit:
|
||||
|
||||
```bash
|
||||
tests/Symcon/bin/run-symcon-tests.sh single EaseeGateway
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
# Ladestation Gateway
|
||||
|
||||
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `4.0`.
|
||||
|
||||
Das Modul bindet genau eine Easee-Ladestation als EMS-Verbraucher an. Es ist
|
||||
Kind eines **Easee Gateway** und besitzt keine Zugangsdaten.
|
||||
|
||||
## Varianten
|
||||
|
||||
| Betriebsmodus | Verhalten |
|
||||
| --- | --- |
|
||||
| `Easee` | Solarladen kann als Startwert konfiguriert und lokal umgeschaltet werden. |
|
||||
| `Easee - Nur Solarladen` | Solarladen ist fest aktiv und kann nicht abgeschaltet werden. |
|
||||
|
||||
Die alte eCarUp-Zusatzabhaengigkeit wird nicht uebernommen. Konto,
|
||||
Ladestatus und Steuerung laufen ausschliesslich ueber Easee.
|
||||
|
||||
## Ereignisbasierter Status
|
||||
|
||||
Das Modul pollt die Ladestation nicht. Es verarbeitet unmittelbar die vom
|
||||
Gateway gelieferten Easee-Observations:
|
||||
|
||||
| Observation | Verwendung |
|
||||
| ---: | --- |
|
||||
| `47` | Maximalstrom der Ladestation. |
|
||||
| `104` | Kabelstromgrenze. |
|
||||
| `109` | Fahrzeugerkennung und Ladezustand. |
|
||||
| `110` | Aktive Ausgangsphase: Werte 10 bis 15 ergeben 1 Phase, Wert 30 ergibt 3 Phasen. |
|
||||
| `119` | Easee-Fehlercode. |
|
||||
| `120` | Gemessene Gesamtleistung in kW. |
|
||||
| `182..185` | Gemessene Leiterstroeme; der groesste Betrag ist der angezeigte Ladestrom. |
|
||||
|
||||
Damit entfallen die alte 60-Sekunden-Erkennung, die 7500-W-Schwelle und die
|
||||
Voll-Erkennung aus einem unterschaetzten Strom. Fahrzeug, Ladeende und
|
||||
Phasenzahl stammen direkt aus der Easee API. Zweiphasige oder noch nicht
|
||||
zugewiesene Ausgangsphasen sowie unbekannte Betriebszustaende werden sicher
|
||||
als ungueltig behandelt und ergeben bis zur gueltigen 1-/3-Phasenmeldung nur
|
||||
`[0]`.
|
||||
|
||||
## Properties
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `Betriebsmodus` | Integer | `0` | `0` Easee, `1` Easee - Nur Solarladen. |
|
||||
| `Ladestationskennung` | String | leer | Easee-Seriennummer. |
|
||||
| `MaximalerLadestrom` | Integer | `16 A` | Lokale Obergrenze von 6 bis 32 A. API- und Kabelgrenze wirken zusaetzlich. |
|
||||
| `Ladefreigabe` | Boolean | `false` | Startwert der lokalen Ladefreigabe. |
|
||||
| `Solarladen` | Boolean | `true` | Startwert im normalen Easee-Modus. |
|
||||
| `PrioritaetPV` | Integer | `0` | Prioritaet im PV-Betrieb. |
|
||||
| `PrioritaetPeak` | Integer | `0` | Prioritaet im Peakbetrieb. |
|
||||
| `Meldeintervall` | Integer | `10 s` | Periodische Vollmeldung an den Manager. |
|
||||
| `VorgabeTimeout` | Integer | `120 s` | Ablauf einer nicht erneuerten Manager-Vorgabe. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Zeigt Ladefreigabe, Solarladen, Istleistung und Sollleistung. |
|
||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Zeigt technische Diagnosewerte. |
|
||||
| `LoggingEin` | Boolean | `false` | Aktiviert Debugmeldungen ohne Geheimnisse. |
|
||||
|
||||
## Variablen
|
||||
|
||||
Immer sichtbar sind `Aktiv`, `FahrzeugVerbunden`, `FahrzeugGeladen`,
|
||||
`Ladestrom` und `Phasenzahl`. Mit `EinstellungenInVisu=true` erscheinen
|
||||
zusaetzlich `Ladefreigabe`, `Solarladen`, `Istleistung` und `Sollleistung`.
|
||||
|
||||
Mit `DiagnosevariablenAnzeigen=true` werden der numerische `Fahrzeugstatus`
|
||||
und die gemeinsamen EMS-Diagnosewerte sowie
|
||||
`GatewayVerbunden`, `ApiMaximalstrom`, `EaseeAktiviert`,
|
||||
`AutorisierungErforderlich`, `RemoteStartErforderlich`, `SmartCharging`,
|
||||
`GrundKeinLadestrom`, `LetzterGeraetebefehl`, `LetzterStartbefehl` und
|
||||
`LeistungsangebotDiagnose` angezeigt. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
|
||||
die Autorisierung fehlt.
|
||||
|
||||
## Regelung
|
||||
|
||||
Die Leistungsstufen und das PV-/Peak-Verhalten entsprechen der
|
||||
Ladestation Stand-Alone:
|
||||
|
||||
| Betriebsart | Solarladen | Angebot |
|
||||
| --- | --- | --- |
|
||||
| PV | ein | `[0, ...Ladestufen]` |
|
||||
| PV | aus | nur maximale Ladestufe |
|
||||
| Peak | ein | `[0]` |
|
||||
| Peak | aus | `[0, ...Ladestufen]` |
|
||||
|
||||
Die niedrigste Grenze aus Property, Observation `47` und Observation `104`
|
||||
bestimmt den angebotenen Maximalstrom. Eine neue Observation berechnet das
|
||||
Angebot sofort neu und meldet die Aenderung kurz gebuendelt an den Manager.
|
||||
Beim Aktivieren oder Umschalten von Solarladen bleibt der bisherige
|
||||
Geraetestrom bis zur neuen Manager-Vorgabe unveraendert. Ohne Managerantwort
|
||||
greift nach zwei Sekunden das lokale Ersatzverhalten. Sicherheitsabschaltungen
|
||||
werden nicht verzoegert.
|
||||
|
||||
Bei einer Gateway-Unterbrechung werden Fahrzeug- und Phasenstatus sofort
|
||||
verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder
|
||||
frei. Die eigentliche Vorgabe wird als dynamischer Ladestrom mit `minutes=0`
|
||||
an Easee gesendet. Meldet Easee bei positiver Stromvorgabe weiterhin
|
||||
`Awaiting Start`, sendet das Modul einmal pro Fahrzeugverbindung zusaetzlich
|
||||
`start_charging`. Dieser Befehl autorisiert den Ladevorgang, uebersteuert aber
|
||||
keinen vorhandenen Easee-Ladeplan.
|
||||
|
||||
## Managerkommunikation
|
||||
|
||||
Das Modul implementiert den
|
||||
[EMS-Nachrichtenvertrag `4.0`](../../Schnittstelle.md). Es wird im Manager
|
||||
wie die Stand-Alone-Ladestation unter dem Lizenzkatalog `ev_charger`
|
||||
gefuehrt. Es gibt keine Manager-ID-Property; die Zuordnung erfolgt nur im
|
||||
Manager.
|
||||
|
||||
## Inbetriebnahme
|
||||
|
||||
1. Ein konfiguriertes Easee Gateway mit Status `102` bereitstellen.
|
||||
2. Darunter **Ladestation Gateway** erstellen.
|
||||
3. Variante, Seriennummer und elektrische Maximalgrenze konfigurieren.
|
||||
4. Ladefreigabe und Solarladen festlegen.
|
||||
5. Die Instanz im Enelix Manager aktiv zuordnen.
|
||||
6. Ohne Fahrzeug Status `1` und Angebot `[0]` pruefen.
|
||||
7. Fahrzeug verbinden und die API-Werte fuer Status und Phasenzahl kontrollieren.
|
||||
8. Erst danach `Aktiv` einschalten und eine kleine Vorgabe testen.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
composer check
|
||||
tests/Symcon/bin/run-symcon-tests.sh single LadestationGateway
|
||||
```
|
||||
@@ -0,0 +1,331 @@
|
||||
# Ladestation Stand-Alone
|
||||
|
||||
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `4.0`.
|
||||
|
||||
Das Modul bindet eine einzelne Ladestation direkt an Enelix EMS an. Es liest
|
||||
den Fahrzeug- und Ladezustand ueber die Geraete-API, ermittelt die Phasenzahl
|
||||
wie Enelix 1 und setzt den vom Manager gewaehlten Ladestrom. Es benoetigt keine
|
||||
Gateway-Instanz und hat keine technische Beziehung zum separaten Modul
|
||||
**Ladestation Gateway**.
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- direkte Statusabfrage und Steuerung der unterstuetzten Ladestationen,
|
||||
- Fahrzeug-, Lade- und Phasenerkennung nach der Enelix-1-Logik,
|
||||
- Leistungsangebote fuer PV- und Peakbetrieb,
|
||||
- lokale Freigabe und Umschaltung zwischen Solar- und Normalbetrieb,
|
||||
- Kommunikation mit dem Enelix Manager ueber den Nachrichtenvertrag `4.0`,
|
||||
- optional sichtbare Diagnosevariablen und schaltbares Debug-Logging,
|
||||
- automatisierte Tests mit Fake-HTTP-Transport ohne reale Ladestation.
|
||||
|
||||
## Unterstuetzte Geraete
|
||||
|
||||
| Geraetetyp | Statusabfrage | Steuerung | Erforderliche Konfiguration |
|
||||
| --- | --- | --- | --- |
|
||||
| go-e Charger, alte API | `GET /mqtt?payload=` | `alw` und `amp` | IP-Adresse oder Hostname |
|
||||
| go-e Charger Gemini / Gemini flex | `GET /api/status` | `frc` und `amp` | IP-Adresse oder Hostname |
|
||||
| smart-me Pico | Pico-Charging-API | Load-Management-Current-API | Geraete-ID, Seriennummer, Benutzername und Passwort |
|
||||
|
||||
Bei go-e wird die lokale HTTP-API verwendet. Die Geraeteadresse wird ohne
|
||||
`http://`, Pfad oder Parameter eingetragen. Die Pico-Anbindung verwendet HTTPS
|
||||
und HTTP Basic Auth gegen `api.smart-me.com`. Zugangsdaten werden weder als
|
||||
Variable noch im Debug-Protokoll oder im Diagnosefeld
|
||||
`LetzterGeraetebefehl` ausgegeben.
|
||||
|
||||
## Fahrzeug- und Phasenerkennung
|
||||
|
||||
Die Auswertung folgt bewusst dem Verhalten der bisherigen Enelix-1-Ladestation:
|
||||
|
||||
1. Bei go-e gilt das Fahrzeug als verbunden, wenn `car != 1` ist. Bei Pico
|
||||
wird entsprechend `State != 1` ausgewertet.
|
||||
2. Die gemessene Leistung wird bei der alten go-e-API mit Faktor 10, bei
|
||||
Gemini direkt in Watt und bei Pico von kW in Watt umgerechnet.
|
||||
3. go-e meldet die drei Phasenstroeme einzeln. Sobald mindestens zwei Phasen
|
||||
Strom fuehren, wird dreiphasiges Laden direkt erkannt.
|
||||
4. Sind beim Anstecken noch keine belastbaren Phasenwerte vorhanden, gibt das
|
||||
Modul zunaechst nur den Mindeststrom von 6 A als Erkennungsstrom vor. Bis zur
|
||||
abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot `[0]`.
|
||||
5. Nach `Phasenerkennungszeit` wird die gemessene Leistung relativ zum
|
||||
angeforderten Erkennungsstrom ausgewertet. Dadurch lassen sich eine und drei
|
||||
Phasen bereits bei 6 A unterscheiden. Die erkannte Phasenzahl bleibt bis
|
||||
zum Ausstecken erhalten und kippt waehrend einer Ladepause nicht zurueck.
|
||||
6. Der Ladestrom wird aus der Leistung mit `230 W/A` einphasig beziehungsweise
|
||||
`684 W/A` dreiphasig berechnet.
|
||||
7. Ein Ladeende wird nur nach zuvor tatsaechlich gemessener Ladeleistung
|
||||
erkannt. Eine vom EMS angeordnete Solarpause gilt deshalb nicht als
|
||||
`FahrzeugGeladen`.
|
||||
|
||||
Der normalisierte `Fahrzeugstatus` verwendet folgende Werte:
|
||||
|
||||
| Wert | Bedeutung |
|
||||
| ---: | --- |
|
||||
| `0` | unbekannt oder noch nicht ermittelt |
|
||||
| `1` | kein Fahrzeug verbunden |
|
||||
| `2` | Fahrzeug verbunden und bereit |
|
||||
| `3` | Fahrzeug laedt |
|
||||
| `4` | Fahrzeug geladen |
|
||||
|
||||
## Leistungsregelung
|
||||
|
||||
Ein Leistungsangebot groesser als `0 W` wird nur erzeugt, wenn das Modul
|
||||
aktiviert, die Ladefreigabe gesetzt, ein Fahrzeug verbunden und das Fahrzeug
|
||||
noch nicht als geladen erkannt ist. Andernfalls meldet das Modul `[0]` und
|
||||
stoppt die Ladestation.
|
||||
|
||||
| Betriebsart | Solarladen | Leistungsangebot |
|
||||
| --- | --- | --- |
|
||||
| PV | ein | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum |
|
||||
| PV | aus | ausschliesslich die maximale Ladeleistung |
|
||||
| Peak | ein | ausschliesslich `[0]`; die Ladestation wird gestoppt |
|
||||
| Peak | aus | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum |
|
||||
|
||||
Die Leistungsstufen entsprechen Enelix 1: `230 W` pro Ampere einphasig und
|
||||
`684 W` pro Ampere dreiphasig. Ohne gueltige Manager-Vorgabe faehrt das Modul
|
||||
bei aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es
|
||||
die maximale angebotene Leistung.
|
||||
|
||||
Beim Aktivieren oder Umschalten von Solarladen bleibt ein bereits gesetzter
|
||||
Geraetestrom zunaechst unveraendert. Das neue Angebot wird zuerst an den
|
||||
zugeordneten Manager gemeldet; dessen neue Vorgabe wird anschliessend direkt
|
||||
uebernommen. Antwortet kein Manager, greift nach zwei Sekunden das lokale
|
||||
Ersatzverhalten. Sicherheitsabschaltungen wirken weiterhin sofort.
|
||||
|
||||
Nach einer Leistungsaenderung wird die aktuelle Stufe fuer
|
||||
`ZeitZwischenZustandswechseln` gehalten. Waehrend der
|
||||
`Mindesteinschaltdauer` bleiben positive Leistungsstufen regelbar, `0 W`
|
||||
wird jedoch nicht angeboten. Nach dem Abschalten meldet das Modul waehrend der
|
||||
`Mindestausschaltdauer` ausschliesslich `[0]`. Sicherheitsabschaltungen
|
||||
durch Deaktivierung, fehlende Ladefreigabe, Ausstecken oder ein erkanntes
|
||||
Ladeende greifen sofort.
|
||||
|
||||
## Bedienung
|
||||
|
||||
| Ident | Wirkung |
|
||||
| --- | --- |
|
||||
| `Aktiv` | Gemeinsame lokale EMS-Freigabe. `false` setzt das Angebot auf `[0]` und stoppt die Ladestation. |
|
||||
| `Ladefreigabe` | Lokale Freigabe fuer das Laden. Diese Variable ist nur mit `EinstellungenInVisu=true` sichtbar. |
|
||||
| `Solarladen` | Schaltet das oben beschriebene PV-/Peak-Verhalten um. Diese Variable ist nur mit `EinstellungenInVisu=true` sichtbar. |
|
||||
|
||||
Die Properties `Ladefreigabe` und `Solarladen` definieren die Startwerte. Eine
|
||||
Aenderung der jeweiligen Property wird beim Anwenden der Konfiguration in den
|
||||
lokalen Zustand uebernommen. Eine Bedienung der Variablen verwirft eine noch
|
||||
gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Gemeinsame EMS-Einstellungen
|
||||
|
||||
| Property | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `PrioritaetPV` | Integer | `0` | Prioritaet des Verbrauchers im PV-Betrieb. |
|
||||
| `PrioritaetPeak` | Integer | `0` | Prioritaet des Verbrauchers im Peakbetrieb. |
|
||||
| `Meldeintervall` | Integer | `10 s` | Intervall fuer die periodische Vollmeldung an zugeordnete Manager. |
|
||||
| `VorgabeTimeout` | Integer | `120 s` | Gueltigkeitsdauer einer Manager-Vorgabe ohne Erneuerung. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Blendet `Ladefreigabe`, `Solarladen`, `Istleistung` und `Sollleistung` ein. |
|
||||
| `LoggingEin` | Boolean | `false` | Aktiviert das laufende Debug-Protokoll des Moduls. |
|
||||
|
||||
### Ladestation und Diagnose
|
||||
|
||||
| Property | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `Geraetetyp` | Integer | `0` | `1` go-e alt, `2` go-e Gemini, `3` smart-me Pico; `0` ist nicht konfiguriert. |
|
||||
| `Geraeteadresse` | String | leer | IP-Adresse oder Hostname fuer beide go-e-Varianten. |
|
||||
| `GeraeteID` | String | leer | Geraete-ID fuer die Pico-Statusabfrage. |
|
||||
| `Seriennummer` | String | leer | Seriennummer fuer die Pico-Stromvorgabe. |
|
||||
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
|
||||
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. |
|
||||
| `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
|
||||
| `Phasenerkennungszeit` | Integer | `60 s` | Dauer der 6-A-Probe vor der leistungsbasierten Phasenerkennung. |
|
||||
| `FahrzeugstromErkennungszeit` | Integer | `90 s` | Zeit, die eine stabile Stromunterschreitung anliegen muss, bevor die Fahrzeuggrenze mit 2,5 A Reserve uebernommen wird. |
|
||||
| `ZeitZwischenZustandswechseln` | Integer | `1 min` | Mindestabstand zwischen zwei Leistungsaenderungen. |
|
||||
| `Mindesteinschaltdauer` | Integer | `0 min` | Mindestdauer nach dem Einschalten, in der kein Abschalten auf 0 W angeboten wird. |
|
||||
| `Mindestausschaltdauer` | Integer | `0 min` | Mindestpause nach dem Abschalten, in der nur 0 W angeboten werden. |
|
||||
| `Abfrageintervall` | Integer | `5 s` | Intervall der Geraetestatusabfrage. |
|
||||
| `Ladefreigabe` | Boolean | `true` | Startwert der lokalen Ladefreigabe. |
|
||||
| `Solarladen` | Boolean | `true` | Startwert der lokalen Solarlogik. |
|
||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Blendet gemeinsame und ladestationsspezifische Diagnosevariablen ein. |
|
||||
|
||||
Die internen Properties `Testmodus` und `Testantwort` gehoeren ausschliesslich
|
||||
zum automatisierten Symcon-Funktionstest und erscheinen nicht im
|
||||
Konfigurationsformular.
|
||||
|
||||
## Variablen
|
||||
|
||||
Immer sichtbar:
|
||||
|
||||
| Ident | Typ / Zugriff | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; der Initialwert ist `false`. |
|
||||
| `FahrzeugVerbunden` | Boolean / Anzeige | Zeigt, ob die API ein verbundenes Fahrzeug meldet. |
|
||||
| `FahrzeugGeladen` | Boolean / Anzeige | Ergebnis der kompatiblen Enelix-1-Voll-Erkennung. |
|
||||
| `Ladestrom` | Float / Anzeige | Aus der gemessenen Leistung ermittelter Ladestrom in A. |
|
||||
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig oder `3` dreiphasig. |
|
||||
|
||||
Nur mit `EinstellungenInVisu=true` sichtbar; Ladefreigabe und Solarladen sind bedienbar:
|
||||
|
||||
- `Ladefreigabe`
|
||||
- `Solarladen`
|
||||
- `Istleistung`
|
||||
- `Sollleistung`
|
||||
|
||||
Nur mit `DiagnosevariablenAnzeigen=true` sichtbar:
|
||||
|
||||
| Ident | Beschreibung |
|
||||
| --- | --- |
|
||||
| `Leistungsquelle` | Immer `2` fuer eine gemessene Leistung. |
|
||||
| `Fahrzeugstatus` | Normalisierter Status von `0` bis `4`. |
|
||||
| `SollwertGueltig` | Zeigt, ob die Vorgabe noch gueltig und im aktuellen Angebot enthalten ist. |
|
||||
| `Verfuegbar` | Zeigt, ob die Ladestation grundsaetzlich Leistung aufnehmen kann. |
|
||||
| `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. |
|
||||
| `Stoerung` | Sammelstatus fuer Konfigurations- und Kommunikationsfehler. |
|
||||
| `Stoertext` | Letzte verstaendliche Fehlerbeschreibung. |
|
||||
| `LetzterGeraetebefehl` | Letzter Steuerbefehl mit HTTP-Methode und URL ohne Zugangsdaten; Statusabfragen ueberschreiben ihn nicht. |
|
||||
| `LeistungsangebotDiagnose` | Aktuelles Leistungsangebot als JSON-Liste in W. |
|
||||
|
||||
Die Regellogik speichert ihren Zustand unabhaengig von der Sichtbarkeit der
|
||||
Diagnosevariablen. Das Ein- oder Ausblenden veraendert daher nicht das
|
||||
Regelverhalten.
|
||||
|
||||
## Managerkommunikation
|
||||
|
||||
Das Modul implementiert
|
||||
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
|
||||
[ausschliesslich den Nachrichtenvertrag `4.0`](../../Schnittstelle.md). Eine
|
||||
separate Manager-ID wird nicht konfiguriert. Das Modul akzeptiert Vorgaben nur
|
||||
von einem Manager, in dessen manueller oder automatischer
|
||||
Verbraucherzuordnung die Ladestation aktiv eingetragen ist.
|
||||
|
||||
An den Manager werden neben den gemeinsamen Feldern diese Zustaende gemeldet:
|
||||
|
||||
- `Sollleistung_W`
|
||||
- `FahrzeugVerbunden`
|
||||
- `FahrzeugGeladen`
|
||||
- `Fahrzeugstatus`
|
||||
- `Phasenzahl`
|
||||
- `Ladestrom_A`
|
||||
- `Ladefreigabe`
|
||||
- `Solarladen`
|
||||
- `Ladefehler` mit Stoertext
|
||||
|
||||
Eine Sollleistung wird nur angenommen, wenn sie im zuletzt berechneten
|
||||
Leistungsangebot enthalten ist. Nach `VorgabeTimeout` ohne Erneuerung wird sie
|
||||
ungueltig. Statusaenderungen werden kurz verzoegert und zusaetzlich alle
|
||||
`Meldeintervall` Sekunden an alle zugeordneten Manager gemeldet.
|
||||
|
||||
## Instanzstatus und Fehlerbehandlung
|
||||
|
||||
| Status | Bedeutung |
|
||||
| ---: | --- |
|
||||
| `102` | Konfiguration und letzte Geraetekommunikation sind gueltig. |
|
||||
| `201` | Konfiguration ungueltig, beispielsweise fehlende Adresse oder Pico-Zugangsdaten. |
|
||||
| `202` | Geraetekommunikation oder Antwortauswertung fehlgeschlagen. |
|
||||
|
||||
Bei einem Fehler meldet das Modul `Verfuegbar=false`,
|
||||
`AenderungMoeglich=false` und setzt `Stoerung` sowie `Stoertext`. HTTP-Anfragen
|
||||
verwenden 5 Sekunden Verbindungs- und 10 Sekunden Gesamt-Timeout. Antworten ab
|
||||
HTTP-Status 400 sowie unvollstaendige oder ungueltige JSON-Antworten gelten als
|
||||
Kommunikationsfehler.
|
||||
|
||||
## Installation und Inbetriebnahme
|
||||
|
||||
1. Im IP-Symcon Module Control den Testing-Branch `develop` der Bibliothek
|
||||
`https://git.belevo.ch/ENELIX/Enelix-EMS.git` installieren oder
|
||||
aktualisieren.
|
||||
2. Unter **Instanz hinzufuegen** nach **Ladestation Stand-Alone** suchen und
|
||||
eine Instanz anlegen.
|
||||
3. Den Geraetetyp auswaehlen und die dazugehoerigen Verbindungsdaten eintragen:
|
||||
bei go-e nur IP-Adresse oder Hostname, bei Pico Geraete-ID, Seriennummer,
|
||||
Benutzername und Passwort.
|
||||
4. Den maximal zulaessigen Ladestrom der Installation zwischen 6 und 32 A
|
||||
einstellen. Diese Grenze ersetzt keine elektrische Absicherung.
|
||||
5. `Abfrageintervall`, `Meldeintervall`, `VorgabeTimeout` sowie die beiden
|
||||
Prioritaeten festlegen.
|
||||
6. Die gewuenschten Startwerte fuer `Ladefreigabe` und `Solarladen` setzen.
|
||||
7. Fuer die Erstinbetriebnahme `EinstellungenInVisu`,
|
||||
`DiagnosevariablenAnzeigen` und bei Bedarf `LoggingEin` aktivieren.
|
||||
8. Die Instanz im Manager manuell aktiv zuordnen oder bei automatischer Suche
|
||||
in der gefundenen Liste aktivieren.
|
||||
9. Zuerst ohne Fahrzeug kontrollieren, ob die Statusabfrage fehlerfrei ist.
|
||||
Danach unter Aufsicht ein Fahrzeug verbinden und `Aktiv` einschalten.
|
||||
|
||||
Fuer go-e muss IP-Symcon das Geraet im lokalen Netz per HTTP erreichen koennen.
|
||||
Fuer Pico ist ausgehender HTTPS-Zugriff auf `api.smart-me.com` erforderlich.
|
||||
Passwoerter gehoeren ausschliesslich in das dafuer vorgesehene Passwortfeld und
|
||||
niemals in Repository-Dateien, Skripte oder Screenshots.
|
||||
|
||||
## Abnahmecheckliste
|
||||
|
||||
- Die Instanz erreicht Status `102`, und `Stoerung` bleibt `false`.
|
||||
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
|
||||
Leistungsangebot `[0]`.
|
||||
- Nach dem Anstecken wird das Fahrzeug erkannt und bei unbekannten Phasen
|
||||
zunaechst der Mindeststrom von 6 A zur Erkennung angefordert.
|
||||
- Nach der Erkennungszeit werden eine oder drei Phasen relativ zum Pruefstrom
|
||||
gespeichert. Bei go-e koennen die einzelnen Phasenstroeme die Erkennung
|
||||
bereits vorher abschliessen.
|
||||
- Die erkannte Phasenzahl bleibt bei einer anschliessenden Ladepause stabil.
|
||||
- Die gemessene Leistung wird plausibel in `Istleistung` und `Ladestrom`
|
||||
abgebildet.
|
||||
- `Aktiv=false` oder `Ladefreigabe=false` stoppt die Ladestation.
|
||||
- PV mit `Solarladen=true` bietet `0 W` und die Ladestufen an.
|
||||
- PV mit `Solarladen=false` bietet nur die maximale Leistung an.
|
||||
- Peak mit `Solarladen=true` stoppt die Ladestation und bietet nur `[0]` an.
|
||||
- Peak mit `Solarladen=false` bietet `0 W` und die Ladestufen an.
|
||||
- Eine Manager-Vorgabe ausserhalb des gemeldeten Angebots wird abgewiesen.
|
||||
- Leistungswechsel-, Mindest-Ein- und Mindest-Aus-Zeiten werden im gemeldeten
|
||||
Angebot sichtbar und vom Manager eingehalten.
|
||||
- Nach Ablauf des Vorgabe-Timeouts gilt wieder das lokale Ersatzverhalten.
|
||||
- Im Debug-Protokoll und in `LetzterGeraetebefehl` erscheinen keine
|
||||
Zugangsdaten.
|
||||
|
||||
## Fehlersuche
|
||||
|
||||
- Status `201`: Geraetetyp und Pflichtfelder kontrollieren. Bei go-e darf die
|
||||
Adresse kein Protokoll, keinen Pfad und keine Parameter enthalten. Bei Pico
|
||||
muessen alle vier Zugangsfelder befuellt sein.
|
||||
- Status `202`: Erreichbarkeit, DNS, lokale Firewall und API-Antwort pruefen.
|
||||
`Stoertext` enthaelt den konkreten Kommunikations- oder JSON-Fehler.
|
||||
- Fahrzeug wird nicht erkannt: Rohstatus der Geraete-API kontrollieren. Der
|
||||
Adapter erwartet bei go-e `car` und `nrg[11]`, bei Pico `State` und
|
||||
`ActiveChargingPower`.
|
||||
- Phasenzahl bleibt laenger auf `0`: Waehrend der Erkennungsphase muessen
|
||||
Ladestation und Fahrzeug den angeforderten Maximalstrom tatsaechlich
|
||||
freigeben. Bei Pico muss fuer eine sichere Dreiphasenerkennung waehrend der
|
||||
Erkennung mehr als 7500 W anliegen; go-e kann zusaetzlich anhand der
|
||||
einzelnen Phasenstroeme erkennen.
|
||||
- Manager-Vorgabe wird abgewiesen: aktive Zuordnung im Manager sowie
|
||||
`LeistungsangebotDiagnose` und die Betriebsart kontrollieren.
|
||||
|
||||
## Tests
|
||||
|
||||
Die Unit- und Adaptertests verwenden einen injizierten Fake-HTTP-Transport.
|
||||
Damit werden fuer go-e alt, go-e Gemini und smart-me Pico Statusantworten,
|
||||
URL, HTTP-Methode, Authentisierung und Steueraufrufe ohne reale Hardware
|
||||
geprueft.
|
||||
|
||||
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
|
||||
API-Antworten. Er prueft alle drei Geraetevarianten, die Mindeststrom-Probe
|
||||
beim Anstecken, stabile Phasen waehrend einer Ladepause, die Schaltsperren,
|
||||
Fahrzeugstrom-Erkennung, die Leistungsangebote in PV und Peak sowie die
|
||||
Umschaltung ueber den `Solarladen`-Button.
|
||||
|
||||
PHP-Syntax, Struktur- und Unit-Tests des gesamten Repositorys:
|
||||
|
||||
```bash
|
||||
composer check
|
||||
```
|
||||
|
||||
Funktionstest nur fuer die Ladestation gegen IP-Symcon 8:
|
||||
|
||||
```bash
|
||||
tests/Symcon/bin/run-symcon-tests.sh single LadestationStandAlone
|
||||
```
|
||||
|
||||
Vollstaendiger Symcon-Modultestlauf:
|
||||
|
||||
```bash
|
||||
tests/Symcon/bin/run-symcon-tests.sh all
|
||||
```
|
||||
|
||||
Aufbau, Testvertrag und Ergebnisdateien sind unter
|
||||
[`docs/testing/README.md`](../../testing/README.md) beschrieben.
|
||||
@@ -0,0 +1,442 @@
|
||||
# Manager
|
||||
|
||||
> Status: Implementiert. Führt Hauptmanager und Peakshaving zusammen.
|
||||
|
||||
Der Manager liest die Netzleistung, verwaltet ausschliesslich die ausgewählten
|
||||
Verbraucher und verteilt Leistung im PV- oder Peak-Betrieb. Die Prioritäten
|
||||
kommen aus den Verbrauchermeldungen.
|
||||
|
||||
## Variablen
|
||||
|
||||
Die drei Regelungsvariablen `Aktiv`, `Betriebsart` und `Netzleistung` sind immer sichtbar. Die vom Manager gefuehrten Mess- und Energievariablen werden immer angelegt und archiviert, koennen aber gemeinsam ausgeblendet werden.
|
||||
|
||||
| Ident | Typ / Zugriff | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Aktiv` | Boolean / bedienbar | Regelung ein/aus; Start `false`. |
|
||||
| `Betriebsart` | String / Anzeige | `Inaktiv`, `PV` oder `Peak`. |
|
||||
| `Netzleistung` | Float / Anzeige | Aktuelle Netzleistung in W; positiv Bezug, negativ Einspeisung. |
|
||||
| `PVLeistungArchiv` | Float / Logging | Normalisierte PV-Leistung in W. |
|
||||
| `HausverbrauchLeistungArchiv` | Float / Logging | Normalisierter Hausverbrauch in W. |
|
||||
| `NetzleistungArchiv` | Float / Logging | Normalisierte Netzleistung in W; positiv Bezug, negativ Einspeisung. |
|
||||
| `BatterieleistungArchiv` | Float / Logging | Batterieleistung in W; positiv Laden, negativ Entladen. |
|
||||
| `PVEnergie` | Float / Zaehler | Integrierte PV-Erzeugung in kWh. |
|
||||
| `Hausenergie` | Float / Zaehler | Integrierter Hausverbrauch in kWh. |
|
||||
| `NetzbezugEnergie` | Float / Zaehler | Integrierter Netzbezug in kWh. |
|
||||
| `EinspeisungEnergie` | Float / Zaehler | Integrierte Einspeisung in kWh. |
|
||||
| `BatterieLadenEnergie` | Float / Zaehler | Integrierte Batterieladung in kWh. |
|
||||
| `BatterieEntladenEnergie` | Float / Zaehler | Integrierte Batterieentladung in kWh. |
|
||||
| `NetzleistungGueltig` | Boolean / Logging | Messquelle vorhanden und aktuell. |
|
||||
| `WirksameLastspitzengrenze` | Float / Logging | Aktuelle feste oder monatliche Bezugsgrenze in W. |
|
||||
| `WirksameEinspeisegrenze` | Float / Logging | Anlagenweite Einspeisegrenze in W. |
|
||||
| `Abregelbedarf` | Float / Logging | Aktuell erforderliche PV-Leistungsreduktion in W. |
|
||||
| `Wechselrichterstatus` | String/JSON / Logging | Verteilte Grenzen und Zustand der PV-Regelung. |
|
||||
| `Verteilbudget` | Float / Logging | Aktuell verfügbares Budget in W. |
|
||||
| `VerbraucherAnzahl` | Integer / Logging | Anzahl zugeordneter Verbraucher. |
|
||||
| `VerbraucherVerfuegbar` | Integer / Logging | Anzahl aktuell verfügbarer Verbraucher. |
|
||||
| `Verbraucherstatus` | String/JSON / Logging | Letzte vollständige Meldungen. |
|
||||
| `Prognosestatus` | String / Logging | `NichtVerwendet`, `Verbunden` oder `Fehler`. |
|
||||
| `SDLStatus` | String / Logging | Status des SDL/VGT-Anschlusses. |
|
||||
| `Lizenzstatus` | String / Logging | Status der Lizenzprüfung. |
|
||||
| `Stoerueberwachungsstatus` | String / Logging | Status der externen Störüberwachung. |
|
||||
| `Sammelstoerung` | Boolean / Logging | Eigene und weitergeleitete Störungen. |
|
||||
| `Stoertext` | String / Logging | Lesbare Sammelmeldung. |
|
||||
|
||||
## Properties
|
||||
|
||||
| Ident | Typ | Standard / Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Rolle` | Auswahl | `Alleine`; alternativ `Hauptmanager` oder `Untermanager`. |
|
||||
| `NetzleistungVariableID` | Integer | `0*`; gültige Messquelle vor Regelstart. |
|
||||
| `Netzleistungsfaktor` | Float | `1`; auf W und die vereinbarte Vorzeichenrichtung normieren. |
|
||||
| `MesswertMaxAlter` | Integer | `60` s; auf echte Messaktualisierung bezogen. |
|
||||
| `VerbraucherZuordnung` | String/JSON | `[]`; manuelle Auswahl aus Instanz-ID und Aktiv-Status. |
|
||||
| `AutomatischeVerbraucherZuordnung` | String/JSON | `[]`; Auswahl aus automatisch gefundenen Verbrauchern. |
|
||||
| `AutomatischeSuche` | Boolean | `true`; zeigt gefundene Verbraucher zur gezielten Auswahl. |
|
||||
| `SuchbereichID` | Integer | `0`; optionaler Suchbereich für die automatische Suche. |
|
||||
| `KeepAlive` | Integer | `60` s; erneuert laufende Vorgaben zyklisch. |
|
||||
| `VerbraucherTimeout` | Integer | `60` s; erkennt ausgebliebene Verbrauchermeldungen. |
|
||||
| `Lastspitzenmodus` | Auswahl | `Aus`; alternativ `Konstant` oder `Monatlich`. |
|
||||
| `Lastspitzengrenze` | Float | Feste Grenze in W für Modus `Konstant`. |
|
||||
| `Monatsgrenzen` | String/JSON | Editierbare Liste mit zwölf Monatswerten in W. |
|
||||
| `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. |
|
||||
| `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
|
||||
| `EinspeisebegrenzungAktiv` | Boolean | `false`; gemeinsame Exportgrenze am Netzanschlusspunkt aktivieren. |
|
||||
| `Einspeisegrenze` | Float | Maximale Einspeisung der Gesamtanlage in W; `0` bedeutet Nulleinspeisung. |
|
||||
| `PrognoseAktiv` | Boolean | `false`; Prognosetelemetrie und Topologiesynchronisation mit `forecast_pv` plus `forecast_load` oder `grid_schedule` aktivieren. |
|
||||
| `NetzfahrplanAktiv` | Boolean | `false`; lizenzierten, tarif- und prognosebasierten Netzzielwert verwenden. |
|
||||
| `NetzbezugEnergieVariableID` | Integer | Optionaler kumulativer Netzbezugszaehler. |
|
||||
| `NetzeinspeisungEnergieVariableID` | Integer | Optionaler kumulativer Einspeisezaehler. |
|
||||
| `NetzbezugEnergiefaktor`, `NetzeinspeisungEnergiefaktor` | Float | Umrechnung der Netzenergiezaehler nach kWh. |
|
||||
| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Nennleistung, Leistungs- und Erzeugungsenergiemessung sowie optionalem Begrenzungsregister. |
|
||||
| `AnlagenPVFlaechen` | String/JSON | `[]`; PV-Flaechen mit DC-Leistung, Ausrichtung und Wechselrichter-ID. |
|
||||
| `AnlagenBatterien` | String/JSON | `[]`; Batteriespeicher mit Kapazitaeten, Kopplung, Leistung, SOC sowie Lade- und Entladeenergiezaehlern. |
|
||||
| `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. |
|
||||
| `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. |
|
||||
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
|
||||
| `SDLAktiv` | Boolean | `false`; separat gemessene SDL-/Regelenergie in der Anlagenbilanz beruecksichtigen. Keine SDL-Steuerung. |
|
||||
| `SDLLeistungVariableID` | Integer | `0`; bei aktiver SDL erforderliche numerische Istleistungsquelle. |
|
||||
| `SDLLeistungsfaktor` | Float | `1`; Umrechnung nach W, positiv Laden/Bezug, negativ Entladen/Abgabe. Fuer kW `1000`, bei umgekehrtem Vorzeichen negativ. |
|
||||
| `SDLSOCVariableID` | Integer | `0`; optionaler separater SDL-Ladezustand in Prozent, 0 bis 100. |
|
||||
| `SDLEnergieflussAnzeigen` | Boolean | `false`; SDL mit optionalem SOC als eigenen Speicher im aktivierten Energiefluss anzeigen. |
|
||||
| `SDLDiagrammeAnzeigen` | Boolean | `false`; SDL-Leistung, Lade-/Entladeenergie und optionalen SOC in aktivierten Diagrammen anzeigen. |
|
||||
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. |
|
||||
| `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. |
|
||||
| `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. |
|
||||
| `LoggingEin` | Boolean | `false`; laufende Meldungen im Debug-Fenster ausgeben. |
|
||||
| `EnergieaufzeichnungAktiv` | Boolean | `true`; managergefuehrte Leistungs- und Energieaufzeichnung aktivieren. |
|
||||
| `MesswerteAnzeigen` | Boolean | `false`; einzelne Manager-Messwerte in der Objektstruktur anzeigen. |
|
||||
| `EnergyPieAnzeigen` | Boolean | `false`; Energy Pie unter dem Manager anlegen oder entfernen. |
|
||||
| `EnergiediagrammeAnzeigen` | Boolean | `false`; native Leistungs- und Energiediagramme anlegen oder entfernen. |
|
||||
| `FunFactsAnzeigen` | Boolean | `false`; responsive Energy Facts anlegen oder entfernen. |
|
||||
| `EnergieflussAnzeigen` | Boolean | `false`; native Energy Distribution anlegen oder entfernen. |
|
||||
| `LeistungsaufzeichnungMinuten` | Auswahl | `1`; Verdichtung auf 1 Minute, 5 Minuten oder 1 Stunde. |
|
||||
| `LeistungLoeschenMonate` | Integer | `12`; Leistungswerte nach zwoelf Monaten loeschen, `0` deaktiviert die Loeschung. |
|
||||
| `EnergieaufzeichnungMinuten` | Auswahl | `5`; Verdichtung auf 1 Minute, 5 Minuten oder 1 Stunde. |
|
||||
| `EnergieVerdichtenMonate` | Integer | `12`; Energiezaehler nach zwoelf Monaten auf Tageswerte verdichten. |
|
||||
| `EnergieLoeschenMonate` | Integer | `0`; Energiezaehler nicht loeschen. |
|
||||
|
||||
Es gibt keine Sollwertquellenauswahl und keine zweite Prioritätseinstellung im
|
||||
Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt.
|
||||
Die Monatsgrenzen werden in den Grundeinstellungen über eine Schaltfläche
|
||||
ein- und ausgeblendet. Die automatische Verbrauchersuche kann dort erneut
|
||||
ausgeführt werden, ohne andere ungespeicherte Formulareingaben zu verlieren.
|
||||
|
||||
## Energieaufzeichnung und Visualisierung
|
||||
|
||||
Sobald Netzleistung und mindestens eine PV-Istleistung in der Anlagentopologie
|
||||
konfiguriert sind, tastet der Manager die Quellen minuetlich ab. PV-Leistung ist
|
||||
die Summe der den PV-Flaechen zugeordneten Wechselrichter. Batterieleistung und
|
||||
der kapazitaetsgewichtete SOC stammen aus den Batteriespeichern. Positive
|
||||
Batterieleistung bedeutet Laden, negative Entladen. Der Hausverbrauch wird als
|
||||
`PV + Netz - Batterie` bilanziert. Sind Netzbezugs-, Einspeise-, PV-Erzeugungs-
|
||||
sowie gegebenenfalls Batterie-Lade- und Entladezaehler vollstaendig hinterlegt,
|
||||
verwendet der Manager deren Deltas fuer die Energieaufzeichnung. Die Hausenergie
|
||||
wird dann als `PV + Netzbezug + Batterieentladung - Einspeisung - Batterieladung`
|
||||
berechnet. Ohne vollstaendigen Zaehlerdatensatz integriert der Manager weiterhin
|
||||
die Leistungswerte. Dieselben Quellen versorgen ohne zweite Eingabe auch die
|
||||
Prognosetelemetrie. Ausfallluecken ueber fuenf Minuten werden bei der
|
||||
Leistungsintegration nicht nachberechnet.
|
||||
|
||||
Batteriespeicher bleiben als physische Komponenten samt Messquellen in der
|
||||
Anlagentopologie. Ein zusaetzliches Batterieverbrauchermodul beschreibt dagegen
|
||||
Regelung, Leistungsangebot und Betriebszustand; beide Rollen sind bewusst
|
||||
getrennt.
|
||||
|
||||
Die eigenen Variablen werden automatisch im ersten Archive Control aktiviert.
|
||||
Leistungen verwenden die Standardaggregation, Energievariablen den Zaehlermodus.
|
||||
Standardmaessig werden Leistungen auf eine Minute verdichtet und nach zwoelf
|
||||
Monaten geloescht. Energie wird auf fuenf Minuten verdichtet, nach zwoelf
|
||||
Monaten auf Tageswerte reduziert und nie geloescht. Aenderungen an diesen
|
||||
Regeln werden idempotent ueber die Archiv-API gesetzt und reaggregiert.
|
||||
|
||||
Vier Schalter im Bereich `Energieaufzeichnung` verwalten die
|
||||
Visualisierungen idempotent. Beim Einschalten werden die Objekte direkt unter
|
||||
dem Manager angelegt oder aktualisiert. Beim Ausschalten werden ausschliesslich
|
||||
die vom Manager anhand ihrer festen Kennung und ihres Typs erkannten Objekte
|
||||
entfernt beziehungsweise die native Energy Distribution ausgeblendet:
|
||||
|
||||
- Energy Pie aus Enelix Utils mit den vier relevanten Energiezaehlern,
|
||||
- zwei native IP-Symcon-Diagrammmedien fuer Leistungen und Energien,
|
||||
- responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten,
|
||||
- native Energy Distribution fuer PV, Netz, Haus und Batterie.
|
||||
|
||||
## SDL / Regelenergie
|
||||
|
||||
`SDL aktiv` blendet die Quellen- und Anzeigeauswahl ein. Die SDL-Istleistung
|
||||
beschreibt einen **separat gemessenen AC-Zweig am selben Netzanschlusspunkt**.
|
||||
Sie darf weder in der normalen Batterieleistung noch in einer PV-Leistung
|
||||
enthalten sein. Eine identische Variablen-ID fuer SDL und Netz/PV/Batterie wird
|
||||
abgewiesen. Ueberschneidungen in extern gebildeten Summen muss die
|
||||
Anlagenkonfiguration ausschliessen. Ein gemeinsamer Speicher mit nur einem
|
||||
Gesamtleistungsmesswert kann damit nicht in EMS- und SDL-Anteile zerlegt werden.
|
||||
|
||||
Bei aktiver SDL lautet die Hauslast `max(0, PV + Netz - Batterie - SDL)`.
|
||||
Die Korrektur geschieht vor der Begrenzung auf null und unabhaengig davon,
|
||||
ob SDL angezeigt wird. Diese Hauslast wird an den Prognosedienst gesendet;
|
||||
Netzleistung und der kapazitaetsgewichtete SOC der normalen Batterien bleiben
|
||||
unveraendert. SDL-SOC ersetzt keinen Batterie-SOC und wird nicht als
|
||||
planbare EMS-Speicherkapazitaet exportiert. Ungueltige SDL-Leistungswerte
|
||||
unterbrechen die Messaufzeichnung und den jeweiligen Telemetrieversand,
|
||||
statt als null in die Hauslast einzugehen. Der SDL-Status ist bei aktiver
|
||||
SDL auch ohne eingeschaltete Diagnosevariablen sichtbar.
|
||||
|
||||
`SDLLeistungArchiv` zeichnet Watt auf, `SDLSOCArchiv` gueltige Prozentwerte.
|
||||
`SDLLadenEnergie` und `SDLEntladenEnergie` integrieren die getrennten Richtungen
|
||||
in kWh. Die Abtastung erfolgt wie bei den anderen Leistungen minuetlich, nicht
|
||||
als abrechnungsgenaue SDL-Energiemessung. Auch bei vorhandenen physikalischen
|
||||
Energiezaehlern wird die Hausenergie um die im selben Intervall integrierte
|
||||
SDL-Ladung vermindert und um SDL-Entladung erhoeht. Bei Messluecken ueber
|
||||
fuenf Minuten, Quellenwechseln oder Zaehlerresets wird keine unvollstaendige
|
||||
Hausenergie nachberechnet; Historie und bereits erfasste Zaehler bleiben erhalten.
|
||||
|
||||
Die Anzeigeoptionen wirken beim Anwenden direkt auf die vorhandenen
|
||||
Manager-Visualisierungen. Nur die eigenen SDL-Knoten und -Datensaetze werden
|
||||
ergaenzt oder entfernt. Andere Knoten, Farben, Datensaetze und sonstige
|
||||
Anpassungen bleiben bestehen. Im Energiefluss verwendet SDL dieselbe W/kW-
|
||||
Einheit wie die anderen Manager-Knoten; ein ungueltiger SOC wird dort nicht
|
||||
als aktueller Zusatzwert angezeigt. Ohne separaten SDL-Knoten enthaelt der
|
||||
Manager-Sammelknoten Batteriespeicher die normale Batterie plus SDL, damit
|
||||
die dargestellte Leistungsbilanz vollstaendig bleibt. Im Leistungsdiagramm
|
||||
erscheint die signierte SDL-Istleistung, im Energiediagramm erscheinen Laden
|
||||
und Entladen getrennt. SOC verwendet in beiden Diagrammen eine eigene rechte
|
||||
Prozentachse. Die allgemeinen Schalter fuer Energiefluss, Diagramme und
|
||||
Energieaufzeichnung bleiben erforderlich.
|
||||
|
||||
Migration: SDL bleibt nach dem Modulupdate standardmaessig aus. Bestehende
|
||||
Installationen behalten damit ihre bisherige Bilanz. Erst Quellen und
|
||||
Vorzeichen pruefen, SDL aktivieren und anwenden. Historische Hauslasten oder
|
||||
bereits trainierte Prognosen werden nicht rueckwirkend korrigiert; neue
|
||||
Prognoseeingaben sind ab Aktivierung bereinigt. Ausschalten entfernt nur
|
||||
SDL-Anzeigen, nicht die aufgezeichnete Historie. Der alte `SDLAnschluss`
|
||||
bleibt lesbar; externe SDL-Regelung und Stellbefehle werden nicht veraendert.
|
||||
|
||||
Native Darstellungsvertraege: [Energy Distribution](https://www.symcon.de/en/service/documentation/module-reference/energy/energy-distribution/)
|
||||
und [Diagramme](https://www.symcon.de/en/service/documentation/basics/media/charts/).
|
||||
|
||||
## Anlagenweite Einspeisebegrenzung
|
||||
|
||||
Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der
|
||||
Messung am Netzanschlusspunkt. Fuer jeden regelbaren PV- oder Hybridwechselrichter
|
||||
werden die Istleistungsvariable und eine bedienbare Begrenzungsvariable
|
||||
konfiguriert. Der Stellwert kann als Prozent der AC-Nennleistung oder als
|
||||
absolute Leistung in Watt ausgegeben werden.
|
||||
|
||||
Der Manager berechnet eine einzige Grenze fuer die Gesamtanlage. Beim Abregeln
|
||||
verteilt er sie nach der tatsaechlichen Erzeugung, beim Freigeben nach der
|
||||
AC-Nennleistung auf alle angebotenen Wechselrichter. Jede gemessene Abweichung
|
||||
wird stufenlos nachgefuehrt; ein zusaetzliches Toleranzfenster wird nicht
|
||||
verwendet. Beim Abschalten des Managers oder der Funktion werden zuvor gesetzte
|
||||
Grenzen kontrolliert auf die jeweilige
|
||||
Nennleistung zurueckgesetzt. Wechselrichter ohne beide Register werden nicht
|
||||
geregelt; sobald eines der beiden Register gesetzt ist, muessen beide gueltig
|
||||
sein und das Begrenzungsregister eine IP-Symcon-Aktion besitzen.
|
||||
|
||||
Der intelligente Netzfahrplan verwendet die PV-, Verbrauchs- und
|
||||
Netzleistungsprognose sowie Tarife und Batteriespeicher. Liegt im
|
||||
Prognosezeitraum mehr PV-Ertrag als zulaessige Einspeisung vor, wird dieses
|
||||
Potenzial als flexibler Verbrauch beziehungsweise Speicherladung eingeplant.
|
||||
Damit werden Verbraucher in ertragreiche Zeitfenster verschoben, bevor der
|
||||
harte Anlagenregler die Wechselrichter reduziert. Ein fehlender oder
|
||||
abgelaufener Fahrplan fuehrt automatisch zum konfigurierten
|
||||
`SollwertSolarladen` zurueck; die harte Einspeisebegrenzung bleibt unabhaengig
|
||||
davon aktiv.
|
||||
|
||||
## Anlagentopologie und Prognoseexport
|
||||
|
||||
Der Manager ist die fuehrende Quelle fuer die technischen Stammdaten. Die
|
||||
Konfiguration trennt Wechselrichter, PV-Flaechen und Batteriespeicher. AC-Leistung
|
||||
wird am Wechselrichter, DC-Leistung an der PV-Flaeche gepflegt. Individuelle
|
||||
AC-Einspeise- und Bezugsgrenzen werden nicht erfasst, weil die Begrenzung
|
||||
anlagenweit am Netzanschlusspunkt erfolgt. Modulanzahl und Modulleistung sind
|
||||
optional und muessen gemeinsam gesetzt werden.
|
||||
|
||||
PV-Flaechen und Batterien referenzieren ihren Wechselrichter ueber dessen stabile
|
||||
ID. Bei `hybrid` koennen beide denselben Wechselrichter verwenden. Der Export
|
||||
`ENELIX_AnlagentopologieExportieren($InstanzID)` weist dann eine gemeinsame
|
||||
AC-Grenze aus, damit PV und Batterie die Nennleistung nicht unabhaengig doppelt
|
||||
beanspruchen.
|
||||
|
||||
Der JSON-Export enthaelt die Vertragsversion `1.0`, die Lizenz-Installations-ID,
|
||||
die drei Komponentenlisten, gemeinsame AC-Nennleistungen und aggregierte Summen
|
||||
fuer die schrittweise Prognoseanbindung. Interne Symcon-Variablen-IDs werden
|
||||
nicht exportiert. Tarif- und Variantenparameter bleiben im Prognoseportal. Bei
|
||||
aktivierter Prognose wird eine nichtleere Topologie beim Speichern automatisch ueber
|
||||
einen separaten, widerrufbaren Installationszugang synchronisiert. Der
|
||||
Lizenzserver liefert diesen Zugang nur auf ausdrueckliche Geraeteanforderung;
|
||||
der Manager entfernt ihn vor dem Speichern aus der Lease und haelt ihn in einem
|
||||
internen Attribut. Der Lizenzcode wird nicht als API-Token verwendet. Ein
|
||||
Synchronisationsfehler erscheint im `Prognosestatus`, blockiert die lokale
|
||||
EMS-Regelung aber nicht.
|
||||
|
||||
Der Forecast-Schalter zeigt direkt an, ob fuer die Manager-ID eine passende
|
||||
Kombination `forecast_pv` plus `forecast_load` oder `grid_schedule` vorhanden ist. Nach der Aktivierung sendet der
|
||||
Manager PV-Leistung, berechneten Hausverbrauch, Netzleistung und Batterie-SOC
|
||||
mit UTC-Zeitstempel im eingestellten Sendeintervall. Dieses Intervall bestimmt
|
||||
also, wie oft aktuelle Messwerte zum Prognosedienst hochgeladen werden; es ist
|
||||
kein Regelintervall. Das Minimum von 60 Sekunden
|
||||
passt zum Rate-Limit des Lizenzportals. Die Uebertragung nutzt denselben
|
||||
widerrufbaren Installationszugang wie die Topologie; Lizenzcode und Geraet Token
|
||||
werden weder als Telemetriefelder noch im Debug-Log ausgegeben. Bei HTTP 401
|
||||
oder 403 verwirft der Manager den Geraetezugang und fordert ihn bei der naechsten
|
||||
Lizenzpruefung neu an.
|
||||
|
||||
Die Entscheidung und ihre Alternativen sind in
|
||||
[ADR 0005](../../adr/0005-anlagentopologie-fuer-prognosen.md) dokumentiert.
|
||||
|
||||
## Lizenzierung
|
||||
|
||||
Der Manager arbeitet nur mit einer gueltigen Manager-Lizenz. Das Lizenzfeld steht
|
||||
zuoberst im Konfigurationsformular. Ohne Freigabe bleibt die Instanz mit Status
|
||||
`203` inaktiv und sendet keine Leistungsvorgaben.
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
- Der Auftrag im Enelix-Lizenzportal ist bezahlt und enthaelt eine aktive
|
||||
Manager-Berechtigung.
|
||||
- IP-Symcon erreicht `https://license.enelix.ch` ueber HTTPS (Port 443).
|
||||
- Der Lizenzcode liegt im Format `ENX-XXXX-XXXX-XXXX-XXXX` vor.
|
||||
|
||||
### Lizenz aktivieren
|
||||
|
||||
1. Manager-Konfiguration in IP-Symcon oeffnen.
|
||||
2. Lizenzcode im Bereich `Lizenzierung` eintragen.
|
||||
3. `Lizenz pruefen und binden` ausloesen.
|
||||
4. Die erfolgreiche Freigabe am angezeigten Lizenzstatus kontrollieren.
|
||||
5. Die Manager-Konfiguration mit `Uebernehmen` beziehungsweise `OK` speichern,
|
||||
damit der eingegebene Lizenzcode als Property erhalten bleibt.
|
||||
|
||||
Beim ersten Anlegen erzeugt die Manager-Instanz eine UUIDv4 als stabile
|
||||
Installations-ID. Die Aktivierung sendet ausschliesslich `code` und
|
||||
`installationId` per
|
||||
`POST https://license.enelix.ch/api/v1/licenses/activate`. Sie benoetigt weder
|
||||
Portal-Cookies noch einen CSRF-Token. Derselbe Code kann von derselben
|
||||
Installation erneut abgerufen werden; die Bindung an eine andere Installation
|
||||
wird vom Lizenzserver abgelehnt.
|
||||
|
||||
### Berechtigungen
|
||||
|
||||
| Berechtigung | Freigegebene Funktion |
|
||||
| --- | --- |
|
||||
| `manager_standard` | Manager-Grundregelung ohne Peak Shaving. |
|
||||
| `manager_peak` | Bezugs- und Einspeisebegrenzung am Netzanschlusspunkt. |
|
||||
| `forecast_pv` | PV-Ertragsprognose; interne Modellvarianten bleiben verborgen. |
|
||||
| `forecast_load` | Verbrauchsprognose; interne Modellvarianten bleiben verborgen. |
|
||||
| `grid_schedule` | Intelligenter Netzfahrplan inklusive PV- und Verbrauchsprognose sowie Vermeidung von Abregelung. |
|
||||
|
||||
Wird mit `manager_standard` ein Lastspitzenmodus aktiviert, bleibt der Manager
|
||||
mit dem Hinweis `Peak Shaving ist nicht lizenziert` gesperrt. `manager_peak`
|
||||
gilt zugleich als Berechtigung fuer die Grundregelung.
|
||||
|
||||
### Erneuerung und Offline-Betrieb
|
||||
|
||||
Die erfolgreiche Serverantwort wird als Lease in der Manager-Instanz gespeichert.
|
||||
Der Manager erneuert sie ab `refreshAfter` automatisch ueber denselben
|
||||
Aktivierungsendpunkt. Schlaegt eine Erneuerung fehl, wird fruehestens nach einer
|
||||
Stunde erneut angefragt. Eine bereits bestaetigte Entwicklungsfreigabe bleibt
|
||||
bis `offlineUntil` verwendbar. Der aktuelle Entwicklungsvertrag setzt diesen
|
||||
Zeitpunkt ungefaehr 14 Tage nach Ausstellung. Danach sperrt der Manager die
|
||||
Regelung, bis der Lizenzserver wieder eine gueltige Antwort liefert.
|
||||
|
||||
Die Installations-ID und die Lease liegen in internen Instanzattributen. Die
|
||||
ID wird erst nach dem Laden bestehender Attribute initialisiert und bleibt bei
|
||||
Modulupdates, Modul-Neuladen und einem normalen Neustart unveraendert. Bei einer
|
||||
Migration muss trotzdem die vollstaendige Manager-Instanz mitsamt ihren
|
||||
Attributen uebernommen werden. Eine neu erzeugte Instanz erhaelt eine andere
|
||||
Installations-ID und kann einen bereits gebundenen Code nicht selbststaendig
|
||||
uebertragen. Wurde die ID mit einer aelteren Manager-Version bereits ungewollt
|
||||
geaendert, muss im Lizenzportal einmalig ein Ersatzcode erzeugt und an die nun
|
||||
stabile ID gebunden werden.
|
||||
|
||||
### Status und Fehlerbehebung
|
||||
|
||||
| Anzeige / Serverstatus | Bedeutung und Massnahme |
|
||||
| --- | --- |
|
||||
| `Lizenzcode fehlt.` | Code eintragen, pruefen und die Konfiguration speichern. |
|
||||
| `Lizenzcode ist ungueltig.` | Format und Zeichen des Codes kontrollieren. |
|
||||
| HTTP `404` | Code unbekannt oder zugehoeriger Auftrag noch nicht bezahlt. |
|
||||
| HTTP `409` | Code ist bereits an eine andere Installation gebunden. |
|
||||
| HTTP `429` | Zu viele Aktivierungsversuche; vor dem naechsten Versuch warten. |
|
||||
| `Lizenzserver nicht erreichbar` | DNS, Internetzugang, HTTPS und Systemzeit des Symcon-Systems pruefen. Eine bestehende Lease gilt nur bis `offlineUntil`. |
|
||||
| `Offline-Freigabe ist abgelaufen.` | Verbindung zum Lizenzserver wiederherstellen und Lizenz erneut pruefen. |
|
||||
| `Keine Prognoseberechtigung vorhanden` | Im Lizenzportal entweder PV-Ertragsprognose zusammen mit Verbrauchsprognose oder den Intelligenten Netzfahrplan ergaenzen. Anschliessend im Manager `Lizenz pruefen und binden` ausloesen, die Prognose aktivieren und die Konfiguration speichern. |
|
||||
|
||||
Fuer eine genauere Diagnose koennen die Diagnosevariablen eingeblendet werden.
|
||||
`Lizenzstatus` zeigt dann den aktuellen Zustand. Mit aktiviertem Debug-Logging
|
||||
werden Fehlermeldungen der Lizenzpruefung ausgegeben, niemals jedoch der
|
||||
Lizenzcode selbst.
|
||||
|
||||
### Datenschutz und Entwicklungsstand
|
||||
|
||||
Der Lizenzcode wird als Manager-Property in der IP-Symcon-Konfiguration
|
||||
gespeichert. Fuer den internen Abgleich mit der Lease verwendet der Manager
|
||||
zusaetzlich nur einen SHA-256-Hash und schreibt den Code nicht ins Debug-Log.
|
||||
Die aktuelle Serverantwort ist ein Entwicklungsvertrag mit
|
||||
`development: true` und noch nicht kryptografisch signiert. Ein optionaler
|
||||
Geraete-Public-Key sowie Challenge-, Heartbeat- oder separate
|
||||
Entitlement-Endpunkte werden vom Manager derzeit bewusst nicht verwendet.
|
||||
|
||||
## Verteilalgorithmus
|
||||
|
||||
Der Manager bildet das Verteilbudget aus der aktuellen Netzleistung, dem Ziel
|
||||
der aktiven Betriebsart und der aktuellen Leistung aller frisch gemeldeten
|
||||
Verbraucher. Eine gültige Istleistung wird bevorzugt; fehlt sie, bleibt der im
|
||||
Zustand bestätigte Sollwert konservativ reserviert.
|
||||
|
||||
Nicht verfügbare oder aktuell angebotlose Verbraucher behalten ihre Leistung
|
||||
und erhalten keine neue Vorgabe. Jeder verfügbare, synchronisierte Verbraucher
|
||||
mit einem nicht leeren Angebot erhält dagegen einen Sollwert aus diesem Angebot.
|
||||
Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
|
||||
wird mit genau `11000 W` zugeteilt.
|
||||
|
||||
Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
|
||||
sortiert. Zuerst reserviert der Manager die richtungsneutralen erlaubten Werte
|
||||
sowie die gemessene Leistung fester oder gesperrter Verbraucher.
|
||||
|
||||
Positives Restbudget wird innerhalb jeder Priorität schrittweise verteilt:
|
||||
Zuerst kommt die kleinste nächste erreichbare **absolute Sollleistung**, nicht
|
||||
die kleinste Erhöhung. Bei gleicher nächster Sollleistung entscheidet die exakt
|
||||
gezählte bezogene Energie in Wh, danach stabil die Instanz-ID. Die bisherigen
|
||||
2-kWh-Gruppen werden für diese Vergabe nicht mehr verwendet. Vom Budget wird nur
|
||||
die Differenz zum bereits zugeteilten Sollwert abgezogen. Ein nicht finanzierbarer
|
||||
Schritt wird übersprungen; andere passende Schritte und danach tiefere
|
||||
Prioritäten können das verbleibende Budget nutzen.
|
||||
|
||||
Beispiel bei gleicher Priorität: A bietet `[0,100,200,400,800]` bei 10 kWh,
|
||||
B bietet `[0,100,110,500,780,1500]` bei 12 kWh. Die Zuteilungsfolge bei
|
||||
ausreichendem Budget ist `A100, B100, B110, A200, A400, B500, B780, A800, B1500`.
|
||||
Die zugehörigen Gesamtbudgets sind 100, 200, 210, 310, 510, 900, 1180, 1580 und
|
||||
2300 W. Energie entscheidet nur bei gleichen nächsten Leistungsstufen, nicht
|
||||
mehr über die vollständige Versorgung eines Verbrauchers vor allen anderen.
|
||||
|
||||
Ganzzahlige Bereiche folgen derselben Reihenfolge wie einzelne Wattstufen,
|
||||
werden aber über gemeinsame Leistungsniveaus effizient verarbeitet. Lücken
|
||||
werden nie durch unzulässige Werte geschlossen. Bei negativem Restbudget bleibt
|
||||
die bestehende Defizitregelung einschliesslich ihrer Energiegruppen unverändert.
|
||||
Eine verbleibende Abweichung wird im `Verbraucherstatus` dokumentiert.
|
||||
|
||||
Für das Update sind keine neuen Properties oder eine Migration nötig. Bestehende
|
||||
Energiezähler, Sperrzeiten und Freigaben bleiben erhalten. Nach dem Modulupdate
|
||||
gilt die neue Vergabe beim nächsten Regellauf. Vor einem Update einer laufenden
|
||||
Anlage Einstellungen sichern und die neuen Sollwerte kontrolliert prüfen;
|
||||
die Git-Veröffentlichung allein aktualisiert keine laufende Installation.
|
||||
|
||||
Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
|
||||
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
|
||||
bleibt sie erhalten. `Umschaltdifferenz` verhindert zusätzlich einen Wechsel,
|
||||
wenn sich die beiden berechneten Korrekturen nicht ausreichend unterscheiden.
|
||||
|
||||
## Laufzeit und Fehlerverhalten
|
||||
|
||||
- Netzleistungsänderungen und Verbrauchermeldungen lösen die Berechnung aus.
|
||||
- Identische Sollwerte werden nur beim Keep-alive erneut gesendet.
|
||||
- Diagnosevariablen und laufendes Debug-Logging werden getrennt aktiviert.
|
||||
- Verbraucherpakete werden zentral geprüft und nur von aktiv zugeordneten
|
||||
Absendern angenommen.
|
||||
- Veraltete oder noch fehlende Verbrauchermeldungen werden nicht verteilt und
|
||||
als Sammelstörung ausgewiesen.
|
||||
- Bei fehlender oder veralteter Netzleistung bleibt die Regelung `Inaktiv` und
|
||||
sendet keine neuen Vorgaben.
|
||||
- Ohne gueltige Manager-Berechtigung bleibt der Manager mit Status `203` gesperrt.
|
||||
- Die Serverantwort wird lokal gespeichert und ab `refreshAfter` erneuert. Bei
|
||||
einem Verbindungsfehler gilt eine zuvor bestaetigte Entwicklungsfreigabe bis
|
||||
`offlineUntil`; danach wird die Regelung wieder gesperrt.
|
||||
- `manager_standard` erlaubt die Grundregelung. Ein aktiver Lastspitzenmodus
|
||||
benoetigt `manager_peak`.
|
||||
- Der Lizenzcode wird nie geloggt. Lokal wird fuer den Lease-Abgleich nur sein
|
||||
SHA-256-Wert gespeichert.
|
||||
- Die oberen Anschlüsse bleiben optional. Solange kein konkreter Adapter
|
||||
implementiert ist, meldet ein aktivierter Anschluss den Status `Fehler`, ohne
|
||||
die lokale EMS-Regelung zu blockieren.
|
||||
|
||||
## Schnittstellen
|
||||
|
||||
- Empfängt genau `VerbraucherdatenEmpfangen(array $daten)`.
|
||||
- Sendet an jeden Verbraucher nur Kopf und `Sollleistung_W`.
|
||||
- Obere Anschlüsse: siehe [Obere Anschlüsse](../../Obere-Anschluesse.md).
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Monatliche Batteriereserve aus dem alten Peakshaving-Modul übernehmen?
|
||||
- Anbieterformate für Prognose und Störüberwachung festlegen.
|
||||
- Produktive signierte Lizenz-Leases nach Abschluss des Entwicklungsvertrags integrieren.
|
||||
- Verhalten und Messabgrenzung bei Untermanagern im Anlagentest bestätigen.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Verbraucher Pufferspeicher
|
||||
|
||||
> Status: implementiert fuer IP-Symcon 8.0+ und den Enelix-2-Vertrag `4.0`.
|
||||
|
||||
Das Modul bindet elektrische Heizstufen eines Pufferspeichers an den
|
||||
Enelix-Manager an. Die Freigabe wird aus Puffer- und Aussentemperatur,
|
||||
Heizkurve, Hysterese, lokaler Aktivierung und aktueller Betriebsart berechnet.
|
||||
|
||||
Es ist eine gezielte Adaption des Enelix-1-Moduls `Puffer_Speicher`.
|
||||
Leistungsstufen, Temperaturquellen, optionale PT1-Glaettung und Heizkurve
|
||||
wurden uebernommen und ueberarbeitet. Zyklische Altlogik, alte
|
||||
Kommunikationsvariablen, `Idle` und `PowerSteps` als Symcon-Variable wurden
|
||||
verworfen.
|
||||
|
||||
| Technisches Merkmal | Wert |
|
||||
| --- | --- |
|
||||
| Modulname | `VerbraucherPufferspeicher` |
|
||||
| Alias | `Pufferspeicher` |
|
||||
| Modul-ID | `{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}` |
|
||||
| Enelix-Vertrag | `4.0` |
|
||||
|
||||
## Regelverhalten
|
||||
|
||||
Die Solltemperatur wird berechnet als:
|
||||
|
||||
`FusspunktVorlauftemperatur + HeizkurvenSteigung * (20 - Aussentemperatur)`
|
||||
|
||||
Anschliessend wird sie auf `HeizkurveMinimaltemperatur` und
|
||||
`HeizkurveMaximaltemperatur` begrenzt.
|
||||
|
||||
Die Einschaltschwelle wird je nach `MindesttemperaturModus` bestimmt:
|
||||
|
||||
- `Aus`: Solltemperatur minus Hysterese,
|
||||
- `Statisch`: konfigurierte absolute Mindesttemperatur,
|
||||
- `Differenz`: Solltemperatur minus konfigurierte Differenz.
|
||||
|
||||
Es gilt:
|
||||
|
||||
- im PV-Betrieb unter der Einschaltschwelle: `[0, ...Leistungsstufen]`,
|
||||
- im PV-Betrieb an oder oberhalb der Einschaltschwelle: `[0]`,
|
||||
- im Peakbetrieb unabhaengig von der Temperatur: `[0]`,
|
||||
- bei `Aktiv=false`: immer `[0]`,
|
||||
- bei ungueltigen Temperaturen: `[0]` und nicht verfuegbar.
|
||||
|
||||
Der Pufferspeicher ist im Peakbetrieb damit pauschal gesperrt. Beim Wechsel in
|
||||
Peak wird eine aktive Stufe sicher ausgeschaltet.
|
||||
|
||||
## Properties
|
||||
|
||||
Die Properties erscheinen im Formular in dieser Reihenfolge.
|
||||
|
||||
### Manager und Zeitverhalten
|
||||
|
||||
| Ident | Standard | Beschreibung |
|
||||
| --- | ---: | --- |
|
||||
| `PrioritaetPV` | `0` | Prioritaet im PV-Betrieb |
|
||||
| `PrioritaetPeak` | `0` | Prioritaet im Peakbetrieb |
|
||||
| `Meldeintervall` | `10 s` | Vollstaendige Rueckmeldung |
|
||||
| `VorgabeTimeout` | `120 s` | Gueltigkeit einer Vorgabe |
|
||||
| `LastwechselSperrzeit` | `5 s` | Mindestzeit zwischen Lastwechseln |
|
||||
|
||||
### Pufferspeicher
|
||||
|
||||
| Ident | Standard | Beschreibung |
|
||||
| --- | ---: | --- |
|
||||
| `LeistungsStufen` | `[]` | Stufe, Leistung und Boolean-Schaltkontakt |
|
||||
| `PufferfuehlerVariableID` | `0` | Integer- oder Floatvariable |
|
||||
| `AussentemperaturVariableID` | `0` | Integer- oder Floatvariable |
|
||||
|
||||
### Heizkurve
|
||||
|
||||
| Ident | Standard | Beschreibung |
|
||||
| --- | ---: | --- |
|
||||
| `FusspunktVorlauftemperatur` | `35.0 C` | Vorlauf bei 20 C aussen |
|
||||
| `HeizkurvenSteigung` | `1.0` | Anhebung je Kelvin fallender Aussentemperatur |
|
||||
| `HeizkurveMinimaltemperatur` | `20.0 C` | Untere Sollwertbegrenzung |
|
||||
| `HeizkurveMaximaltemperatur` | `80.0 C` | Obere Sollwertbegrenzung |
|
||||
| `Hysterese` | `5.0 K` | Abstand zur Einschaltschwelle |
|
||||
| `MindesttemperaturModus` | `Aus` | Aus, statisch oder Differenz |
|
||||
| `Mindesttemperatur` | `20.0 C` | Wert im statischen Modus |
|
||||
| `MindesttemperaturDifferenz` | `5.0 K` | Wert im Differenzmodus |
|
||||
| `WaermepumpenSolltemperaturVariableID` | `0` | Optionale Quelle fuer die WP-Uebernahme |
|
||||
|
||||
Der Button **Wert von Waermepumpe uebernehmen** berechnet den Fusspunkt so,
|
||||
dass die lokale Heizkurve beim aktuellen Aussenwert durch den aktuellen
|
||||
WP-Sollwert verlaeuft. Es entsteht keine dauerhafte Laufzeitkopplung.
|
||||
|
||||
### Erweiterte Einstellungen
|
||||
|
||||
| Ident | Standard | Beschreibung |
|
||||
| --- | ---: | --- |
|
||||
| `TemperaturMaxAlter` | `120 s` | Maximal zulaessiges Alter beider Messwerte |
|
||||
| `PuffertemperaturGlaetten` | `false` | Aktiviert PT1-Glaettung |
|
||||
| `ZeitKonstante` | `120 s` | PT1-Zeitkonstante |
|
||||
| `EinstellungenInVisu` | `false` | Gemeinsame Verbraucheroption |
|
||||
| `DiagnosevariablenAnzeigen` | `false` | Diagnosevariablen einblenden |
|
||||
| `LoggingEin` | `false` | Debug-Protokoll aktivieren |
|
||||
|
||||
## Variablen
|
||||
|
||||
Immer vorhanden:
|
||||
|
||||
| Ident | Typ | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Aktiv` | Boolean / bedienbar | Lokale Ein-/Aus-Freigabe |
|
||||
| `Puffertemperatur` | Float | Aktueller, gegebenenfalls geglaetteter Wert |
|
||||
| `Aussentemperatur` | Float | Heizkurveneingang |
|
||||
| `Solltemperatur` | Float | Berechneter und begrenzter Sollwert |
|
||||
| `Heizbedarf` | Boolean | Temperatur liegt unter der Einschaltschwelle |
|
||||
|
||||
Optional sichtbar sind die gemeinsamen Verbraucher-Diagnosevariablen sowie
|
||||
`AktiveStufe`, `BezogeneEnergie` und `TemperaturenGueltig`.
|
||||
|
||||
## Managerkommunikation
|
||||
|
||||
Das Modul implementiert `VerbraucherSchnittstelle` und Vertrag `4.0`.
|
||||
Es uebernimmt zuerst die vom Manager angekuendigte Betriebsart, berechnet das
|
||||
passende Leistungsangebot und meldet es mit derselben Betriebsart zurueck.
|
||||
Erst danach wird eine Sollleistung angenommen.
|
||||
|
||||
Die Rueckmeldung enthaelt zusaetzlich:
|
||||
|
||||
- `Puffertemperatur_C`,
|
||||
- `Aussentemperatur_C`,
|
||||
- `Solltemperatur_C`,
|
||||
- `Heizbedarf`,
|
||||
- `AktiveStufe`,
|
||||
- `Fuehlerfehler` und `Schaltfehler`.
|
||||
|
||||
## Sicherheit und Schaltung
|
||||
|
||||
Die Kontakte werden Break-before-make geschaltet: zuerst alle aus, danach
|
||||
hoechstens eine Stufe ein. Ein Schaltfehler fuehrt zu einem Ausschaltversuch
|
||||
aller Kontakte und einer Stoerungsmeldung. Hardwareseitige Verriegelungen,
|
||||
Temperaturbegrenzer und Schutzorgane bleiben erforderlich.
|
||||
|
||||
## Tests
|
||||
|
||||
- `PufferspeicherReglerTest` prueft Heizkurve, Hysterese, Ein/Aus, Peak und
|
||||
alle Mindesttemperatur-Modi.
|
||||
- `PufferspeicherModulstrukturTest` prueft Metadaten, Properties, Formular,
|
||||
Vertrag und Schaltfolge.
|
||||
- Der standardisierte Symcon-Test prueft die reale Modulinstanz und ist im
|
||||
gemeinsamen Manifest sowie in der Manager-Matrix registriert.
|
||||
@@ -0,0 +1,28 @@
|
||||
# EMS-Module und Modulentwürfe
|
||||
|
||||
> Manager, Warmwassererwaermer, Pufferspeicher, Batterie, Verbraucher 1-Stufig,
|
||||
> Waermepumpe, Ladestation Stand-Alone, Easee Gateway und Ladestation Gateway
|
||||
> sind als installierbare IP-Symcon-Module umgesetzt. Die weiteren Ordner
|
||||
> enthalten Besprechungsgrundlagen.
|
||||
|
||||
Alle steuerbaren Verbraucher verwenden die gemeinsamen Datenpunkte aus der
|
||||
[EMS-Schnittstelle](../Schnittstelle.md). In den Modul-READMEs stehen deshalb
|
||||
nur zusätzliche Properties, Variablen, Zustände und offene Punkte.
|
||||
|
||||
| Modul | Rolle |
|
||||
| --- | --- |
|
||||
| [Manager](Manager/README.md) | Leistungsverteilung und Anschlüsse (implementiert) |
|
||||
| [Batterie](Batterie/README.md) | Laden und Entladen eines Speichers (implementiert) |
|
||||
| [Wassererwärmer](Wassererwaermer/README.md) | Mehrstufige elektrische Erwärmung (implementiert) |
|
||||
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
||||
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher (implementiert) |
|
||||
| [Wärmepumpe](Waermepumpe/README.md) | Sperrkontakt oder SG Ready |
|
||||
| [Ladestation Stand-Alone](Ladestation-Stand-Alone/README.md) | Direkte Geräteanbindung (implementiert) |
|
||||
| [Ladestation Gateway](Ladestation-Gateway/README.md) | Eventbasierte Ladestation am Easee Gateway (implementiert) |
|
||||
| [Easee Gateway](Easee-Gateway/README.md) | Gemeinsame Easee-Kommunikation (implementiert) |
|
||||
|
||||
## Review-Regel
|
||||
|
||||
`0*` bezeichnet einen noch nicht eingerichteten Anlagenwert. Offene Punkte
|
||||
werden nicht durch Annahmen ersetzt. Erst nach Freigabe wird aus einem Entwurf
|
||||
ein Ordner mit `module.json`, `form.json`, `module.php` und Unit-Tests.
|
||||
@@ -0,0 +1,220 @@
|
||||
# Verbraucher 1-Stufig
|
||||
|
||||
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `4.0`.
|
||||
|
||||
Das Modul schaltet einen elektrischen Ein-/Aus-Verbraucher. Im frei
|
||||
schaltbaren Zustand meldet es bei PV `[0, Nennleistung]` und bei Peak `[0]`.
|
||||
Ist die Tagesmindestlaufzeit faellig, wird bei PV die Nennleistung erzwungen.
|
||||
Bei Peak ist konfigurierbar, ob `[0, Nennleistung]` angeboten oder die
|
||||
Nennleistung ohne Sperrmoeglichkeit erzwungen wird. Waerend einer
|
||||
Mindest-Ein- oder Mindest-Aus-Zeit sowie bei einer ausstehenden Rueckmeldung
|
||||
wird unabhaengig von der Betriebsart nur der aktuelle bestaetigte
|
||||
Leistungswert angeboten.
|
||||
|
||||
## Architekturentscheidung
|
||||
|
||||
Die Regelung ist ereignisbasiert. Neuberechnungen werden ausgeloest durch:
|
||||
|
||||
- eine neue Manager-Vorgabe,
|
||||
- eine Aenderung an Schaltkontakt oder optionaler Rueckmeldung,
|
||||
- das Ein- oder Ausschalten der lokalen EMS-Freigabe,
|
||||
- das Ende der zustandsabhaengigen Mindestzeit,
|
||||
- das Ablaufen einer Manager-Vorgabe,
|
||||
- den naechsten relevanten Zeitpunkt der Tageslaufzeitplanung.
|
||||
|
||||
Es gibt keinen Regelzyklus, kein `Interval`, keinen `IdleCounterMax` und
|
||||
keinen allgemeinen `Umschaltabstand`. `Meldeintervall` bleibt als
|
||||
vertraglich geforderte periodische Vollmeldung bestehen und steuert keine
|
||||
Regelberechnung.
|
||||
|
||||
## Properties
|
||||
|
||||
Zu den sechs gemeinsamen Verbraucher-Properties aus
|
||||
[`Schnittstelle.md`](../../Schnittstelle.md) kommen neun
|
||||
Modul-Properties hinzu.
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `Nennleistung` | Integer | `0` | Positive elektrische Leistung in W. |
|
||||
| `SchaltkontaktVariableID` | Integer | `0` | Boolean-Aktor mit Standard- oder benutzerdefinierter Aktion. |
|
||||
| `SchaltkontaktInvertiert` | Boolean | `false` | Kehrt die Ein-/Aus-Semantik des Aktors um. |
|
||||
| `RueckmeldungVariableID` | Integer | `0` | Optionale Boolean-Rueckmeldung; `true` bedeutet physisch eingeschaltet. |
|
||||
| `Mindestlaufzeit` | Integer | `0` | Geforderte Laufzeit pro lokalem Kalendertag in Sekunden, maximal 86400. |
|
||||
| `PeakSperreBeiMindestlaufzeitAnbieten` | Boolean | `true` | Bietet bei faelliger Tagesmindestlaufzeit im Peakbetrieb zusaetzlich `0` als Sperre an. Bei `false` wird die Nennleistung lokal erzwungen. |
|
||||
| `Mindesteinschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Ein-Zustand gehalten wird. |
|
||||
| `Mindestausschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Aus-Zustand gehalten wird. |
|
||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt gemeinsame und modulspezifische Diagnosevariablen an. |
|
||||
|
||||
Beide Mindestzeiten duerfen `0` sein. Die lokale Aktion `Aktiv=false`
|
||||
bleibt der einzige bewusste Override und schaltet sicher aus, auch wenn die
|
||||
Mindesteinschaltdauer noch laeuft.
|
||||
|
||||
`EinstellungenInVisu` ist Teil der gemeinsamen Verbraucherbasis. Das Modul
|
||||
besitzt derzeit keine zusaetzlichen bedienbaren Einstellvariablen und wertet
|
||||
diese Property deshalb nicht weiter aus.
|
||||
|
||||
## Variablen
|
||||
|
||||
Immer sichtbar:
|
||||
|
||||
| Ident | Typ / Zugriff | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Startwert `false`. |
|
||||
| `Schaltzustand` | Boolean / Anzeige | Bestaetigter oder aus dem Aktor berechneter Zustand. |
|
||||
| `Tageslaufzeit` | Integer / Anzeige | Bestaetigte Laufzeit des lokalen Kalendertags in Sekunden. |
|
||||
|
||||
Mit `DiagnosevariablenAnzeigen` werden zusaetzlich angelegt:
|
||||
|
||||
- `Istleistung`
|
||||
- `Leistungsquelle`
|
||||
- `Sollleistung`
|
||||
- `SollwertGueltig`
|
||||
- `Verfuegbar`
|
||||
- `AenderungMoeglich`
|
||||
- `Stoerung`
|
||||
- `Stoertext`
|
||||
- `Rueckmeldefehler`
|
||||
- `SchaltbefehlAusstehend`
|
||||
- `RestMindestzeit`
|
||||
|
||||
Die Regellogik arbeitet mit persistenten Attributen und ist nicht von der
|
||||
Sichtbarkeit der Diagnosevariablen abhaengig.
|
||||
|
||||
## Mindest-Ein- und Mindest-Aus-Zeit
|
||||
|
||||
Nach einer bestaetigten Einschaltung wird fuer
|
||||
`Mindesteinschaltdauer` Sekunden kein regulaeres Ausschalten angeboten. Nach
|
||||
einer bestaetigten Ausschaltung blockiert `Mindestausschaltdauer` ein
|
||||
regulaeres Einschalten.
|
||||
|
||||
Waerend einer Mindestzeit gilt:
|
||||
|
||||
- `AenderungMoeglich=false`,
|
||||
- `Schaltbereit=false`,
|
||||
- `RestMindestzeit_s` enthaelt die verbleibenden Sekunden,
|
||||
- `Leistungswerte_W` enthaelt nur die bestaetigte Istleistung.
|
||||
|
||||
Ein einmaliger Timer loest am Ende der Mindestzeit genau eine Neuberechnung
|
||||
aus. Der fruehere allgemeine `Umschaltabstand` ist vollstaendig entfallen.
|
||||
|
||||
## Rueckmeldung und Schaltbereitschaft
|
||||
|
||||
Ohne separate Rueckmeldung wird der logische Zustand der Aktorvariable als
|
||||
sofortige Schaltbestaetigung verwendet.
|
||||
|
||||
Mit `RueckmeldungVariableID` ist ausschliesslich deren Booleanwert fuer
|
||||
`Schaltzustand`, `Istleistung_W`, Tageslaufzeit und den Beginn der
|
||||
Mindestzeit massgeblich. Nach einem Aktorbefehl und vor der passenden
|
||||
Rueckmeldung meldet das Modul:
|
||||
|
||||
- die bisherige bestaetigte Istleistung,
|
||||
- `SchaltbefehlAusstehend=true`,
|
||||
- `Schaltbereit=false`,
|
||||
- `AenderungMoeglich=false`,
|
||||
- nur den bisherigen Leistungswert in `Leistungswerte_W`.
|
||||
|
||||
Sobald die Rueckmeldung den Zielzustand bestaetigt, beginnt die passende
|
||||
Mindest-Ein- oder Mindest-Aus-Zeit. Es gibt bewusst keinen zusaetzlichen
|
||||
zyklischen Rueckmelde-Timeout. Bleibt die Rueckmeldung aus, bleibt der
|
||||
Schaltbefehl sichtbar ausstehend.
|
||||
|
||||
Weichen Aktor- und Rueckmeldezustand ohne ausstehenden Schaltbefehl voneinander
|
||||
ab, wird `Rueckmeldefehler=true`, `Verfuegbar=false` und eine Stoerung
|
||||
gemeldet.
|
||||
|
||||
## Tagesmindestlaufzeit
|
||||
|
||||
Die Tageslaufzeit wird mit der in IP-Symcon eingestellten lokalen Zeitzone
|
||||
gefuehrt. Sommer- und Winterzeit sowie der Wechsel um Mitternacht werden
|
||||
beruecksichtigt. Nur der bestaetigte Ein-Zustand zaehlt.
|
||||
|
||||
Ist die konfigurierte Tagesmindestlaufzeit noch nicht erreicht, wird der
|
||||
Verbraucher zum spaetestmoeglichen Zeitpunkt angefordert, an dem die
|
||||
Restlaufzeit bis Mitternacht noch erfuellt werden kann. Im PV-Betrieb wird die
|
||||
Nennleistung dann lokal erzwungen. Im Peakbetrieb meldet das Modul bei
|
||||
aktivierter Property `PeakSperreBeiMindestlaufzeitAnbieten` wie Enelix 1
|
||||
`[0, Nennleistung]`, sodass der Manager zwischen Sperren und Nachholen der
|
||||
Laufzeit waehlen kann. Ist die Property deaktiviert, wird auch im Peakbetrieb
|
||||
nur die Nennleistung angeboten und lokal erzwungen. Eine noch aktive
|
||||
Mindestausschaltdauer hat Vorrang; eine unmoegliche Restlaufzeit kann nicht
|
||||
rueckwirkend nachgeholt werden.
|
||||
|
||||
## Managerkommunikation
|
||||
|
||||
Das Modul implementiert
|
||||
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
|
||||
ausschliesslich Vertrag `4.0`. Der Verbraucher besitzt keine
|
||||
Manager-ID-Property. Er akzeptiert nur Manager, in deren manueller oder
|
||||
automatischer Verbraucherzuordnung seine Instanz aktiv eingetragen ist.
|
||||
|
||||
Neben den gemeinsamen Vertragsfeldern werden diese Zustaende gemeldet:
|
||||
|
||||
- `Schaltzustand`
|
||||
- `SchaltbefehlAusstehend`
|
||||
- `Schaltbereit`
|
||||
- `RestMindestzeit_s`
|
||||
- `Tageslaufzeit_s`
|
||||
- `Rueckmeldefehler`
|
||||
|
||||
Rueckmeldungen erfolgen bei relevanten Ereignissen, kurz verzoegert nach einer
|
||||
Manager-Vorgabe und zusaetzlich alle `Meldeintervall` Sekunden. Eine Vorgabe
|
||||
wird nach `VorgabeTimeout` Sekunden ohne Erneuerung ungueltig.
|
||||
|
||||
## Installation und Inbetriebnahme
|
||||
|
||||
1. Im IP-Symcon Module Control den Testing-Branch `develop` der Bibliothek
|
||||
`https://git.belevo.ch/ENELIX/Enelix-EMS.git` installieren oder
|
||||
aktualisieren.
|
||||
2. Unter **Instanz hinzufuegen** nach **Verbraucher 1-Stufig** suchen und eine
|
||||
Instanz anlegen.
|
||||
3. `Nennleistung` in ganzen Watt eintragen.
|
||||
4. Als Schaltkontakt eine Booleanvariable mit funktionsfaehiger Aktion
|
||||
auswaehlen und bei Bedarf `SchaltkontaktInvertiert` aktivieren.
|
||||
5. Optional eine separate Boolean-Rueckmeldung auswaehlen. Dort muss
|
||||
`true` dem physisch eingeschalteten Verbraucher entsprechen.
|
||||
6. Tagesmindestlaufzeit, Mindest-Einschaltdauer und Mindest-Ausschaltdauer
|
||||
passend zum angeschlossenen Geraet festlegen.
|
||||
7. Die Instanz im Manager manuell aktiv zuordnen oder bei automatischer Suche
|
||||
in der gefundenen Liste aktivieren.
|
||||
8. Fuer die Erstpruefung `DiagnosevariablenAnzeigen` und bei Bedarf
|
||||
`LoggingEin` einschalten.
|
||||
9. Unter Aufsicht `Aktiv` einschalten und ueber den Manager je eine Ein- und
|
||||
Aus-Vorgabe pruefen.
|
||||
|
||||
## Abnahmecheckliste
|
||||
|
||||
- `Aktiv=false` schaltet den Ausgang aus.
|
||||
- Eine Ein-Vorgabe setzt zuerst den Aktor; mit separater Rueckmeldung bleibt
|
||||
`SchaltbefehlAusstehend=true`, bis diese einschaltet.
|
||||
- Die Mindesteinschaltdauer beginnt erst mit der Ein-Rueckmeldung.
|
||||
- Vor Ablauf der Mindesteinschaltdauer wird kein Ausschalten angeboten.
|
||||
- Nach bestaetigter Ausschaltung beginnt die Mindestausschaltdauer.
|
||||
- Vor Ablauf der Mindestausschaltdauer wird kein Einschalten angeboten.
|
||||
- `RestMindestzeit_s` erreicht 0 und `Schaltbereit` wird danach true.
|
||||
- `Tageslaufzeit` steigt nur bei bestaetigtem Ein-Zustand.
|
||||
- Ein unerwarteter Unterschied zwischen Aktor und Rueckmeldung erzeugt eine
|
||||
Stoerung.
|
||||
- Nach einem Neustart wird keine alte Manager-Vorgabe ungeprueft fortgesetzt.
|
||||
|
||||
## Tests
|
||||
|
||||
Nur dieses Modul:
|
||||
|
||||
```bash
|
||||
composer check:verbraucher-einstufig
|
||||
```
|
||||
|
||||
Gesamtes Repository inklusive dieser Modulsuite:
|
||||
|
||||
```bash
|
||||
composer check
|
||||
```
|
||||
|
||||
Die separate Suite und ihre Struktur sind unter
|
||||
[`tests/VerbraucherEinStufig`](../../../tests/VerbraucherEinStufig/README.md)
|
||||
dokumentiert.
|
||||
|
||||
## Migration
|
||||
|
||||
Die Zuordnung der Enelix-1- und bisherigen Enelix-2-Zeitwerte steht in
|
||||
[`docs/migration/Verbraucher-1-Stufig.md`](../../migration/Verbraucher-1-Stufig.md).
|
||||
@@ -0,0 +1,89 @@
|
||||
# Wärmepumpe
|
||||
|
||||
> Status: Implementiert.
|
||||
|
||||
Das Modul bindet eine Wärmepumpe über zwei schaltbare Boolean-Ausgänge in den
|
||||
Enelix-Manager ein. Die Ausgänge können als getrennte Kontakte für Sperre und
|
||||
Erhöhung oder als SG-Ready-Eingänge verwendet werden. Die Regelung arbeitet mit
|
||||
dem Nachrichtenvertrag 4.0 und meldet für PV und Peak ein
|
||||
zustandsabhängiges Leistungsangebot.
|
||||
|
||||
## Verhalten
|
||||
|
||||
- Im PV-Betrieb kann eine freigegebene Wärmepumpe aus dem Normalzustand in die
|
||||
Erhöhung geschaltet werden.
|
||||
- Im Peak-Betrieb kann eine laufende Wärmepumpe gesperrt werden, sofern
|
||||
Mindestlaufzeit und Sperrgrenzen dies erlauben.
|
||||
- Schaltvorgänge erfolgen als Break-before-Make: beide Kontakte werden zuerst
|
||||
deaktiviert, danach wird höchstens ein Kontakt aktiviert.
|
||||
- Nach einem erfolglosen PV-Anlauf wird die Erhöhung aufgehoben und bis zum
|
||||
Ablauf der Wiederholsperre nicht erneut angeboten.
|
||||
- Die maximale Sperrzeit am Stück und innerhalb von 24 Stunden erzwingt eine
|
||||
Erholungsphase im Normalbetrieb.
|
||||
- Ohne gültige Manager-Vorgabe fällt die Steuerung in den sicheren
|
||||
Normalzustand zurück.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
| Property | Standard | Beschreibung |
|
||||
| --- | ---: | --- |
|
||||
| `Kontaktart` | `0` | `0`: Sperre/Erhöhung, `1`: SG Ready |
|
||||
| `Kontakt1VariableID` | `0` | Schaltbarer Ausgang für Sperre beziehungsweise SG Ready 1 |
|
||||
| `Kontakt2VariableID` | `0` | Schaltbarer Ausgang für Erhöhung beziehungsweise SG Ready 2 |
|
||||
| `Kontakt1Invertiert` | `false` | Logik des ersten Ausgangs invertieren |
|
||||
| `Kontakt2Invertiert` | `false` | Logik des zweiten Ausgangs invertieren |
|
||||
| `Nennleistung` | `0 W` | Elektrische Nennleistung der Wärmepumpe |
|
||||
| `Rueckmeldungsart` | `0` | `0`: gemessene Leistung, `1`: Boolean-Betriebsstatus |
|
||||
| `IstleistungVariableID` | `0` | Leistungsmessung oder Betriebsrückmeldung |
|
||||
| `Laufschwelle` | `100 W` | Ab dieser Leistung gilt die Wärmepumpe als laufend |
|
||||
| `Anlaufwartezeit` | `30 s` | Wartezeit auf bestätigten Anlauf |
|
||||
| `Wiederholsperre` | `300 s` | Pause nach einem erfolglosen Anlauf |
|
||||
| `Mindestlaufzeit` | `1200 s` | Mindestlaufzeit nach bestätigtem Start |
|
||||
| `Mindestsperrzeit` | `1200 s` | Mindestdauer einer begonnenen Sperre |
|
||||
| `MaximaleSperrzeit` | `7200 s` | Maximale zusammenhängende Sperre; `0` deaktiviert |
|
||||
| `MaximaleSperrzeit24h` | `21600 s` | Maximale Sperrsumme in 24 Stunden; `0` deaktiviert |
|
||||
|
||||
Hinzu kommen die gemeinsamen Verbraucher-Properties für Prioritäten,
|
||||
Meldeintervall, Vorgabe-Timeout, Visualisierung und Logging.
|
||||
|
||||
## Variablen
|
||||
|
||||
| Ident | Typ | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `Waermepumpenzustand` | Integer | `1` normal/aus, `2` normal/laufend, `3` erhöht, `4` gesperrt |
|
||||
| `WaermepumpeLaeuft` | Boolean | Bestätigter Betriebsstatus |
|
||||
| `SperreAktiv` | Boolean | Sperrkontakt ist aktiv |
|
||||
| `ErhoehungAktiv` | Boolean | Erhöhungskontakt ist aktiv |
|
||||
| `SGReadyZustand` | Integer | Bitwert der beiden logischen Kontakte |
|
||||
| `Laufzeit` | Integer | Kumulierte bestätigte Laufzeit in Sekunden |
|
||||
|
||||
Optionale Diagnosevariablen zeigen unter anderem Ist- und Sollleistung,
|
||||
Verfügbarkeit, Anlaufstatus, Wiederholsperre, Sperrerholung, verbleibende
|
||||
Mindestzeiten und die Sperrsumme der letzten 24 Stunden.
|
||||
|
||||
## Adaption aus Enelix 1
|
||||
|
||||
| Bereich | Behandlung in Enelix 2 |
|
||||
| --- | --- |
|
||||
| Grundzustände Normal, Sperre und Erhöhung | angepasst übernommen |
|
||||
| Zwei physische Kontakte | angepasst und mit Invertierung sowie Rücklesekontrolle umgesetzt |
|
||||
| Leistungsangebot an den Manager | neu nach Nachrichtenvertrag 4.0 implementiert |
|
||||
| Mindestlauf- und Sperrzeiten | neu als ereignisbasierte Schutzlogik implementiert |
|
||||
| Wetter-, Wolken- und Sonnenaufgangslogik | nicht uebernommen |
|
||||
| Warmwasser-Schwellwert im Wärmepumpenmodul | nicht uebernommen |
|
||||
| Fester zyklischer Fünf-Sekunden-Regler | nicht uebernommen; Ereignisse und Einmal-Timer |
|
||||
|
||||
## Lizenzierung und Manager
|
||||
|
||||
Der Manager erkennt das Modul über die Modul-ID
|
||||
`{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}`. Es verwendet das bestehende
|
||||
Lizenzkontingent `consumer_single`, das im Portal als einstufiger Verbraucher
|
||||
mit Wärmepumpe als Beispiel angeboten wird.
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
Die beiden Ausgänge müssen unterschiedliche, schaltbare Boolean-Variablen sein.
|
||||
Die Rückmeldung ist verpflichtend: entweder eine numerische Leistungsmessung
|
||||
oder ein Boolean-Betriebsstatus. Bei ungültiger Konfiguration, fehlgeschlagenem
|
||||
Schalten oder widersprüchlicher Rücklesung setzt das Modul seinen Status auf
|
||||
Fehler und meldet sich dem Manager als nicht verfügbar.
|
||||
@@ -0,0 +1,406 @@
|
||||
# Verbraucher Warmwassererwaermer
|
||||
|
||||
> Status: implementiert fuer IP-Symcon 8.0+ und den Enelix-2-Vertrag `4.0`.
|
||||
|
||||
Das Modul bindet einen elektrischen Warmwasserspeicher mit einer oder mehreren
|
||||
exklusiven Leistungsstufen an den Enelix-Manager an. Der Manager kann nur eine
|
||||
der aktuell angebotenen Leistungen vorgeben. Mindesttemperatur, Hysterese,
|
||||
Zeitplan und Legionellenfunktion bestimmen dabei zustands- und
|
||||
betriebsartabhaengig das Leistungsangebot.
|
||||
|
||||
Das Modul ist eine gezielte Adaption des Enelix-1-Moduls
|
||||
`Boiler_x_Stufig`. Zyklische Altlogik und alte Kommunikationsvariablen wurden
|
||||
nicht uebernommen. Die Regelung arbeitet ereignisbasiert und verwendet
|
||||
ausschliesslich den Enelix-2-Nachrichtenvertrag.
|
||||
|
||||
| Technisches Merkmal | Wert |
|
||||
| --- | --- |
|
||||
| Modulname | `VerbraucherWarmwassererwaermer` |
|
||||
| Alias | `Wassererwärmer` |
|
||||
| Modul-ID | `{B7C54AF4-AD7D-4FE4-B75D-203693906251}` |
|
||||
| Modultyp | Geraeteinstanz (`3`) |
|
||||
| Funktionspraefix | `ENELIX` |
|
||||
| Enelix-Vertrag | `4.0` |
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- beliebig viele, eindeutig konfigurierte Leistungsstufen inklusive `0 W`,
|
||||
- exklusive Break-before-make-Schaltung der Stufenkontakte,
|
||||
- lokale Mindest- und Maximaltemperatur mit Hysterese,
|
||||
- optionale PT1-Glaettung des Temperaturmesswerts,
|
||||
- thermische Vorhersage fuer taegliche Solltemperatur-Zeitpunkte,
|
||||
- zweistufige Legionellenfunktion mit Fruehest- und Spaetestintervall,
|
||||
- berechnete Istleistung, aktive Stufe und bezogene Energie,
|
||||
- ereignisbasierte Regelung mit einstellbarer Lastwechselsperre,
|
||||
- Kommunikation mit manuell oder automatisch zugeordneten Enelix-Managern,
|
||||
- optionale Diagnosevariablen und Debug-Logging.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- IP-Symcon ab Version 8.0,
|
||||
- installierte Bibliothek `Enelix-EMS` vom Branch `develop`,
|
||||
- eine Integer- oder Floatvariable als Temperaturfuehler,
|
||||
- mindestens eine positive Leistungsstufe,
|
||||
- je Leistungsstufe eine eigene Booleanvariable mit funktionsfaehiger Aktion,
|
||||
- ein Enelix-Manager, dem die Verbraucherinstanz aktiv zugeordnet wird.
|
||||
|
||||
Die Schaltkontakte duerfen nicht mehrfach verwendet werden. Der Anlagenaufbau
|
||||
muss sicherstellen, dass die konfigurierten Leistungsstufen elektrisch
|
||||
zulaessig sind. Die Software ersetzt keine hardwareseitigen Verriegelungen,
|
||||
Temperaturbegrenzer oder Schutzorgane.
|
||||
|
||||
## Regelungsablauf
|
||||
|
||||
Eine Neuberechnung wird insbesondere ausgeloest durch:
|
||||
|
||||
- einen neuen oder aktualisierten Temperaturmesswert,
|
||||
- das Ein- oder Ausschalten von `Aktiv`,
|
||||
- eine neue Manager-Vorgabe,
|
||||
- eine Aenderung der bedienbaren Temperatursollwerte,
|
||||
- das Ende der Lastwechselsperre,
|
||||
- das Uebernehmen einer neuen Instanzkonfiguration,
|
||||
- eine periodische Vollmeldung an den Manager.
|
||||
|
||||
`Meldeintervall` ist kein Regelintervall. Es stellt die regelmaessige
|
||||
vollstaendige Rueckmeldung sicher. Die Lastwechselsperre verwendet einen
|
||||
einmaligen Timer und loest nach ihrem Ablauf genau eine Neuberechnung aus.
|
||||
|
||||
Die Zielentscheidung folgt der Enelix-1-Zustandsmatrix:
|
||||
|
||||
1. Bei deaktivierter Instanz oder ungueltiger Temperatur ist der Verbraucher
|
||||
nicht verfuegbar und wird ausgeschaltet. Eine ungueltige Konfiguration wird
|
||||
bereits beim Uebernehmen mit Status `202` abgewiesen.
|
||||
2. Unter der wirksamen Mindesttemperatur sowie in der unteren Hysterese bei
|
||||
aktiver Stufe gilt: PV erzwingt die Maximalleistung, Peak bietet
|
||||
`[0, ...Leistungsstufen]` an.
|
||||
3. Ausserhalb dieses Mindesttemperaturbereichs bietet Peak nur `[0]` an.
|
||||
4. Im PV-Betrieb werden unterhalb von `Maximaltemperatur - Hysterese` alle
|
||||
Stufen angeboten. In der oberen Hysterese gilt dies nur, solange bereits
|
||||
eine Stufe aktiv ist.
|
||||
5. An oder oberhalb der wirksamen Maximaltemperatur wird ausgeschaltet.
|
||||
|
||||
Eine erzwungene PV-Maximalleistung wird mit `AenderungMoeglich=false`
|
||||
gemeldet. Das Peak-Array bleibt dagegen durch den Manager waehlbar.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
Das Modul besitzt 19 Properties: sechs gemeinsame Verbraucher-Properties und
|
||||
13 modulspezifische Properties.
|
||||
|
||||
### Manager und Zeitverhalten
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `PrioritaetPV` | Integer | `0` | Prioritaet im PV-Betrieb; kleinere Werte werden zuerst beruecksichtigt. |
|
||||
| `PrioritaetPeak` | Integer | `0` | Prioritaet im Peak-Betrieb; kleinere Werte werden zuerst beruecksichtigt. |
|
||||
| `Meldeintervall` | Integer | `10` | Abstand der vollstaendigen Verbraucherrueckmeldungen in Sekunden. |
|
||||
| `VorgabeTimeout` | Integer | `120` | Zeit in Sekunden, nach der eine nicht erneuerte Manager-Vorgabe ungueltig wird. |
|
||||
| `LastwechselSperrzeit` | Integer | `5` | Mindestzeit zwischen zwei Lastwechseln in Sekunden. |
|
||||
|
||||
### Speichereinstellungen
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `LeistungsStufen` | JSON-Liste | `[]` | Positive Leistung, Stufennummer und Boolean-Schaltkontakt jeder Stufe. |
|
||||
| `Boilerfuehler_PT1` | Integer | `0` | Objekt-ID der Integer- oder Floatvariable fuer die Speichertemperatur. |
|
||||
| `LegionellenfunktionAktiv` | Boolean | `true` | Aktiviert die periodische Anhebung auf Legionellentemperatur. |
|
||||
| `LegionellenMinimalintervallTage` | Integer | `4` | Fruehestens: Legionellentemperatur wird zur Maximaltemperatur. |
|
||||
| `LegionellenMaximalintervallTage` | Integer | `7` | Spaetestens: Legionellentemperatur wird zur Mindesttemperatur. |
|
||||
|
||||
Jeder Eintrag in `LeistungsStufen` besteht aus:
|
||||
|
||||
| Feld | Anforderung |
|
||||
| --- | --- |
|
||||
| `Stufe` | Positive Ganzzahl. |
|
||||
| `Leistung` | Eindeutige positive Ganzzahl in Watt. |
|
||||
| `Schaltkontakt_Stufe` | Eindeutige Objekt-ID einer Booleanvariable. |
|
||||
|
||||
Die Eintraege werden intern nach Leistung sortiert. `0 W` wird automatisch als
|
||||
Aus-Zustand in das Leistungsangebot aufgenommen und darf nicht als eigene
|
||||
Stufe konfiguriert werden.
|
||||
|
||||
### Erweiterte Speichereinstellungen
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `Boilervolumen` | Integer | `300` | Speichervolumen in Litern fuer die thermische Zeitplanprognose. |
|
||||
| `Hysterese` | Float | `5.0` | Temperaturhysterese in Kelvin. |
|
||||
| `Zeitplan` | JSON-Liste | `[]` | Taegliche Zielwerte mit `Uhrzeit` im Format `HH:MM` und `Solltemperatur`. |
|
||||
|
||||
Der jeweils naechste Zeitplaneintrag wird fuer heute oder den folgenden Tag
|
||||
ermittelt. Reicht die verbleibende Zeit bei maximaler elektrischer Leistung
|
||||
rechnerisch nicht mehr aus, wird das Zeitplanziel zur wirksamen
|
||||
Mindesttemperatur. Im PV-Betrieb wird dann die hoechste Stufe erzwungen, im
|
||||
Peakbetrieb werden alle Stufen angeboten. Die Berechnung verwendet Wasser mit
|
||||
`4186 J/(kg K)` und beruecksichtigt keine Speicher- oder Leitungsverluste. Fuer
|
||||
Datum und Uhrzeit gilt die in IP-Symcon eingestellte lokale Zeitzone.
|
||||
|
||||
### Erweiterte sonstige Einstellungen
|
||||
|
||||
| Ident | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `TemperaturMaxAlter` | Integer | `30` | Maximal zulaessiges Alter des Temperaturmesswerts in Sekunden. |
|
||||
| `BoilertemperaturGlaetten` | Boolean | `false` | Aktiviert die PT1-Glaettung. |
|
||||
| `ZeitKonstante` | Integer | `120` | PT1-Zeitkonstante in Sekunden. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Zeigt Temperatursollwerte an und gibt ihre Bedienung frei. |
|
||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt die 14 Diagnosevariablen an. |
|
||||
| `LoggingEin` | Boolean | `false` | Aktiviert laufende Debug-Ausgaben des Moduls. |
|
||||
|
||||
Alle Zeitwerte und Intervalle muessen groesser als `0` sein. Die beiden
|
||||
Prioritaeten muessen mindestens `0` betragen. Das Legionellen-Minimalintervall
|
||||
darf nicht groesser als das Maximalintervall sein.
|
||||
|
||||
## Temperaturverarbeitung
|
||||
|
||||
Der Rohwert des konfigurierten Fuehlers muss numerisch und juenger als
|
||||
`TemperaturMaxAlter` sein. Andernfalls werden `TemperaturGueltig=false`,
|
||||
`Verfuegbar=false` und eine Stoerung gemeldet; eine aktive Stufe wird
|
||||
ausgeschaltet.
|
||||
|
||||
Ohne Glaettung entspricht `Boilertemperatur` dem letzten gueltigen Rohwert.
|
||||
Mit aktivierter Glaettung verwendet das Modul ein PT1-Glied. Der erste Wert
|
||||
initialisiert den Filter; danach wird die tatsaechlich seit der letzten
|
||||
Temperaturberechnung vergangene Zeit verwendet.
|
||||
|
||||
Die Solltemperaturen werden bei der ersten Initialisierung auf folgende Werte
|
||||
gesetzt:
|
||||
|
||||
| Variable | Initialwert |
|
||||
| --- | ---: |
|
||||
| `Mindesttemperatur` | `45.0 Grad C` |
|
||||
| `Maximaltemperatur` | `60.0 Grad C` |
|
||||
| `Legionellentemperatur` | `65.0 Grad C` |
|
||||
|
||||
Es gilt immer:
|
||||
|
||||
`Mindesttemperatur < Maximaltemperatur <= Legionellentemperatur`
|
||||
|
||||
Temperaturwerte muessen zwischen `0` und `100 Grad C` liegen. Bedienaktionen sind
|
||||
nur erlaubt, wenn `EinstellungenInVisu=true` gesetzt ist.
|
||||
|
||||
## Hysterese und lokales Nachladen
|
||||
|
||||
Sinkt die Temperatur unter die wirksame Mindesttemperatur, erzwingt PV die
|
||||
hoechste Leistungsstufe; Peak bietet dagegen Aus und alle Stufen an. In der
|
||||
unteren Hysterese bis `Mindesttemperatur + Hysterese` bleibt dieses Verhalten
|
||||
nur erhalten, solange bereits eine Stufe aktiv ist.
|
||||
|
||||
Im PV-Betrieb werden unter `Maximaltemperatur - Hysterese` alle Stufen
|
||||
angeboten. Innerhalb der oberen Hysterese bleibt das Angebot nur bei einer
|
||||
bereits aktiven Stufe erhalten. An der Maximaltemperatur wird ausgeschaltet.
|
||||
|
||||
## Legionellenfunktion
|
||||
|
||||
Die Legionellenfunktion arbeitet zweistufig:
|
||||
|
||||
1. Ab `LegionellenMinimalintervallTage` wird die Legionellentemperatur zur
|
||||
wirksamen Maximaltemperatur. Der Manager kann die Aufheizung innerhalb des
|
||||
verbleibenden Zeitfensters ermoeglichen.
|
||||
2. Ab `LegionellenMaximalintervallTage` wird die Legionellentemperatur auch zur
|
||||
wirksamen Mindesttemperatur. PV erzwingt dadurch die Maximalleistung; Peak
|
||||
bietet das vollstaendige Stufenarray bis zum Ziel an.
|
||||
|
||||
Erreicht die gueltige Speichertemperatur die Legionellentemperatur, wird der
|
||||
Zeitpunkt als erfolgreicher Abschluss gespeichert und das Intervall beginnt
|
||||
neu. `LegioCounter` zeigt die Sekunden seit diesem Abschluss.
|
||||
|
||||
`Legionellentemperatur` wird nur als Symcon-Variable angelegt, wenn
|
||||
`LegionellenfunktionAktiv=true` ist. Beim Ausschalten der Funktion wird die
|
||||
Variable geloescht; der letzte Wert bleibt intern erhalten und steht bei einem
|
||||
spaeteren Wiedereinschalten wieder zur Verfuegung.
|
||||
|
||||
## Lastwechselsperre und Schaltsicherheit
|
||||
|
||||
Nach jedem tatsaechlichen Lastwechsel wird fuer
|
||||
`LastwechselSperrzeit` Sekunden kein weiterer regulaerer Wechsel zugelassen.
|
||||
In dieser Zeit gilt:
|
||||
|
||||
- `AenderungMoeglich=false`,
|
||||
- `Leistungswerte_W` enthaelt nur die aktuell gehaltene Leistung,
|
||||
- eine abweichende Manager-Vorgabe wird abgewiesen,
|
||||
- ein einmaliger Timer plant die Freigabe.
|
||||
|
||||
Ein sicherheitsbedingtes Ausschalten, beispielsweise bei ungueltiger
|
||||
Temperatur oder erreichter Maximaltemperatur, darf die Sperre uebergehen.
|
||||
Separate Variablen `Idle` oder `IdleCounter` existieren nicht.
|
||||
|
||||
Beim Schalten werden zuerst alle konfigurierten Kontakte ausgeschaltet und
|
||||
anschliessend hoechstens der Kontakt der Zielstufe eingeschaltet. Schlaegt ein
|
||||
Schaltvorgang fehl, versucht das Modul alle Kontakte auszuschalten, setzt die
|
||||
berechnete Istleistung auf `0 W` und meldet einen Schaltfehler.
|
||||
|
||||
## Variablen
|
||||
|
||||
### Betriebs- und Einstellvariablen
|
||||
|
||||
| Ident | Typ / Zugriff | Sichtbarkeit | Beschreibung |
|
||||
| --- | --- | --- | --- |
|
||||
| `Aktiv` | Boolean / bedienbar | immer | Lokale EMS-Freigabe; Startwert `false`. |
|
||||
| `Boilertemperatur` | Float / Anzeige | immer | Letzter gueltiger, gegebenenfalls geglaetteter Temperaturwert. |
|
||||
| `Mindesttemperatur` | Float / bedienbar | bei `EinstellungenInVisu` | Untere Grenze fuer lokales Nachladen. |
|
||||
| `Maximaltemperatur` | Float / bedienbar | bei `EinstellungenInVisu` | Abschaltgrenze im Normalbetrieb. |
|
||||
| `Legionellentemperatur` | Float / bedienbar | Funktion aktiv; Anzeige freigegeben | Ziel des Legionellenprogramms. |
|
||||
|
||||
### Diagnosevariablen
|
||||
|
||||
Mit `DiagnosevariablenAnzeigen=true` werden diese 14 Variablen angelegt. Beim
|
||||
Abschalten der Property werden sie wieder geloescht. Die Regellogik verwendet
|
||||
persistente interne Zustaende und funktioniert unabhaengig von diesen
|
||||
Anzeigevariablen.
|
||||
|
||||
| Ident | Typ | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `Istleistung` | Float | Berechnete aktuelle Leistung in Watt. |
|
||||
| `Leistungsquelle` | Integer | Immer `1` fuer berechnete Leistung. |
|
||||
| `Sollleistung` | Integer | Aktuell angenommene oder lokal erzwungene Zielvorgabe in Watt. |
|
||||
| `SollwertGueltig` | Boolean | Eine Manager-Vorgabe ist vorhanden und noch nicht abgelaufen. |
|
||||
| `Verfuegbar` | Boolean | Das Modul kann grundsaetzlich durch das EMS gesteuert werden. |
|
||||
| `AenderungMoeglich` | Boolean | Der Manager darf momentan eine neue Leistung vorgeben. |
|
||||
| `Stoerung` | Boolean | Mindestens eine Stoerbedingung ist aktiv. |
|
||||
| `Stoertext` | String | Zusammengefasste lesbare Stoerbeschreibung. |
|
||||
| `AktiveStufe` | Integer | Stufennummer der berechneten aktuellen Leistung; `0` bedeutet aus. |
|
||||
| `BezogeneEnergie` | Float | Aus Istleistung und verstrichener Zeit berechnete Energie in kWh. |
|
||||
| `TemperaturGueltig` | Boolean | Temperaturfuehler vorhanden, numerisch und nicht veraltet. |
|
||||
| `NachladenAktiv` | Boolean | Lokales Nachladen aufgrund der Mindesttemperatur ist aktiv. |
|
||||
| `LegionellenbetriebAktiv` | Boolean | Das Fruehestintervall der Legionellenfunktion ist erreicht. |
|
||||
| `LegioCounter` | Integer | Sekunden seit dem letzten erfolgreichen Legionellenabschluss. |
|
||||
|
||||
## Managerkommunikation
|
||||
|
||||
Das Modul implementiert
|
||||
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet den
|
||||
Enelix-2-Vertrag `4.0`. Es besitzt keine Manager-ID-Property. Zugelassen sind
|
||||
nur Manager, in deren manueller oder automatischer Verbraucherzuordnung die
|
||||
Instanz aktiv eingetragen ist.
|
||||
|
||||
Eine Manager-Vorgabe wird nur angenommen, wenn:
|
||||
|
||||
- Vertragsversion, Absender, Empfaenger und Datentypen gueltig sind,
|
||||
- der Manager die Instanz aktiv zugeordnet hat,
|
||||
- `Sollleistung_W` im zuletzt berechneten `Leistungswerte_W` enthalten ist.
|
||||
|
||||
Rueckmeldungen erfolgen bei relevanten Ereignissen, kurz verzoegert nach einer
|
||||
Manager-Vorgabe und zusaetzlich alle `Meldeintervall` Sekunden. Die kurze
|
||||
Verzoegerung verhindert eine synchrone Manager-Verbraucher-Endlosschleife.
|
||||
|
||||
Die Verbraucherrueckmeldung enthaelt insbesondere:
|
||||
|
||||
- Prioritaeten fuer PV- und Peak-Betrieb,
|
||||
- `Leistungswerte_W`, `AenderungMoeglich` und `Verfuegbar`,
|
||||
- `Istleistung_W` mit `Leistungsquelle=1`,
|
||||
- Zustandseintraege fuer Sollleistung, Wassertemperatur, Maximaltemperatur,
|
||||
aktive Stufe, Nachladen, Legionellenbetrieb und Stoerungen.
|
||||
|
||||
Nach `VorgabeTimeout` Sekunden ohne Erneuerung wird eine Manager-Vorgabe
|
||||
bei der naechsten Neuberechnung, spaetestens bei der folgenden Vollmeldung,
|
||||
ungueltig. Beim Neustart wird keine alte Manager-Vorgabe ungeprueft wieder
|
||||
aufgenommen.
|
||||
|
||||
## Abgrenzung des aktuellen Stands
|
||||
|
||||
- `Istleistung`, `AktiveStufe` und `BezogeneEnergie` sind berechnete Werte; das
|
||||
Modul besitzt keinen Anschluss fuer einen elektrischen Leistungsmesser.
|
||||
- Die Stufenkontakte sind Aktoren, keine separate physische Rueckmeldung. Ihr
|
||||
Booleanzustand wird bei einer Regelberechnung auf Plausibilitaet geprueft.
|
||||
- Externe Aenderungen an den Stufenkontakten loesen selbst keine
|
||||
Neuberechnung aus. Der Temperaturfuehler ist die abonnierte Messvariable.
|
||||
- Der Zeitplan beschreibt taeglich wiederkehrende Zielzeitpunkte und keine
|
||||
einmaligen Kalendertermine.
|
||||
|
||||
## Status- und Fehlerzustaende
|
||||
|
||||
| Status | Bedeutung | Typische Ursache |
|
||||
| ---: | --- | --- |
|
||||
| `102` | Aktiv | Konfiguration und Temperaturmessung sind gueltig. |
|
||||
| `201` | Temperaturmessung ungueltig | Fuehler fehlt, ist nicht numerisch oder zu alt. |
|
||||
| `202` | Konfiguration ungueltig | Unzulaessige Intervalle, Temperaturen, Stufen, Kontakte oder Zeitplaneintraege. |
|
||||
| `203` | Schaltfehler | Eine Aktion eines Stufenkontakts ist fehlgeschlagen. |
|
||||
|
||||
Eine Uebertemperatur liegt vor, wenn `Boilertemperatur` die hoehere Grenze aus
|
||||
Maximal- und Legionellentemperatur um mehr als `Hysterese` ueberschreitet. Sie
|
||||
wird ueber `Stoerung` und `Stoertext` gemeldet; der Modulstatus bleibt ohne
|
||||
zusaetzlichen Schalt- oder Fuehlerfehler `102`.
|
||||
|
||||
## Installation und Inbetriebnahme
|
||||
|
||||
1. Im IP-Symcon Module Control den Branch `develop` der Bibliothek
|
||||
`https://git.belevo.ch/ENELIX/Enelix-EMS.git` installieren oder
|
||||
aktualisieren.
|
||||
2. Unter **Instanz hinzufuegen** nach dem Alias **Wassererwärmer** oder dem
|
||||
Modulnamen `VerbraucherWarmwassererwaermer` suchen und eine Instanz anlegen.
|
||||
3. Den Temperaturfuehler auswaehlen und `TemperaturMaxAlter` passend zum
|
||||
Aktualisierungsintervall des Fuehlers einstellen.
|
||||
4. Alle Leistungsstufen mit positiver Leistung und jeweils eigenem
|
||||
Boolean-Schaltkontakt konfigurieren.
|
||||
5. Normal-, Legionellen- und Zeitplanwerte pruefen. Danach die Konfiguration
|
||||
uebernehmen.
|
||||
6. Die Instanz im Enelix-Manager manuell aktiv zuordnen oder bei automatischer
|
||||
Suche in der gefundenen Liste aktivieren.
|
||||
7. Fuer die Erstpruefung `DiagnosevariablenAnzeigen` und bei Bedarf
|
||||
`LoggingEin` einschalten.
|
||||
8. Unter Aufsicht `Aktiv` einschalten und jede Stufe einzeln pruefen. Dabei
|
||||
kontrollieren, dass nie zwei Stufenkontakte gleichzeitig aktiv sind.
|
||||
9. Eine Manager-Vorgabe fuer `0 W` und fuer jede konfigurierte Stufe pruefen.
|
||||
10. Diagnosevariablen nach der Abnahme bei Bedarf wieder ausschalten.
|
||||
|
||||
## Abnahmecheckliste
|
||||
|
||||
- Die Instanz erreicht Status `102`.
|
||||
- `Aktiv=false` schaltet alle Stufenkontakte aus.
|
||||
- Der Fuehlerwert erscheint unverfaelscht oder erwartungsgemaess geglaettet in
|
||||
`Boilertemperatur`.
|
||||
- Unterhalb der Mindesttemperatur wird die hoechste Stufe lokal erzwungen.
|
||||
- Oberhalb der Maximaltemperatur werden alle Stufen ausgeschaltet.
|
||||
- Jede Manager-Vorgabe schaltet genau den zugeordneten Kontakt.
|
||||
- Direkt nach einem Wechsel ist `AenderungMoeglich=false`; nach der
|
||||
konfigurierten Sperrzeit wird ein regulaerer Wechsel wieder freigegeben.
|
||||
- Eine Gegenanforderung waehrend der Sperre liegt nicht im gemeldeten
|
||||
Leistungsangebot.
|
||||
- Ein veralteter oder ungueltiger Fuehlerwert fuehrt zu Status `201`, einer
|
||||
Stoerung und sicherem Ausschalten.
|
||||
- Bei aktiver Legionellenfunktion wird `Legionellentemperatur` angelegt; nach
|
||||
dem Abschalten der Funktion wird die Variable geloescht.
|
||||
- Mit ausgeschalteten Diagnosevariablen bleiben Regelung und Kommunikation
|
||||
funktionsfaehig.
|
||||
|
||||
## Diagnosehinweise
|
||||
|
||||
- Status `202`: zuerst Fuehler-ID, Leistungsstufen, eindeutige Kontakte,
|
||||
Temperaturreihenfolge, Intervalle und Zeitplanformat pruefen.
|
||||
- Status `201`: `VariableUpdated`, Datentyp des Fuehlers und
|
||||
`TemperaturMaxAlter` kontrollieren.
|
||||
- Status `203`: Aktionen der Boolean-Schaltkontakte einzeln in IP-Symcon
|
||||
testen; das Modul versucht bei einem Fehler alle Kontakte auszuschalten.
|
||||
- Manager-Vorgabe wird abgewiesen: Zuordnung im Manager,
|
||||
`AenderungMoeglich` und das aktuelle `Leistungswerte_W` pruefen.
|
||||
- Unerwartetes Nachladen: Mindesttemperatur, Hysterese, naechstes Zeitplanziel
|
||||
und Alter des letzten Legionellenabschlusses kontrollieren.
|
||||
|
||||
## Tests
|
||||
|
||||
`WarmwasserReglerTest.php` prueft die reine Regellogik fuer Leistungsstufen,
|
||||
PT1, Lastwechselsperre, Energieberechnung, thermische Prognose, Zeitplan und
|
||||
Legionellengrenzen.
|
||||
|
||||
`WarmwassererwaermerModulstrukturTest.php` prueft Metadaten, Formular, alle 19
|
||||
Properties, ereignisbasierte Lastwechselsperre, Vertrag `4.0`, bedarfsgesteuerte
|
||||
Diagnosevariablen, Legionellentemperatur, Temperatursollwerte und
|
||||
Break-before-make-Schaltung.
|
||||
|
||||
## Migration von Enelix 1
|
||||
|
||||
Die Zuordnung der uebernommenen, angepassten und entfallenen Felder des alten
|
||||
Moduls `Boiler_x_Stufig` ist in
|
||||
[`docs/migration/Boiler-x-Stufig.md`](../../migration/Boiler-x-Stufig.md)
|
||||
dokumentiert.
|
||||
|
||||
Wesentliche Unterschiede:
|
||||
|
||||
- `Interval` entfaellt; die Regelung ist ereignisbasiert.
|
||||
- `IdleCounterMax` wird durch `LastwechselSperrzeit` in Sekunden ersetzt.
|
||||
- `Idle` und `IdleCounter` entfallen vollstaendig.
|
||||
- `PowerSteps` wird durch das Vertragsfeld `Leistungswerte_W` ersetzt.
|
||||
- Prioritaeten sind Properties; Betriebsart und Leistungsverteilung liegen im
|
||||
Manager.
|
||||
- Die neue Modul-ID erfordert eine neue Instanz; eine automatische Umwandlung
|
||||
des Enelix-1-Objekts findet nicht statt.
|
||||
@@ -0,0 +1,99 @@
|
||||
# V4: getrennte Last-/Batterie-/SDL-Bilanz (Kandidat, keine Stellfreigabe)
|
||||
|
||||
## Status 2026-10-02
|
||||
|
||||
`NetzfahrplanV4Bilanzierung` ist ein reiner Rechen-/Validierungsbaustein. Noch keine
|
||||
Einbindung in die laufende Telemetrie, den Trainer oder den Kontrolltest. Er ruft
|
||||
keine IPS-, Netzwerk-, Archiv- oder Stellfunktionen auf. Ergebnis ist immer
|
||||
`controlEligible=false`, auch bei geschlossener mathematischer Bilanz.
|
||||
|
||||
34 isolierte PHP-Pruefungen wurden mit der gleichen SHA256-Datei auf iot-symcon01
|
||||
bestanden. Das ist kein Nachweis fuer die reale Hardware-/Messgrenzenzuordnung
|
||||
und kein vollstaendiger PHPUnit- oder Symcon-Kernel-Test.
|
||||
|
||||
## Gefundener Unterschied in Lihrenmoos
|
||||
|
||||
Installierter Manager `Manager/module.php::leseMessleistungen()` verwendet:
|
||||
|
||||
Haus = PV + Netz - Summe der konfigurierten Batterie-Istleistungen
|
||||
|
||||
Konfiguriert ist hier die virtuelle EV-Leistung 52020, nicht die gesamte physische
|
||||
Batterieleistung. Daher bleibt der externe SDL-Anteil und eine allfaellige
|
||||
Abweichung der virtuellen Aufteilung im an den Prognosedienst gesendeten Hauswert.
|
||||
`sendePrognoseTelemetrie()` sendet genau diesen errechneten Wert. Die parallel
|
||||
existierenden Hausvariablen 46714 und 40713 sind NICHT automatisch diese Quelle.
|
||||
|
||||
Der bisherige Gateway `Bat_EV_SDL_V4` liest die physische Batterieleistung aus
|
||||
47725, 35724 und 36380 und invertiert diese Vorzeichen. EV und SDL sind daraus
|
||||
abgeleitete, separat gefilterte virtuelle Konten, keine zwei unabhaengigen Zaehler.
|
||||
Die Filter koennen alte Teilwerte halten. `UpdateActualPowerSplit` aktualisiert die
|
||||
Ausgabevariablen trotzdem. Ein neuer Ausgabezeitstempel beweist deshalb nicht die
|
||||
Frische aller darunter liegenden Originalmessungen.
|
||||
|
||||
## Kandidat fuer die korrekte Bilanz
|
||||
|
||||
Mit einheitlicher Messgrenze und Vorzeichen Batterie+ = Laden, Netz+ = Bezug:
|
||||
|
||||
Verbraucherlast = Netz + PV - gesamte physische Batterieleistung
|
||||
Grundlast = Verbraucherlast - separat geplante flexible Verbraucher
|
||||
Zuordnungsrest = physische Batterie - virtuell steuerbare Batterie - externe virtuelle Konten
|
||||
Externer Netzeffekt = externe virtuelle Konten + Zuordnungsrest
|
||||
Netz = Grundlast + flexible Last + steuerbare Batterie + externer Netzeffekt - PV
|
||||
|
||||
Der Zuordnungsrest wird nicht erneut als Hausverbrauch trainiert, sondern separat
|
||||
angezeigt. Eine grosse Abweichung sperrt die Datenqualitaet. Gegenlaeufige virtuelle
|
||||
Konten (z.B. EV +42 kW, SDL -40 kW, physisch +2 kW) bleiben korrekt getrennt.
|
||||
Spaetere Ladestationen/Boiler sind ueber `flexible_load` erweiterbar. Solange sie
|
||||
nicht separat geplant werden, verbleiben sie in der nicht separat steuerbaren Last.
|
||||
|
||||
Keine Messgroesse wird doppelt subtrahiert. Ein negatives Grundlastergebnis wird
|
||||
nicht heimlich auf null begrenzt. Numerische Nullen sind echte Werte; fehlende,
|
||||
nicht endliche oder umtypisierte Werte sind keine Nullen.
|
||||
|
||||
## Originalzeit und Datenqualitaet
|
||||
|
||||
Jede Quelle hat explizite Variable/Parent/Ident, Faktor, maximales Alter und optionale
|
||||
Ursprungsabhaengigkeiten. Alle Quellen werden zweimal gelesen. Wertewechsel waehrend
|
||||
der Aufnahme werden abgewiesen; Zeitstempel werden nie kuenstlich verjuengt.
|
||||
Abhaengigkeiten sind zyklenfrei, doppelte Quellen werden verhindert.
|
||||
Eine virtuelle Summenvariable erbt das aelteste Datum ihrer physischen Eingangsquellen.
|
||||
Konfigurierbare Alters-/Synchronitaetspruefung ist erforderlich. Diagnoseberechnungen
|
||||
mit ungeeigneten Daten bleiben `quality_hold`, nicht `base_load`-Trainingsfreigabe.
|
||||
Die momentanen Beispielgrenzen (60 s Alter, 30 s Versatz) sind konservative
|
||||
Diagnoseparameter, keine garantierten Geraeteeigenschaften.
|
||||
|
||||
Die konkrete Solar-PV-Herkunft sowie AC/DC-/Wirkungsgradgrenzen aller Hybridgeraete
|
||||
sind noch zu verifizieren. Eine algebraisch geschlossene Gleichung beweist diese
|
||||
physische Messgrenze NICHT. Die vorgeschlagene Quellkonfiguration ist nicht freigegeben.
|
||||
|
||||
## Migration in die Prognose (noch offen)
|
||||
|
||||
1. Originalmessquellen und Messgrenze dokumentieren und Qualitaet ueberwachen.
|
||||
2. Korrigierte Grundlast und externe Kanaele als NEUE versionierte Zeitreihen erfassen.
|
||||
3. Bestehende Historien nicht ueberschreiben oder als bereinigte Historie umbenennen.
|
||||
Rekonstruktion nur bei ausreichenden zeitlich passenden Originaldaten.
|
||||
4. Prognosemodell auf der korrigierten Basis trainieren/validieren.
|
||||
5. Zukunfts-SDL separat liefern. Unbekannt ist nicht null; eine geschaetzte Annahme
|
||||
muss explizit bezeichnet werden und darf nicht als veroeffentlichter SDL-Fahrplan gelten.
|
||||
6. Erst nach diesen Pruefungen eine echte Bilanzierungsreferenz im Trial-Gate verwenden.
|
||||
`mappingId` aus dieser Klasse ist ein Konfigurationsfingerabdruck, KEIN solcher Nachweis.
|
||||
|
||||
## Gateway-Ausfallverhalten (nicht getestet/freigegeben)
|
||||
|
||||
Quelltextpruefung des installierten Alt-Gateways ergab keinen Frischetest fuer
|
||||
`Nennleistung_Soll_EV` im Weg `Update -> ApplySetpoints`. Der zwischengespeicherte
|
||||
EV-Sollwert wird weiter verwendet, solange die Gateway-Schleife laeuft. Der Eingang
|
||||
wird von `scripts/31800.ips.php` geschrieben; dieser enthaelt ebenfalls keine Lease.
|
||||
Der vorhandene 30-s-Timeout des EMS-Batterieadapters ist ein Schutz innerhalb von
|
||||
Symcon, nicht ein unabhaengiger Wechselrichter-Timeout bei Rechnerausfall.
|
||||
|
||||
`State=false` im Alt-Gateway beendet `Update` ohne dort Nullwerte zu schreiben.
|
||||
Diesen Schalter nicht als nachgewiesenen Hardware-Notstopp verwenden.
|
||||
Keine Kommunikationsunterbrechung, kein Abschalttest und keine neue Stellwertausgabe
|
||||
wurden an der realen Anlage durchgefuehrt. Auch aus dem fehlenden Code kann NICHT
|
||||
gefolgert werden, dass das Geraet selbst keinen Timeout hat. Exakte Typen/Firmware
|
||||
und das wirksame geraeteseitige Verhalten muessen noch festgestellt werden.
|
||||
|
||||
Wichtig fuer spaetere Anpassung: Ein EV-Befehlsablauf darf nicht pauschal den
|
||||
externen SDL-Auftrag loeschen. Ein Kernel-interner Timer ersetzt wiederum keinen
|
||||
Schutz bei komplettem Kernel-/Rechner-/Kommunikationsausfall.
|
||||
@@ -0,0 +1,131 @@
|
||||
# V4 application data and forecast integration
|
||||
|
||||
## Implemented application path
|
||||
|
||||
`ManagerNetzfahrplanV4DatenTrait` records the configured raw power/SOC/counter sources
|
||||
inside the existing Manager. This replaces the need for permanent standalone
|
||||
observation categories once the native delivery path has been accepted. It never
|
||||
issues device commands. New acquisition properties default disabled.
|
||||
|
||||
The private outbox at `data/enelix-v4/<installationId>` is append-only and delivery
|
||||
is acknowledged by dataset and capture time. Unacknowledged data survives network
|
||||
failures and 429 responses. The native payload budget is below the existing portal
|
||||
1 MiB request-body limit. Existing authentication and V4 rate limiting are reused.
|
||||
|
||||
Server application source is now versioned under `services/netplan-v4`, rather than
|
||||
existing only as an untracked server working tree. No live measurements, databases,
|
||||
credentials, dependency binaries or settings exports are included in this directory.
|
||||
|
||||
The existing worker handles per-plant dataset ingestion, physical five-minute
|
||||
rollup, model training, model storage and corrected-load substitution into the V4
|
||||
optimizer. It continues to consume the existing PV forecasts and price/operation
|
||||
inputs. The public device route can append measurements but cannot alter source
|
||||
meaning, register a dataset, change live permissions or arm a controlled trial.
|
||||
|
||||
## Explicit settings and model meaning
|
||||
|
||||
- `forecastSource=legacy` preserves the existing forecast input by default.
|
||||
- `forecastSource=corrected_profile` plus `measurementDataset` uses the trained
|
||||
corrected physical load. Missing models are reported; legacy house values are
|
||||
not substituted silently.
|
||||
- `trainingCadence=daily|weekly` is now connected to real profile fitting in the
|
||||
application worker, independently from the optimizer refresh frequency.
|
||||
- Corrected load profiles are tagged `physical-profile-v1`: robust daily profile,
|
||||
recent-day profile and weekday/weekend profile paired with PV families 3/13/23.
|
||||
They are not claimed to be the previous load models unchanged. Economic automatic
|
||||
selection still requires the separate, not yet finished cost-replay integration.
|
||||
- Initial models are explicitly bootstrap models. Later candidates record causal
|
||||
holdout errors; an overlapping validation window cannot certify a promotion.
|
||||
- Current SDL request persistence is only a labelled shadow scenario. Unknown or
|
||||
stale SDL is not silently zero, and the scenario is not a guaranteed future
|
||||
schedule or a robust SDL-aware production control policy.
|
||||
|
||||
## Lihrenmoos package scope
|
||||
|
||||
Effective EV capacity 161.44 kWh and power 39 kW remain unchanged; SDL reserve is
|
||||
already excluded. The prepared dataset `lihrenmoos-physical-v1` uses a configured
|
||||
physical estimate with SolarEdge signed terminal power counted once. It requires
|
||||
at least 24 equivalent usable hours before the first bootstrap profile; the
|
||||
history window is 28 days. Coverage policy 95% with maximum 10-second unsupported
|
||||
portion is an explicit modelling assumption. Gaps and source ages remain visible.
|
||||
It is not an independent electrical metering-boundary proof.
|
||||
|
||||
Server entry point on the service host:
|
||||
|
||||
python3 /home/agent/services/netplan-v4-shadow/commissioning/deploy_application.py \
|
||||
--plant e3a08f9e-af12-4695-99bd-8b51c0520021 --apply
|
||||
|
||||
Default without --apply checks source only. The explicit apply command rebuilds
|
||||
and tests V4 in Python 3.11, tests portal routing, backs up the V4 database and
|
||||
recreates only V4 and the portal. It configures the dataset but DOES NOT select it
|
||||
for the existing plan automatically. No forecasts/tariff importer or controllers
|
||||
are restarted by the server command. A previous-image rollback is prepared.
|
||||
|
||||
Then, inside Lihrenmoos Symcon:
|
||||
|
||||
require '/srv/agent/netplan-v4-application-build/install.php';
|
||||
|
||||
This is a data-only patch of the currently installed passive manager, with file
|
||||
backup and library reload. It does NOT install the development control-trial
|
||||
changes in Manager/Batterie. The first runtime invocation imports at most 48h of
|
||||
the existing 23-channel observer as a durable backlog, preserving the original
|
||||
files, then activates acquisition and delivery. If Symcon module registration is
|
||||
not immediately available, the installer reports that a single repeat is needed.
|
||||
|
||||
The two temporary standalone samplers are not stopped automatically by this
|
||||
initial package; disable them only after native batch acceptance and history
|
||||
continuity are confirmed. This package creates no new root diagnostic category.
|
||||
|
||||
## Validation performed before deployment
|
||||
|
||||
219 Python tests passed in the isolated host QA runtime, including actual synthetic
|
||||
measurement -> profile -> existing optimizer -> stored shadow plan. 16 Node proxy
|
||||
checks passed. 21 PHP data-path checks and 5 installer scenarios passed with mocked
|
||||
IPS/HTTP and temporary files on the test host. Full staged PHP syntax passed.
|
||||
No new container or new manager data integration has yet been run in production.
|
||||
|
||||
Evidence: service `APPLICATION_PIPELINE_TEST_RESULTS.txt`,
|
||||
`APPLICATION_PORTAL_TEST_RESULTS.txt`, `commissioning/application-source/RELEASE.json`;
|
||||
test host `netplan-v4-application-build/PREPARATION.json` and `TEST_RESULTS.txt`.
|
||||
|
||||
## Remaining product scope (do not disguise as complete)
|
||||
|
||||
This integrates application data and load forecasting, not completed production
|
||||
commissioning. Economic replay-based automatic family selection, full corrected
|
||||
feedback/control integration, long-term outbox/server retention and real live/
|
||||
failure acceptance remain open. Existing trial gates are unchanged. No sensor,
|
||||
accounting or hardware evidence identifier is fabricated by data ingestion.
|
||||
|
||||
## Installer class-path correction (2026-10-03)
|
||||
|
||||
The original commissioning installer loaded `source/libs/NetzfahrplanV4Messaufnahme.php`
|
||||
while importing observation history. A later Manager callback loads the installed
|
||||
copy at `modules/Enelix-EMS/libs/NetzfahrplanV4Messaufnahme.php`. PHP's once-only file
|
||||
inclusion does not deduplicate class declarations across those different files.
|
||||
The dependent `NetzfahrplanV4Bilanzierung` class is subject to the same collision.
|
||||
|
||||
The tested installer-only fix is preserved in
|
||||
`examples/V4ApplicationData/application-class-loading.patch` (SHA256
|
||||
`6d53d74d4059bb1ae3a446350637a4f4a4a73cc2fd64d2fc2f0967bd23ec7173`).
|
||||
When building a commissioning package, load both classes only from the installed
|
||||
module tree, after validating their expected source hashes. Before any require,
|
||||
check both already-loaded origins without autoload. Reject a foreign staged origin
|
||||
with a controlled error; do not hide it by wrapping the class declaration in a guard.
|
||||
Include `installed_capture_bootstrap.php` in the package checksum map. Do not change
|
||||
the installed-module manifest or bless unrelated source changes.
|
||||
|
||||
The Lihrenmoos staging entrypoint has this fix applied, with backups of its original
|
||||
installer and checksum map. No running modules, data files, timers, native callbacks,
|
||||
server components or actuator settings were changed by the repair. Actual completion
|
||||
still requires the user's fresh Symcon invocation of the corrected installer.
|
||||
|
||||
Validation: the original two-path failure was reproduced with the real PHP classes.
|
||||
Thirteen additional isolated installer scenarios passed, including callback/reload
|
||||
loading order, preloaded dependencies, history import, repeat/finished import,
|
||||
foreign-class refusal, partial-import preservation, source drift, registration wait
|
||||
and first installation. These are PHP CLI tests with mocked IPS in temporary trees,
|
||||
not a completed test in the actual Symcon kernel.
|
||||
|
||||
Evidence on the test host:
|
||||
`/srv/agent/netplan-v4-application-build/class-loading-fix/TEST_RESULTS.json` and
|
||||
`FIX_RESULT.json`. The test harness and exact candidate source are retained there.
|
||||
@@ -0,0 +1,96 @@
|
||||
# V4: begrenzter Regeltest (Entwicklungskandidat, NICHT in Betrieb)
|
||||
|
||||
Stand 2026-10-02. Diese Erweiterung ist weder Produktionsfreigabe noch ein
|
||||
Installations-/Startauftrag. Bestehende Schattenplaene und ihre `shadow_seen`
|
||||
Bestaetigungen bleiben unveraendert. Ein Vorschauwert allein darf niemals
|
||||
als Stellfreigabe interpretiert werden.
|
||||
|
||||
## Freigabekette
|
||||
|
||||
1. Serverseitige separate Allowlist `NETPLAN_V4_CONTROL_TRIAL_PLANTS` (standardmaessig
|
||||
leer). Interner authentifizierter `planner/trial/arm`-Aufruf mit expliziter
|
||||
Bestaetigung, neuer Sitzung, gepruefter Plan-ID/Revision, Batteriezuordnung,
|
||||
Zeitfenster, Leistungsgrenzen und dokumentierter Geraete-Watchdog-Abnahme.
|
||||
Die bestehenden oeffentlichen Portal-/Geraeterouten proxien diesen Aufruf NICHT.
|
||||
2. `NetzfahrplanV4RegeltestErlaubt` lokal im Manager UND im Batteriemodul (jeweils
|
||||
Standard false). Alte `NetzfahrplanAktiv`-Freigabe muss false bleiben.
|
||||
3. Zusaetzlicher bewusster lokaler Start einer bestimmten Sitzung. Ein Reload,
|
||||
Netzwiederkehr oder gespeicherter Haken startet keine Sitzung automatisch.
|
||||
4. Der Manager prueft den empfangenen Schattenplan und die getrennte
|
||||
`controlled_trial_authority` gemeinsam. Sie sind gebunden an Anlage, Batterie,
|
||||
beide Instanz-IDs, Revision, manuelle Modellfamilie und stabilen Planungskontext.
|
||||
5. Die Batterie nimmt nur einen separaten `controlled_trial_command` mit eigener
|
||||
Sequenz und kurzer Gueltigkeit an. Normale Managerbefehle erneuern diese
|
||||
Gueltigkeit nicht. `shadow_seen` bleibt eine Empfangs-, keine Ausfuehrungsmeldung.
|
||||
|
||||
Die Serverantwort bleibt eine Schattenplan-Antwort (`liveEnabled:false`). Eine
|
||||
getrennte Testfreigabe ist unter `controlledTrial` und
|
||||
`controlledTrialAuthorized` sichtbar. Der Schattenmodus allein ist bei kuenftig
|
||||
bewusst gestartetem Test deshalb kein Nachweis fuer 'keine Batteriebewegung'.
|
||||
Die Controller-Diagnosen benennen eine angenommene Sollvorgabe, nicht bereits
|
||||
physisch gemessene Umsetzung oder Ersparnis.
|
||||
|
||||
## Bewusst begrenzter Pilotumfang
|
||||
|
||||
- Genau eine eindeutig zugeordnete Batterie je Test; Familie manuell festgelegt.
|
||||
- 30 bis 1800 Sekunden Sitzung, maximal 5000 W Laden und 5000 W Entladen.
|
||||
- Server-, Manager- und Batteriegrenze koennen strenger sein; nie grosszuegig
|
||||
ersetzen oder still auf 39 kW hochsetzen.
|
||||
- Gesamtsollleistung, NICHT Zusatzleistung zur bereits fliessenden Batterieleistung.
|
||||
- Andere Verbraucher bleiben im alten Verteiler. Ihre erwartete Leistungsaenderung
|
||||
wird einmal bei der Batteriekorrektur beruecksichtigt.
|
||||
- Batterie wird nur einem Stellwertpfad zugeteilt, nicht gleichzeitig dem alten
|
||||
und neuen Verteiler. Ist der Test ungueltig, stoppt er und es wird eine NEUE
|
||||
normale Verteilung angefordert; kein alter gespeicherter Sollwert wird restauriert.
|
||||
- Physische Grenzverletzung, die innerhalb der Pilotleistung nicht behebbar ist,
|
||||
beendet den Test zugunsten der bestehenden lokalen Regelung.
|
||||
|
||||
## Ausfall- und Zeitverhalten
|
||||
|
||||
- Serverfreigabe maximal 90 Sekunden, lokal regelmaessig frisch abgerufen.
|
||||
Serverseitiger Widerruf wirkt nicht magisch sofort: Cachefrist beachten.
|
||||
- Einzelner Batteriebefehl maximal 10 Sekunden; Batterie kontrolliert im laufenden
|
||||
Kernel mit 1-Sekunden-Timer zusaetzlich zu Messwert-Callbacks.
|
||||
- Manager liefert kurze Befehle im Test mit zusaetzlichem 2-Sekunden-Takt.
|
||||
- Monotone Laufzeit und Kalenderzeit werden getrennt geprueft. Eine rueckwaerts
|
||||
springende Systemuhr darf eine Sitzung nicht verlaengern.
|
||||
- Abbruch widerruft die Sitzung VOR dem Nullstellversuch. Schreibfehler werden als
|
||||
`stop_failed`/nicht bestaetigter Stopp sichtbar, nicht als sicherer Zustand.
|
||||
- Reserve, Hysterese, aktuelle Lade-/Entladeleistung, SOC und Verfuegbarkeit werden
|
||||
auch im Batteriemodul erneut geprueft. Sicherheitsreduktionen werden nicht durch
|
||||
die Umschaltsperre verzoegert; Richtungswechsel laufen ueber null.
|
||||
- Laufzeitsitzungen liegen nur im Buffer, nicht als automatische Wiederanlauf-Freigabe.
|
||||
Ein persistenter Marker sorgt bei Wiederinitialisierung fuer einen erneuten
|
||||
Nullstellversuch, falls ein Testwert zuvor ausgegeben worden sein koennte.
|
||||
|
||||
WICHTIGE GRENZE: PHP-Timer und Software-Nullstellversuche funktionieren nur bei
|
||||
laufendem Kernel und erreichbarem Wechselrichter. Sie sind KEIN physischer
|
||||
Not-Aus und kein Beweis fuer Abschaltung bei Serverabsturz, Stromausfall oder
|
||||
unterbrochener Stellwertverbindung. Dafuer ist ein separat nachgewiesener
|
||||
Befehlsausfall-/Watchdogmechanismus am Geraet bzw. Gateway erforderlich.
|
||||
Das Feld `NetzfahrplanV4GeraeteWatchdogNachweis` ist eine Referenz auf die gepruefte
|
||||
Dokumentation/Abnahme, keine automatische Hardwarepruefung und kein Defaultwert.
|
||||
|
||||
## Noch offene Freigabepunkte (nicht mit Platzhaltern umgehen)
|
||||
|
||||
- Der laufende Lihrenmoos-Prognosestrom ist noch `house_total`. Fuer Testfreigabe
|
||||
muss `base_load` mit nachgewiesener Quelle/SDL-Abgrenzung und
|
||||
`accountingEvidenceId` aus dem geprueften Adapter vorliegen. Ein Labelwechsel
|
||||
oder ein erfundener Nachweis reicht nicht; Quelle und Messbilanz pruefen.
|
||||
- Geraete-/Gateway-Watchdog bei ausbleibenden Stellbefehlen nachweisen.
|
||||
- Vollstaendiges Manager-/Batterie-Callbackverhalten im IP-Symcon-Kernel pruefen.
|
||||
Die isolierten Tests verwenden echte neue Traits, aber simulierte IPS-Aufrufe,
|
||||
Messwerte, Zeit und Register. Reale Modbus-/VGT-Stellreaktion steht aus.
|
||||
- Zielcontainerpruefung des neuesten Serverstands und Feldabnahme.
|
||||
- Keine durchgehende unbeaufsichtigte Produktion, kein automatischer Mehranlagenstart.
|
||||
Modellvergleich, Trainingsanbindung, Mehranlagenabnahme und Betriebs-Release
|
||||
bleiben weitere Aufgaben.
|
||||
|
||||
## Quelltests
|
||||
|
||||
`php tests/V4ControlTrial/checks.php` nur im separaten CLI-Testprozess ausfuehren,
|
||||
niemals im laufenden Symcon-Skripteditor. Die Datei verweigert einen realen
|
||||
Symcon-Kernel. Eine PHPUnit-Huelle bindet sie an die normale Testsuite an.
|
||||
|
||||
Der Server-Test verwendet ausschliesslich synthetische Daten/Temporaerdatenbanken.
|
||||
Keine Testfunktion liest Zugangsdaten oder schreibt eine reale Stellvariable.
|
||||
@@ -0,0 +1,108 @@
|
||||
# V4 corrected physical feedback -> local bounded control
|
||||
|
||||
## Scope of this delivery
|
||||
|
||||
The shared physical feedback reader is now connected to the actual Manager preview,
|
||||
the existing explicit bounded-trial command path, and an independent reread in the
|
||||
Battery module immediately before hardware output. This is not a new observer.
|
||||
The installer leaves both local trial permissions disabled and cannot arm a server
|
||||
trial. Normal operation continues through the existing local controller.
|
||||
|
||||
Lihrenmoos binding: Manager 17004, Battery 44234, virtual asset
|
||||
`anlage01-virtual-ev`. Effective 161.44 kWh / 39 kW unchanged. Source definitions
|
||||
come from the existing versioned capture config and current saved topology, not
|
||||
from new guessed device models. No old histories, sender cursors, virtual energy
|
||||
accounts, SDL requests, archive policies or inverter polling are changed.
|
||||
|
||||
## Feedback and meaning
|
||||
|
||||
`NetzfahrplanV4Rueckmeldung` checks original value timestamps and object identities
|
||||
in two read passes. Live max age is bounded to 60s and inter-source skew to 30s.
|
||||
History interpolation/publication estimates are not accepted as live feedback.
|
||||
The mapping fingerprint normalizes numbers and ordering, so a JSON property round
|
||||
trip does not create a new identity. A mapping change cancels an active session.
|
||||
|
||||
Two adapters exist: sum of explicitly assigned physical meters, and a virtual
|
||||
EV/SDL partition model. For the latter, gateway state and current requests are
|
||||
read but unchanged zero commands are not mistaken for failed sensor heartbeats.
|
||||
When SDL request is exactly zero, current physical battery power is used rather
|
||||
than the filtered virtual EV display. A new EV request does not invent an immediate
|
||||
physical response or force the measured sign toward the desired sign.
|
||||
|
||||
When SDL is nonzero, source timestamps must cover the latest request change and
|
||||
physical tracking error must remain within the configured small tolerance. The
|
||||
partition is explicitly estimated, not separately measured. Opposing EV/SDL flows
|
||||
cannot be uniquely identified; they remain ineligible for trial control. Unknown
|
||||
or stale physical sources never fall back to held filtered EV values.
|
||||
|
||||
`allowEstimatedForTrial` is false in the prepared Lihrenmoos config. Configuring
|
||||
feedback or seeing a valid preview therefore does NOT accept the estimate for a
|
||||
trial and does NOT satisfy accounting/device-watchdog evidence or other gates.
|
||||
This adapter does not claim to resolve arbitrary simultaneous SDL activity.
|
||||
|
||||
## Actual control integration
|
||||
|
||||
The same coherent grid/battery snapshot is used in the Manager. Battery command
|
||||
is a TOTAL power, not a delta added to the existing command. Planned changes in
|
||||
other consumers are counted once and remain explicitly separate from measured grid.
|
||||
|
||||
A separate explicit server authority, both local consents, accounting evidence,
|
||||
device-watchdog evidence, a deliberate session start and fresh plan remain required.
|
||||
The existing trial is limited to 1800s / 5000W per direction; individual command
|
||||
leases remain <=10s and cannot be renewed by replay or ordinary manager messages.
|
||||
The local feedback mapping fingerprint and grid caps are bound to the session.
|
||||
|
||||
Immediately before writing, Battery rereads physical feedback and applies current
|
||||
SOC, reserve, hysteresis, availability and change-lock constraints. Its resulting
|
||||
command must still meet the bound grid limits; an unreachable target revokes the
|
||||
trial rather than claiming that the planned grid value was achieved. It cannot
|
||||
claim physical performance merely because a register call returned successfully.
|
||||
|
||||
Abort revokes the lease and attempts zero, and the Manager wrapper then runs at
|
||||
most one fresh ordinary allocation, clearing cached pre-trial targets. It does
|
||||
not recurse forever or revive an old plan. A failed stop stays explicitly failed.
|
||||
The watchdog still requires a functioning kernel. Independent hardware failure
|
||||
behavior is NOT certified by software tests.
|
||||
|
||||
Outside a V4 session the existing battery power path is unchanged. The compatible
|
||||
BatterieRegler library includes the already developed optional reserve charging
|
||||
limit; with its default (maximal charge) 13,824 old/new ordinary-offer test cases
|
||||
match exactly. That comparison is not an on-device dynamics test.
|
||||
|
||||
## Installation
|
||||
|
||||
A single prepared Symcon script is the next runtime action:
|
||||
|
||||
require '/srv/agent/netplan-v4-feedback-stage/install.php';
|
||||
|
||||
It hash-checks nine changed files and all version-matched dependencies, backs up
|
||||
existing source, installs dependencies before modules and reloads the ENELIX
|
||||
library. Reload/ApplyChanges may execute existing initialization routines; this
|
||||
is not promised to be a zero-effect library reload. It configures only the new
|
||||
feedback mapping on Battery. No server redeployment is required. No shared classes
|
||||
are included from staging, avoiding the earlier duplicate-class problem.
|
||||
|
||||
A delayed module-registration result requests one repeat. A valid installation
|
||||
can still report an unavailable feedback sample; that is not reinterpreted as zero.
|
||||
Any preexisting trial permission, unsaved change, or concurrent source change stops
|
||||
the installer. Source write failure rolls back its own changed files; reload errors
|
||||
are reported for review and do not automatically authorize anything.
|
||||
|
||||
## Validation and remaining acceptance
|
||||
|
||||
125 isolated PHP functional checks cover the reader, real receiver, actual trial
|
||||
traits with a simulated register driver, the extracted actual Manager fallback
|
||||
wrapper, and the real Battery message builder with optional diagnostic variables absent.
|
||||
Source quality is stored internally; a last partition estimate is not relabelled as a
|
||||
measured value just because a trial ends or a diagnostic variable is hidden. Four full module linkage checks and ten isolated installer scenarios also
|
||||
pass. Full PHP syntax and dependency hashes are checked. Neither these fixtures nor
|
||||
the ordinary-offer comparison run the real Symcon kernel or an inverter.
|
||||
|
||||
Evidence: test host `/srv/agent/netplan-v4-feedback-stage/PREPARATION.json`,
|
||||
`TEST_RESULTS.txt`, `INSTALLER_TEST_RESULTS.json`, `ORDINARY_OFFER_TEST.json`.
|
||||
|
||||
Runtime installation, real feedback reception, and a separately authorized field
|
||||
trial remain outstanding. This finishes the code connection for bounded control;
|
||||
it is NOT an unrestricted continuous-production controller or a hardware
|
||||
commissioning certificate. Corrected model selection and actual real-world savings
|
||||
must be checked against their own data. Existing safety gates are not bypassed.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Passive V4 data capture - user inventory and raw acquisition
|
||||
|
||||
Status 2026-10-02: source implementation, CLI/mocked tests and staged user installation.
|
||||
Not part of the live manager, not a control or training release.
|
||||
|
||||
## Lihrenmoos user-reported inventory (not applied to running configuration)
|
||||
|
||||
- GoodWe 1/2: each 50 kW AC, each 156 kWh reported physical battery capacity.
|
||||
- SolarEdge: 10 kW AC, 10 kWh reported physical battery capacity.
|
||||
- EV allocation: 30 kW / 160 kWh total, understood as 10 kW EV per inverter.
|
||||
- PV roof: approximately 20 kWp east and 20 kWp west. Tilt/individual strings unknown.
|
||||
- Nominal battery sum: 322 kWh. 162 kWh nominal difference to EV allocation is NOT a
|
||||
verified usable SDL capacity. AC ratings are not verified battery charge/discharge ratings.
|
||||
- Exact types, firmware, common AC meter boundary and device watchdog remain unverified.
|
||||
- Current saved manager topology still reports 161.44 kWh / 39 kW. Gateway currently
|
||||
lists 130/130/8 kWh and 57/57/5 kW. These may use a different usable/nominal basis.
|
||||
Do not rebase virtual SOC or change current controls based on these notes alone.
|
||||
|
||||
## Why collect before boundary approval
|
||||
|
||||
Record the actual original numerical channels now, with source metadata and timestamps.
|
||||
The algebraic base-load candidate is useful for diagnosis, but NEVER a certified base-load
|
||||
training sample until the physical AC/DC boundary and historical recipe are verified.
|
||||
No dependence on hardware-watchdog approval for passive recording.
|
||||
|
||||
21 channels: PCC power, legacy displayed PCC power, PV for three inverters, three physical
|
||||
battery powers, EV/SDL virtual powers and SOCs, three physical SOCs, dynamic EV limits,
|
||||
T1/T2 import counters, EV/SDL requested setpoints (READ ONLY).
|
||||
A cyclic script reads values already present in Symcon every 30 seconds. It does not issue
|
||||
additional Modbus polls. Original VariableUpdated/VariableChanged are retained; these are
|
||||
Symcon timestamps, not independent device-measurement timestamps. Two-pass reads detect
|
||||
concurrent changes; missing/non-numeric/re-used channels remain explicit quality failures.
|
||||
|
||||
## Storage and interpretation
|
||||
|
||||
Daily append-only raw-YYYYMMDD.jsonl in the separate capture stage data directory, UTC
|
||||
capture times plus original source timestamps, immutable mapping/inventory snapshots by
|
||||
SHA256. Raw errors and quality_hold are recorded too; nothing is invented as zero.
|
||||
The raw observations are a source for later documented 5-minute aggregation, not themselves
|
||||
aligned 5-minute means. No interpolation, training publication or future SDL assumption.
|
||||
|
||||
A 512 MiB journal quota, 64 MiB daily limit, 256 KiB record cap and 100 MiB free-space
|
||||
reserve bound recording. Limits fail visibly without deleting old data. File permissions
|
||||
0640; no upload credentials, request bodies, complete instance configurations or secrets.
|
||||
A separate status variable and STATUS.json show latest capture and failures. Process restart
|
||||
retains files; an incomplete final JSONL row is preserved for manual review, not extended.
|
||||
|
||||
## Install / stop
|
||||
|
||||
Staged user entry point: /srv/agent/netplan-v4-data-capture-stage/install.php
|
||||
Execute in the Symcon PHP editor (not a Linux shell):
|
||||
|
||||
require '/srv/agent/netplan-v4-data-capture-stage/install.php';
|
||||
|
||||
Installer checks exact source hashes and numeric source identities before creating a NEW
|
||||
root category ENELIX_V4_PASSIVE_CAPTURE, a status string, collector script and stop script.
|
||||
IPS_SetScriptTimer enables only this new script at 30 seconds. Repeating installation is
|
||||
idempotent. Unrelated/edited scripts or object identity collisions are not overwritten.
|
||||
|
||||
Stop using the newly created script 'Nur diese Messaufnahme stoppen'; it disables only
|
||||
this collection timer, retains collected files, and does not stop the manager or battery.
|
||||
No module reload, existing archive change, existing timer/poller change, service restart,
|
||||
forecast selection, battery reservation or actuator command is included.
|
||||
|
||||
Current tests: 36 core/file tests + 19 mocked native lifecycle tests with PHP CLI on the
|
||||
test host. First real capture and future timer execution require the user activation.
|
||||
These tests do not certify hardware mappings or a Symcon-kernel run.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Historical publication timing (2026-10-03)
|
||||
|
||||
## Implemented scope
|
||||
|
||||
`services/netplan-v4/netplan_v4/history_timing.py` implements optional, bounded
|
||||
retrospective estimates between consecutive identical source values at DIFFERENT
|
||||
original publication timestamps. It is used by the existing measurement pipeline,
|
||||
model training and forecast source substitution. It is NOT a real-time estimator,
|
||||
a measurement certificate, an actuator authority or a repair of device communications.
|
||||
|
||||
No source `maxAgeSeconds` is changed. When a regular hold expires, only its tail
|
||||
until the next qualifying original publication may be estimated. Strict processing
|
||||
remains the default. The following forbid the estimate: unequal endpoints,
|
||||
nonfinite/missing observations, conflicting same-timestamp values, collector gaps,
|
||||
invalid source intervals or a span beyond the explicit source-specific bound.
|
||||
There is no extrapolation past the last observation and no fabricated zero.
|
||||
|
||||
Each window reports `publicationEstimatedSeconds`, per-source estimated portions,
|
||||
uncovered-source seconds and `availableNotBefore`. Training/validation must not use
|
||||
a confirmation or receipt before it was actually available. Backfilled windows
|
||||
cannot establish a supposedly causal holdout at a date before their availability.
|
||||
All physical windows remain configured estimates; even full support is not proof
|
||||
that the physical waveform stayed constant between the two endpoint readings.
|
||||
|
||||
## Evidence and model assumptions
|
||||
|
||||
Read-only source configuration projection on the test server, snapshot
|
||||
2026-10-03T07:47:24Z: GoodWe 1 instance 19742 (ModBus Device) has Poller 60000 ms;
|
||||
GoodWe 2 instance 57658 has Poller 5000 ms; SolarEdge scale instance 48996
|
||||
(ModBus Address) has Poller 20000 ms. No polling setting was changed.
|
||||
Projection program: /srv/agent/netplan-v4-application-build/inspect_source_update_policy.py.
|
||||
|
||||
Official Symcon documentation accessed on 2026-10-03:
|
||||
https://www.symcon.de/de/service/dokumentation/modulreferenz/geraete/modbus-rtu-tcp/vorlagen/
|
||||
It documents value publication for ModBus Device on changes or when the variable is
|
||||
older than 60 seconds, including ordinary and virtual addresses. VariableUpdated
|
||||
is therefore not automatically the last successful device poll. The documentation
|
||||
is not a proof that a particular installed device link was healthy.
|
||||
|
||||
Explicit Lihrenmoos HISTORICAL estimate bounds:
|
||||
- GoodWe 1 PV/physical storage: equal original publications at most 130 s apart
|
||||
(60 s publication suppression + configured 60 s poll + 10 s scheduling allowance).
|
||||
- GoodWe 2 PV/physical storage: at most 75 s (60 + 5 + 10).
|
||||
- SolarEdge scale: equal scale publications at most 90 s apart. This is a modelling
|
||||
bound based on the configured 20 s polling and observed sparse scale publication,
|
||||
not a manufacturer guarantee and not proof of the same internal implementation
|
||||
as ModBus Device. Raw AC power remains under the original strict limit.
|
||||
The 10 s margins are explicit scheduling assumptions, NOT measured timing guarantees.
|
||||
Gaps of many minutes (e.g. the previously observed GoodWe gaps above 600 s) are NOT
|
||||
bridged. Nonzero changes are never interpolated by this policy.
|
||||
|
||||
## Versioned dataset without restarting collection
|
||||
|
||||
`lihrenmoos-physical-published-v2` references immutable observations in
|
||||
`lihrenmoos-physical-v1`. No observations are copied, deleted, relabelled or resent.
|
||||
Registration requires the same installation and identical original mapping,
|
||||
formula, coverage and training configuration. Cycles/reference chains and device
|
||||
uploads directly into a derived dataset are rejected. Original ingestion continues
|
||||
unchanged in the Manager. The 24 usable-hour minimum stays in force.
|
||||
|
||||
Models/windows are stored separately for v1/v2 and record their timing policy. An
|
||||
existing selected model/family is not automatically switched by this release.
|
||||
The current SDL request still uses the strict current-state requirements; no
|
||||
historical bridge grants a future SDL schedule or a live battery permission.
|
||||
|
||||
## Tests and release
|
||||
|
||||
254 isolated Python tests passed (219 previous + 35 new including simulated
|
||||
installation/recovery). Includes the actual service pipeline from referenced
|
||||
observations to model and optimizer using synthetic records. No new target-container
|
||||
or field-history evaluation has been run with this release yet; do not infer an
|
||||
improved real coverage percentage or production acceptance from unit tests.
|
||||
Evidence: /home/agent/services/qa/history-timing-20261003/ALL_TEST_RESULTS.txt.
|
||||
|
||||
Prepared command on enelix-services (root required for Docker):
|
||||
|
||||
python3 /home/agent/services/netplan-v4-shadow/commissioning/deploy_history_timing.py \
|
||||
--plant e3a08f9e-af12-4695-99bd-8b51c0520021 --apply
|
||||
|
||||
Default without --apply validates staged source only. The apply path backs up source
|
||||
files, builds and runs the exact target tests, backs up the V4 SQLite database, and
|
||||
recreates ONLY the V4 service. A failed test restores source without a service
|
||||
restart; failed post-deploy validation attempts the previous image and preserves
|
||||
additive data. Concurrently modified files are not silently overwritten.
|
||||
|
||||
No portal/forecast/tariff/Symcon restart, no new sensor requests, no change to
|
||||
161.44 kWh / 39 kW, SOC accounting, old raw samplers or actuator permissions.
|
||||
The new dataset is registered but not selected for the active forecast. A root
|
||||
execution and successful report are still required to install it. Previous release
|
||||
manifests remain historical records; do not bypass them to reapply an older build.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Numeric mapping identity defect - 2026-10-03
|
||||
|
||||
## Confirmed from installed code and read-only evidence
|
||||
|
||||
The pending batch contains 120 observations: zero duplicate timestamps, zero
|
||||
backwards pairs, but 75 identity failures. The initial 45 observations in this
|
||||
batch are imported observer records; later native captures use the saved manager
|
||||
configuration. Evidence: test-host `PENDING_DELIVERY_REVIEW.json`.
|
||||
|
||||
The configured mapping fingerprint is
|
||||
`517d1907631c7f152096e21759bb3452cdd0b4b1b73f91f1c7cd3bd62d8aaa8b`.
|
||||
Reproducing native configuration loading generates
|
||||
`3a1a9cf42aae49f55385d2350da74cae780ae12f170e4976849591e48a393994`.
|
||||
The sole difference in the compared configurations is numeric representation of
|
||||
`accounting.splitToleranceW`: prepared float 100.0 versus saved integer 100.
|
||||
Both inventory fingerprints and all measurement definitions match.
|
||||
`NetzfahrplanV4Bilanzierung::konfigurieren` validated this field using a function
|
||||
which returns a float, but discarded the normalized return value. Other numeric
|
||||
factors were already assigned from that validator.
|
||||
|
||||
## Tested source correction
|
||||
|
||||
Assign the returned float to `splitToleranceW`. Equal numeric settings now retain
|
||||
the original fingerprint through a plain JSON property round trip. Actual numeric
|
||||
changes still produce distinct fingerprints, and boolean/string values are still
|
||||
rejected. Input configurations are not mutated.
|
||||
|
||||
42 isolated PHP checks passed on the test host: 34 accounting regressions plus
|
||||
8 numeric identity regressions. A separate check with the actual prepared capture
|
||||
configuration reproduces the configured fingerprint after the property round trip.
|
||||
Evidence: `/srv/agent/netplan-v4-application-build/NUMERIC_MAPPING_TEST_RESULTS.json`
|
||||
and `NUMERIC_MAPPING_TEST_RESULTS.txt`. Library SHA256:
|
||||
`ffda2a03dbb226a4405a2ad66704c080a9dc475ce64282ad3f0d7d60034ab199`.
|
||||
|
||||
## Not deployed / recovery still incomplete
|
||||
|
||||
No installed library, original outbox, cursor, server validator, settings, dataset
|
||||
registration or actuator permission was changed in this turn. Future-capture
|
||||
normalization alone does not recover already stored native observations. The
|
||||
server correctly rejects an entire batch containing a mismatched mapping.
|
||||
|
||||
An untested proposal to send distinct immutable dataset versions in separate
|
||||
batches was removed from the development source and preserved only as a draft at
|
||||
`/home/agent/services/qa/numeric-mapping-20261003/UNTESTED_SENDER_DRAFT.patch`.
|
||||
Metadata preparation files are not an installation or permission to ingest into
|
||||
an unregistered dataset. Tool writes for the complete server recovery package were
|
||||
blocked; no alternative deployment or identity-check bypass was performed.
|
||||
|
||||
Next recovery must preserve original records and provenance, retain exact
|
||||
per-dataset identity validation, acknowledge only accepted data and include tests
|
||||
for the mixed 45/75 batch, retries, partial ACKs and no cursor/data loss. Do not
|
||||
skip the rejected block, rewrite hashes in raw journals or re-run old installers.
|
||||
@@ -0,0 +1,48 @@
|
||||
# V4 plan reception and local preview (observation only)
|
||||
|
||||
This module does not execute a battery command. The existing regulator and its
|
||||
setpoints remain unchanged. `NetzfahrplanAktiv` must remain false.
|
||||
|
||||
The optional `NetzfahrplanV4EmpfangAktiv` property defaults to false. When enabled
|
||||
with the existing shadow sender, one bounded GET is made before the next native
|
||||
operation POST, avoiding reading the plan immediately after queuing its replan.
|
||||
A successful inspection acknowledges `shadow_seen` once per plan/step. This is
|
||||
not `applied`. An HTTP failure clears the local preview; it does not change the
|
||||
old control mode or its connection credentials.
|
||||
|
||||
The V4 service must first support `receiverProtocolVersion=1`, installation IDs
|
||||
in envelope and plan, per-input event references, and the stable controlContext
|
||||
used at calculation time. Old or cross-plant responses are deliberately refused.
|
||||
The client checks fresh/lastRun/pending status, exact configuration revision,
|
||||
model-family registry, known price coverage, contiguous half-open five-minute
|
||||
intervals, asset mapping and power balance. Numerical feasibility checks are
|
||||
not evidence of economic performance or permission to control devices.
|
||||
|
||||
A cache watchdog refreshes the diagnostic display every 10 seconds, without any
|
||||
network requests. Cached response older than 90 seconds, plan older than 900
|
||||
seconds, an expired step, invalid local measurement or policy change invalidates
|
||||
the preview. Missing targets are JSON null and '-' in the display, never zero.
|
||||
Restart/ApplyChanges clears the cache. Existing control functions are not edited.
|
||||
|
||||
The preview is currently limited to ONE battery. Multiple batteries are still
|
||||
accepted by the server planner; local allocation is explicitly unsupported in
|
||||
this stage. Preview feedback uses actual grid and battery power, local capacity,
|
||||
available power, minimum/maximum SOC, discharge hysteresis, network-charging
|
||||
permission and import/export/month limits. It exposes any remaining grid-limit
|
||||
violation instead of pretending the battery can resolve it. It is a momentary
|
||||
calculation only, NOT the commissioned real-time regulator. PV curtailment and
|
||||
external SDL/base-load separation must still be validated before any live test.
|
||||
|
||||
Two read-only variables below the manager expose a HTML summary and detailed
|
||||
JSON. No EnableAction, consumer RequestAction, SendData or actor property writes
|
||||
are used. The existing form.json and web portal assets are not modified.
|
||||
|
||||
Offline checks:
|
||||
|
||||
php tests/V4Receiver/protocol_checks.php
|
||||
php tests/V4Receiver/receiver_checks.php
|
||||
|
||||
The PHPUnit wrapper runs these in separate subprocesses, so fake IPS and curl
|
||||
functions cannot contaminate other module tests. The mocked transport uses no
|
||||
real credentials or connections. Full Symcon kernel testing and installation
|
||||
are separate commissioning steps; these unit tests are not field acceptance.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Separate physical load / EV-SDL observation (NO dispatch or training permission)
|
||||
|
||||
## Implemented 2026-10-02
|
||||
|
||||
- `NetzfahrplanV4Messsicht` recalculates independent physical-load, physical-storage,
|
||||
unfiltered modelled allocation and SolarEdge-origin views from explicitly identified
|
||||
raw snapshots. Quality failures in held virtual accounts do not veto otherwise
|
||||
consistent physical inputs. Missing/stale physical inputs still invalidate their
|
||||
respective view. No automatic relaxation of age/skew limits.
|
||||
- EV/SDL model: delta = physical - requestedEV - requestedSDL; distribute delta in
|
||||
proportion to the absolute requested powers. The SDL component is obtained from
|
||||
physical minus modelled EV to preserve numerical closure. This is an accounting
|
||||
convention, NOT independently measured EV and SDL and NOT a verified response.
|
||||
- Command changes more recent than the oldest physical feedback are flagged. Unknown
|
||||
change timestamps, tracking mismatch, aged/missing origins and idle unallocated
|
||||
power remain explicit. There is no held-value filter and no forced convergence of
|
||||
displayed power to the requested value. All proposals keep `canDispatch=false`.
|
||||
- Actual legacy filter outputs are displayed solely for comparison. They are not
|
||||
reused as inputs of physical-load or unfiltered partition calculations. No changes
|
||||
to the legacy filter, physical driver, EV/SDL setpoints, SOC integration or accounts.
|
||||
|
||||
## New read-only origin finding
|
||||
|
||||
Selected event 38921 writes legacy PV variable 20335 using:
|
||||
|
||||
a = 10 ** GetValue(41853) * GetValue(37975) + GetValue(21447)
|
||||
if (a < 0) a = 0
|
||||
SetValue(20335, a)
|
||||
|
||||
Arithmetic statements were inspected without exposing arbitrary scripts/credentials.
|
||||
Verified saved configuration: variable 37975 parent 35514, register 40083; scale
|
||||
41853 parent 48996, register 40084; battery 21447 parent 30789. An independently
|
||||
updated 20335 is therefore not an independent PV reading and cannot certify all
|
||||
input timestamps. A second constrained arithmetic read confirmed the condition
|
||||
`if ($a < 0)`; this clamp applies to the legacy calculated PV value, not to the
|
||||
new signed terminal-power observation. Neither proves the full AC/DC boundary.
|
||||
|
||||
The staged observer additionally records the ORIGINAL mantissa and exponent, not
|
||||
only their periodically rewritten derived PV result. The alternative candidate
|
||||
uses the scaled terminal power ONCE for this inverter (removing its derived-PV
|
||||
and battery terms). GoodWe AC/DC boundaries and full-site physical boundary are
|
||||
still unverified. This is not a definitive corrected house meter or live forecast.
|
||||
|
||||
## Standalone installation / operational scope
|
||||
|
||||
Stage on test host: `/srv/agent/netplan-v4-separated-observer-stage`.
|
||||
User invocation inside Symcon only:
|
||||
|
||||
require '/srv/agent/netplan-v4-separated-observer-stage/install.php';
|
||||
|
||||
Creates only a NEW root category, three textual diagnostic variables, observe and
|
||||
stop scripts and a NEW 30-second timer. Reads 23 existing numeric variables twice;
|
||||
no Modbus polling requests, network calls, archive settings, existing module or
|
||||
control timer updates. Existing 21-channel raw collector continues untouched.
|
||||
|
||||
New daily journals contain the selected numeric raw snapshot and derived views.
|
||||
A new immutable mapping/inventory reference binds the corrected user EV allocation
|
||||
161.44 kWh / 39 kW. Original collector metadata and its correction addendum remain
|
||||
unchanged. No rescaling or resetting of virtual SOC/energy accounts.
|
||||
|
||||
Derived journal and LATEST/STATUS reports are 0644 WITHIN mode-0700 private stage
|
||||
and data directories, allowing the authorized agent to inspect these selected
|
||||
new outputs without repeated manual exports. No permissions on original journals,
|
||||
Symcon directories or unrelated data are changed. Quotas reuse 64 MiB/day, 512 MiB
|
||||
in total and 100 MiB disk reserve; exceeding them stops recording visibly and does
|
||||
not delete old data. No network transfer or cloud trainer is activated.
|
||||
|
||||
## Validation and limitations
|
||||
|
||||
40 pure view checks + 32 native-mock checks passed on PHP 8.3 CLI at test host.
|
||||
Regression: 36 raw-capture + 19 native-capture + 34 accounting checks passed with
|
||||
new additive `raw_power`/`scale` units. Total 161 isolated checks. Full relevant PHP
|
||||
source lint passed. Not a full PHPUnit run, Symcon kernel trial or hardware test.
|
||||
|
||||
Tests cover opposite EV/SDL requests, held output, non-unique partition labelling,
|
||||
negative terminal power, scale/sentinel validity, old dependency timestamps, changed
|
||||
commands, missing source, no duplicate native objects, unchanged existing signals,
|
||||
append preservation, source drift, private output restrictions and stop behavior.
|
||||
|
||||
The observer is PREPARED, not started by the assistant. It does not replace the
|
||||
installed receiver or its existing preview calculation. The preview/driver must
|
||||
only be switched after measurement-boundary and feedback policy acceptance. Model
|
||||
training and five-minute reconstruction must consume the new versioned snapshots
|
||||
separately; there is no training eligibility or zero future-SDL assumption here.
|
||||
|
||||
Device-side command-loss protection and exact model/firmware identification remain
|
||||
open. Official SolarEdge PDF fetch was unavailable (403/error); no device-specific
|
||||
register suitability or watchdog behavior was inferred from a third-party source.
|
||||
@@ -0,0 +1,77 @@
|
||||
# V4 Unified RC1 - actual scope and release boundary
|
||||
|
||||
This release consolidates application data recovery, native delivery status, economic
|
||||
family comparison, and optional verified archival. It is NOT a production actuation
|
||||
release. No claim that all original product requirements or hardware acceptance are
|
||||
complete. Sources are in the existing develop branch, not feature branches.
|
||||
|
||||
## Delivered code
|
||||
|
||||
* Audited recovery for the exact 100.0/100 splitToleranceW serialization defect.
|
||||
The internal operator endpoint validates the two complete JSON representations,
|
||||
their exact SHA256, installation, inventory, and every field. Only that numeric
|
||||
representation may differ. Device-supplied arbitrary aliases are never accepted.
|
||||
Existing rows/measurements are not rewritten. The original received mapping and
|
||||
proof reference are recorded with the observation. A mixed 45/75 block and retries
|
||||
are regression tested. Adoption requires an explicit deployment flag.
|
||||
* Native normalization prevents recurrence. The sender preserves a separate last
|
||||
successful receipt even during scheduled/backoff states, reports safe numeric HTTP
|
||||
status (no response or credential text), and continues trying to deliver a durable
|
||||
prefix if the current capture cannot be appended. No cursor reset or skipped block.
|
||||
* Economic comparison is actually connected, not an empty score list. At the start
|
||||
of a local calendar day the worker freezes comparable plans for families 3/13/23
|
||||
with the then-known forecasts, prices and common battery/limit state. Completed
|
||||
days are evaluated against subsequently acquired physical residual data. Initial
|
||||
energy, losses, common terminal energy valuation and 15-minute demand charge are
|
||||
included; monthly maxima are charged once over a comparison period, not per day.
|
||||
GUI lookback/minimum days/coverage/switch margin are applied by the selector.
|
||||
* v1 economic scope: one grid-charge-enabled battery, frozen day-ahead plans, with
|
||||
local grid-target tracking simulated during the day. It is NOT a full receding-
|
||||
horizon replay. Historical external SDL uses the then-recorded SDL request and is
|
||||
clearly marked as an estimate, not independently measured delivery. Missing
|
||||
actuals, unmatched contexts and breaches cannot become zero cost or a successful
|
||||
comparison. Broader device topologies stay collecting; no fake equivalence.
|
||||
* Lossless archive implementation and tests are present for acknowledged old native
|
||||
outbox days and old server observations/plans. Archives are verified before plain
|
||||
rows/files are retired. Active plans, acknowledged plans and unacknowledged data
|
||||
are excluded. Archiving is OFF by default; enable only with an explicit storage
|
||||
policy (native NetzfahrplanV4ArchivAktiv; server NETPLAN_V4_ARCHIVE_ENABLED=1).
|
||||
Compressed archives are retained: this is not an external backup or infinite disk.
|
||||
|
||||
## Deployment is not data/model/actuator acceptance
|
||||
|
||||
Server entry: commissioning/deploy_unified_release.py. Default checks sources only.
|
||||
--apply builds and tests a separate candidate context with no network/production
|
||||
mounts before replacing the V4 service. It preserves the running plant allowlist,
|
||||
backs up SQLite consistently, and uses a pinned built image. Only the V4 planner JS
|
||||
asset is updated; unrelated GUI work, portal runtime, tariffs, forecasting, Symcon
|
||||
and actuators are untouched. Rollback restores previous source/image; additive audit
|
||||
rows remain. Database schema changes do not overwrite old observations.
|
||||
|
||||
--approve-numeric-mapping-compatibility explicitly authorizes the demonstrated pair
|
||||
of equivalent configuration representations. Without it, strict original validation
|
||||
remains and backlog containing the legacy serialization is still rejected.
|
||||
|
||||
Lihrenmoos native stage: /srv/agent/netplan-v4-unified-release/install.php.
|
||||
This patches three data-only libraries, reloads the existing library, compares all
|
||||
pre-existing properties and lets the existing data timer resume. No raw replay,
|
||||
force-skip or actuator callback from the installer. Archiving stays disabled by default.
|
||||
|
||||
## Remaining product work - do not label as complete
|
||||
|
||||
1. The full production dispatcher is not installed. Earlier controlled-trial code
|
||||
still has bounded pilot grants; continuous production authority and complete
|
||||
corrected physical/virtual feedback integration are NOT delivered by this RC.
|
||||
2. The actual host/container/kernel and field acceptance of this release are not
|
||||
replaced by CLI mocks. Real source validity and independent command-loss behavior
|
||||
are not asserted by a measurement algebra or an override.
|
||||
3. The replay limitation above must be accepted as a beta feature, or extended to
|
||||
continuous rolling reoptimization and additional asset types before advertising
|
||||
the original full automatic-selection requirement.
|
||||
4. Native multi-plant commissioning, external backups, retained archive lifecycle,
|
||||
model quality on field data and publication authentication remain operational
|
||||
release criteria. The tests do not demonstrate actual cost savings.
|
||||
|
||||
Do not re-run obsolete piecemeal installers after this release, bypass their source
|
||||
checks, turn the previous grid schedule on, or turn 'productionReady' true manually.
|
||||
The retained EV capacity is 161.44 kWh / 39 kW; SDL reserve is already outside EV.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Prognosebedienung im bestehenden Manager
|
||||
|
||||
Stand: 06.10.2026. Gepruefter Integrationsstand zur von Daniel freigegebenen
|
||||
Veroeffentlichung auf develop und beta. Kein Deployment und keine neue Stellfreigabe.
|
||||
|
||||
## Umfang
|
||||
|
||||
Der separate Formularbereich `V4 Planempfang / Prognosemanager` entfaellt.
|
||||
Empfang, Planpruefung, Lizenzportal-Verweis und explizites Ein-/Ausschalten
|
||||
stehen im bestehenden Bereich `Prognose / Forecast`.
|
||||
|
||||
Der neue Formularaufruf `FormNetzfahrplanSchalten` delegiert an den vorhandenen
|
||||
`V4ManagerAktivtestSchalten`-Pfad. Er prueft beim Start nochmals gespeicherte
|
||||
Prognosekonfiguration, Netzfahrplanlizenz, Managerstatus und bestehende lokale
|
||||
Testfreigaben. Ein Formularaufruf allein erteilt keine Berechtigung. Ausschalten
|
||||
bleibt auch nach Lizenzverlust moeglich. Der bestehende Stopp beinhaltet eine
|
||||
frisch berechnete normale Regelung nach Widerruf der V4-Sitzung; er ist kein
|
||||
anlagenweiter Not-Aus und veraendert keine unabhaengigen SDL-Auftraege.
|
||||
|
||||
Die Anzeige nennt weiterhin Testbetrieb und das bestehende Testende. Es gibt
|
||||
keine neue Dauerbetriebsfreigabe, keinen Reset der 48-Stunden-Frist und keine
|
||||
Aenderung an Watchdog-Ausnahmen, Messwertgrenzen oder Geraeteschutzpruefungen.
|
||||
|
||||
## Kompatibilitaet
|
||||
|
||||
- Keine Properties, Attribute, Objekt-IDs, Timer, Datenpfade oder Backend-APIs werden migriert.
|
||||
- Prognosemodelle, Optimierer, Planempfang, ACKs, Messaufnahme und Versand bleiben unveraendert.
|
||||
- Die vorhandene Portalpruefung fuer `grid_schedule` bleibt massgeblich. Keine Portal- oder Lizenzbestellung wird geaendert.
|
||||
- `NetzfahrplanAktiv` bedeutet weiterhin den alten Regler. Bei vorhandener V4-Konfiguration wird dessen ausgeschalteter Schalter ausgeblendet. Ein eingeschalteter Altregler bleibt zum Ausschalten sichtbar.
|
||||
- Nicht auf V4 eingerichtete Installationen behalten den bisherigen Schalter. Diese Aenderung provisioniert keine neue V4-Installation.
|
||||
- `PrognoseAktiv` wird nicht zum automatischen V4-Startsignal. Bestehende Aufnahme-/Sender-Properties und die bisherige Laufzeitvariable bleiben erhalten.
|
||||
|
||||
## Dateien
|
||||
|
||||
- `Manager/form.json`: stabiler Anker fuer den bestehenden Prognosebereich.
|
||||
- `Manager/module.php`: Formularintegration und gepruefter Bedienaufruf.
|
||||
- `libs/ManagerPrognoseFormular.php`: reine Formularzusammenstellung ohne I/O.
|
||||
- `libs/ManagerNetzfahrplanV4EmpfangTrait.php`: nur bisherigen Formulargenerator entfernt; Empfangscode unveraendert.
|
||||
- `tests/PrognoseFormular/`: Formular- und echte RequestAction-Pruefungen mit simulierten IPS-/Stellaufrufen.
|
||||
- `tests/V4Receiver/receiver_checks.php`: Strukturpruefung an die zusammengefuehrte Oberflaeche angepasst.
|
||||
|
||||
## Tests
|
||||
|
||||
Isoliert im PHP-8.3.6-CLI auf einer Quellkopie, nicht im Symcon-Kernel:
|
||||
|
||||
| Suite | Erfolgreiche Pruefungen |
|
||||
|---|---:|
|
||||
| PrognoseFormular/checks.php | 59 |
|
||||
| PrognoseFormular/manager_checks.php | 26 |
|
||||
| V4Receiver/receiver_checks.php | 32 |
|
||||
| V4ControlTrial/checks.php | 96 |
|
||||
| V4Feedback/receiver_checks.php | 8 |
|
||||
| V4Feedback/device_read_checks.php | 9 |
|
||||
| V4ControlTrial/register_checks.php | 1 |
|
||||
| Gesamt | 231 |
|
||||
|
||||
Zusaetzliche Gesamttests des zusammengefuehrten Stands am 06.10.2026:
|
||||
|
||||
- PHP 8.3.6 / PHPUnit 9.6.36: 357 Tests, 1758 Assertions erfolgreich.
|
||||
- Syntaxpruefung: alle 150 PHP-Dateien erfolgreich.
|
||||
- Backend: 310 Python-Tests erfolgreich mit den vorhandenen QA-Abhaengigkeiten.
|
||||
- Portal: 16 Node-Tests erfolgreich.
|
||||
- Konfliktmarker- und Patch-Pruefung erfolgreich.
|
||||
|
||||
Die Pruefungen liefen in isolierten Quellkopien. Die PHP-Laufzeit wurde nur im
|
||||
Agent-Arbeitsordner entpackt, ohne Systeminstallation. Kein Symcon-Kernel-/GUI-
|
||||
oder Hardwaretest; der vorhandene Docker-Zugang war nicht nutzbar.
|
||||
|
||||
Eine vorbestehende, nicht committe Aenderung an measurement_pipeline.py wurde
|
||||
bewusst nicht uebernommen: Sie erzeugte fuer jede konfigurierte Messgrenze ein
|
||||
accountingEvidenceId trotz measurementBoundaryVerified=false und verletzte den
|
||||
bestehenden Backendtest. Die Datei bleibt im Integrationsstand bytegleich zum
|
||||
bisherigen versionierten V4-Stand. Die fremde Arbeitskopie und das laufende
|
||||
Backend wurden nicht veraendert. Eine spaetere Freigabe dieser Messgrenze braucht
|
||||
einen eigenstaendigen fachlichen Nachweis, keine Anpassung des Tests an das Label.
|
||||
|
||||
## Offener Abschluss
|
||||
|
||||
Der Auftrag ist mit dieser Bedienintegration noch nicht vollstaendig umgesetzt:
|
||||
|
||||
- Die dokumentierte Batterietimerblockade und die fehlende Anlagenabnahme werden hier nicht behoben.
|
||||
- Ein regulaerer, lizenzierter Dauerbetrieb anstelle des begrenzten Testpfads ist nicht implementiert.
|
||||
- Die separaten Laufzeitvariablen und Diagnoseordner werden noch nicht entfernt oder ausgeblendet.
|
||||
- In gespeicherten Settings vom 06.10.2026, 13:06:07 UTC sind nur die Kategorien 21196 (`ENELIX_V4_SEPARATED_OBSERVATION`) und 57590 (`ENELIX_V4_PASSIVE_CAPTURE`) eindeutig als V4-Diagnosebereiche erkannt. Darin liegen weiterhin Beobachtungs-/Aufnahmeskripte. Keine pauschale Loeschung nach Namen.
|
||||
- Der allgemeine `Testordner` 10249 enthaelt auch Abrechnung, Schnittstellen und weitere nicht zu dieser Integration gehoerende Objekte. Er bleibt unangetastet.
|
||||
- Vor einem Rollout aktuelle Moduldateien sichern, mit dem getesteten Stand vergleichen und Abhaengigkeiten pruefen. Keine pauschalen Modulupdates, Reloads oder Dienstneustarts aus diesem Dokument ableiten.
|
||||
- Die Divergenz wurde in einem separaten develop-Checkout auf ubuntu zusammengefuehrt: Server-89-Historie bis ce525a1 und origin/develop bis 5437f3f. Die Originalarbeitsverzeichnisse mit fremden Aenderungen bleiben erhalten. Daniel hat Merge, Commit und Push auf develop und beta ausdruecklich freigegeben; main bleibt unveraendert.
|
||||
|
||||
## Uebergabe und Ruecksetzung
|
||||
|
||||
Die fruehere Einzelpatch-Uebergabe ist durch den zusammengefuehrten Quellstand
|
||||
ersetzt. Die Ausgangshistorie und die zugehoerigen lokalen V4-Quellen sind unter
|
||||
/srv/agent/prognose-integration-20261006 gesichert. Der gepruefte Kandidat
|
||||
enthaelt keine temporaren Live-Hooks, Zugangsdaten oder fremden uncommitteten
|
||||
Ladestationsaenderungen. Ruecksetzungen nur additiv nach Driftpruefung; keine
|
||||
fremden Aenderungen verwerfen. Fuer einen spaeteren Runtime-Rollout sind eine
|
||||
eigene Sicherung, Abhaengigkeitspruefung und Anlagenabnahme erforderlich.
|
||||
|
||||
## Vorschlag PR-Text
|
||||
|
||||
**Titel:** Manager: Prognosebedienung im bestehenden Forecast-Bereich buendeln
|
||||
|
||||
**Aenderung:** Separaten V4-Formularbereich entfernen, bestehende Funktionen
|
||||
unter Prognose / Forecast anbieten und explizite Bedienaktionen zusaetzlich
|
||||
gegen Lizenz-, Konfigurations- und Freigabestatus pruefen.
|
||||
|
||||
**Unveraendert:** Prognose-/Optimierungsbackend, Empfangsprotokoll, Anlagenlimits,
|
||||
befristete Testfreigaben, Datenhistorie und SDL. Keine automatische Aktivierung.
|
||||
|
||||
**Pruefung:** 357 PHPUnit-Tests, 310 Backend-Tests, 16 Portal-Tests sowie die
|
||||
oben genannten isolierten Bedien-/V4-Checks; kein Hardware- oder Symcon-GUI-Abnahmetest.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Erste Schritte mit Enelix
|
||||
|
||||
Vom ersten Überblick zur eingerichteten Anlage: Hier siehst du die Reihenfolge und woran du erkennst, dass du zum nächsten Schritt gehen kannst. Die verlinkten Anleitungen erklären die einzelnen Einstellungen.
|
||||
|
||||
**Für Betreiber:** Beginne mit [Enelix verstehen](index.md). Lass die technische Einrichtung fachkundig durchführen und dir anschließend die Bedienung deiner eigenen Anlage zeigen. **Für Installateure:** Arbeite die folgenden Schritte in dieser Reihenfolge ab. Beginne mit einem einzigen Verbraucher.
|
||||
|
||||
## 1. Die Anlage vorbereiten
|
||||
|
||||
Du brauchst IP-Symcon ab Version 8.0, Zugang zur Verwaltungskonsole und die tatsächlichen Mess- und Geräteanschlüsse deiner Anlage. Besonders wichtig ist die **Netzleistung am Netzanschlusspunkt**: Sie zeigt, ob insgesamt Strom bezogen oder eingespeist wird. Die PV-Produktion allein ersetzt diese Messung nicht.
|
||||
|
||||
Halte fest, welches Gerät zuerst geregelt werden soll und welche Regelung es bisher steuert. Sichere die bestehende Konfiguration. Zwei Regler dürfen nicht unkoordiniert dieselben Ausgänge bedienen.
|
||||
|
||||
**Ergebnis:** Du kannst die relevanten Messwerte in IP-Symcon ansehen, kennst die zulässigen Gerätegrenzen und hast einen Rücksetzweg. Weiter mit [Voraussetzungen und Installation](installation.md).
|
||||
|
||||
## 2. Die Bibliotheken installieren
|
||||
|
||||
Füge **Enelix EMS** über die Modulverwaltung von IP-Symcon hinzu. Installiere **Enelix Utils** zusätzlich, wenn du beispielsweise dessen Diagramme oder Geräteanbindungen benötigst. Verwende die Repository-Adressen und den freigegebenen Kanal aus der [Installationsanleitung](installation.md#2-bibliotheken-hinzufügen).
|
||||
|
||||
Eine installierte Bibliothek stellt zunächst die Modulvorlagen bereit. Sie richtet noch keinen vollständigen Manager und keine Verbindung zu deinen Geräten ein.
|
||||
|
||||
**Ergebnis:** Die benötigten Bibliotheken sind ohne Warnung vorhanden und ihre Module stehen zum Anlegen von Instanzen zur Verfügung.
|
||||
|
||||
## 3. Den Manager einrichten
|
||||
|
||||
Lege eine **Manager-Instanz** an. Lass ihre Regelung zunächst ausgeschaltet. Binde die passende Lizenz, wähle die Netzleistungsmessung und prüfe Einheit und Vorzeichen. Stelle nur Grenzen ein, die für diese Anlage freigegeben sind.
|
||||
|
||||
In der normierten Messung bedeutet **positiv: Netzbezug**, **negativ: Einspeisung**. Ein falsch zugeordnetes Vorzeichen kann zu falschen Regelentscheidungen führen.
|
||||
|
||||
**Ergebnis:** Der Manager hat einen gültigen Lizenzstatus und erkennt eine aktuelle, plausible Netzleistung. Die [Manager-Anleitung](manager.md) führt durch Lizenzierung, Messquelle und Betriebsgrenzen.
|
||||
|
||||
## 4. Einen Verbraucher hinzufügen
|
||||
|
||||
Lege die passende Verbraucherinstanz an, zum Beispiel einen Warmwassererwärmer oder eine Ladestation. Richte dort Geräteverbindung, Messwerte, Leistungsgrenzen und Schutzwerte ein. Anschließend wählst du diese Instanz **im Manager** aus.
|
||||
|
||||
Mit der Priorität bestimmst du die Reihenfolge bei der Verteilung. Kleinere Zahlen bedeuten höhere Priorität. Auch ein hoch priorisiertes Gerät erhält nur eine Vorgabe, die zu seinem gemeldeten Zustand und den eingestellten Grenzen passt.
|
||||
|
||||
**Ergebnis:** Gerät und Manager sind bewusst zugeordnet. Die Rückmeldungen sind plausibel; die Regelung bleibt bis zur Funktionsprüfung ausgeschaltet. Die [Verbraucheranleitung](verbraucher.md) erklärt die Unterschiede der Gerätetypen.
|
||||
|
||||

|
||||
|
||||
## 5. Unter Aufsicht prüfen
|
||||
|
||||
Prüfe zuerst Lizenzstatus, Messwerte und Störmeldungen. Teste anschließend mit der fachkundigen Person eine kleine, zulässige Vorgabe und vergleiche sie mit dem tatsächlichen Verhalten am Gerät. Prüfe auch Abschaltung und den Umgang mit fehlenden Messwerten. Erst danach kommen weitere Verbraucher hinzu.
|
||||
|
||||
**Ergebnis:** Es ist nachvollziehbar, welches Gerät wann reagiert und wie die Anlage in einen sicheren Zustand zurückkehrt. Eine Anzeige in der Konsole allein ist noch kein Nachweis einer funktionierenden Geräteansteuerung.
|
||||
|
||||
## 6. Die Anlage im Alltag nutzen
|
||||
|
||||
Lass dir in deiner Visualisierung die Netzleistung, die wichtigsten Verbraucherzustände und mögliche Störmeldungen zeigen. Im Manager zeigt **Betriebsart**, ob die Regelung inaktiv, im PV- oder im Peak-Betrieb ist. Die Freigaben am einzelnen Gerät bleiben ebenfalls wichtig.
|
||||
|
||||
Wenn ein Gerät nicht wie erwartet arbeitet, ändere nicht wahllos Prioritäten oder Schutzwerte. Prüfe zuerst Freigabe, Verbindung, Messwerte und Störtext. Die Abschnitte [Im Alltag](manager.md#im-alltag) und [FAQ](faq.md) helfen dabei.
|
||||
|
||||
**Ergebnis:** Du weisst, wo du den Anlagenzustand abliest, welche Bedienhandlungen vorgesehen sind und wen du bei einer Störung kontaktierst.
|
||||
|
||||
## Optional: Auswertungen und weitere Funktionen
|
||||
|
||||
Mit [Enelix Utils](utils.md) kannst du passende Zusatzfunktionen einrichten, etwa ein Energiediagramm oder einen Verbrauchskostenreport. Richte erst die benötigten Messquellen und Archivdaten ein; ein installiertes Auswertungsmodul hat nicht automatisch alle Daten deiner Anlage.
|
||||
|
||||
Welche Lizenzen du für deinen Ausbau benötigst und was sie kosten, siehst du in der [aktuellen Preisliste](preise.md).
|
||||
@@ -0,0 +1,73 @@
|
||||
# Häufige Fragen
|
||||
|
||||
## Brauche ich für die Dokumentation ein Konto?
|
||||
|
||||
Nein. Anleitungen, Modulreferenzen, FAQ und Preisliste sind öffentlich. Ein Konto wird für die persönlichen Funktionen des Lizenzportals benötigt.
|
||||
|
||||
## Welche IP-Symcon-Version wird benötigt?
|
||||
|
||||
Enelix EMS und Enelix Utils setzen IP-Symcon ab Version 8.0 voraus. Maßgeblich ist der Kernel der Installation, nicht nur die Version der Verwaltungskonsole.
|
||||
|
||||
## Muss ich EMS und Utils gemeinsam installieren?
|
||||
|
||||
Nein. Utils ist eine eigenständige Bibliothek. Installiere sie zusätzlich, wenn du ihre Module oder entsprechende Manager-Visualisierungen benötigst. Die Funktions- und Lizenzvoraussetzungen des gewählten Moduls gelten weiterhin.
|
||||
|
||||
## Welchen Updatekanal soll ich wählen?
|
||||
|
||||
Verwende den für deine Anlage freigegebenen Kanal: `main` entspricht Stable, `beta` Beta und `develop` Testing. Vergleiche den installierten Stand mit dem oben angegebenen Dokumentationskanal. Testsoftware benötigt eine kontrollierte Inbetriebnahme.
|
||||
|
||||
## Warum bleibt der Manager inaktiv?
|
||||
|
||||
Prüfe `Aktiv`, Lizenzstatus und Netzleistungsmessung. Fehlende, veraltete oder falsch normierte Messwerte verhindern korrekte Regelung. Kontrolliere anschließend Verbraucherzuordnung und Störtext. Die [Manager-Anleitung](manager.md) beschreibt die Reihenfolge.
|
||||
|
||||
## Wo ordne ich einen Verbraucher zu?
|
||||
|
||||
Im Manager. Verbraucher haben keine eigene Manager-ID-Property. Die automatische Suche zeigt Kandidaten; ausgewählt und aktiviert werden sie gezielt.
|
||||
|
||||
## Warum wird ein Verbraucher trotz Überschuss nicht eingeschaltet?
|
||||
|
||||
Mögliche Ursachen sind lokale Deaktivierung, fehlende Zuordnung, fehlende Lizenzmenge, Gerätefehler, ein zu kleines Budget, Temperaturbedingungen oder Mindestzeiten. Prüfe auch das angebotene Leistungsraster und den aktuellen Betriebszustand. Bei einer Ladestation gehören Fahrzeugstatus, Ladefreigabe und Solarladen dazu.
|
||||
|
||||
## Was bedeuten positive und negative Leistungen?
|
||||
|
||||
Am Netzanschlusspunkt bedeutet positiv Netzbezug und negativ Einspeisung. Bei Batterien bedeutet positiv Laden und negativ Entladen. Die Messfaktoren müssen die Gerätewerte auf diese Konvention und Watt normieren.
|
||||
|
||||
## Wie funktionieren Prioritäten?
|
||||
|
||||
Kleinere Zahlen haben Vorrang. PV- und Peak-Priorität werden pro Verbraucher eingestellt. Mindestleistungen und technische Sperren bleiben verbindlich. Der aktuelle schrittweise Verteilalgorithmus ist in der [Manager-Referenz](../module/Manager/README.md) beschrieben.
|
||||
|
||||
## Schaltet der Manager-Aus-Schalter die gesamte Anlage sicher ab?
|
||||
|
||||
Nein. Er ersetzt keine elektrische Freischaltung oder unabhängige Schutztechnik. Verbraucher können lokale Betriebs- und Schutzfunktionen besitzen. Für Arbeiten an der Anlage gilt das dafür vorgesehene Sicherheitsverfahren.
|
||||
|
||||
## Kann ich einen Lizenzcode für mehrere Installationen verwenden?
|
||||
|
||||
Die Aktivierung bindet den Code an eine Installations-ID. Eine andere Installation wird abgewiesen. Sichere bei einer Migration die vollständige Manager-Instanz einschließlich ihrer Attribute; kläre einen erforderlichen Gerätewechsel über das Portal beziehungsweise den Betreiber.
|
||||
|
||||
## Was passiert ohne Internetverbindung?
|
||||
|
||||
Eine bereits bestätigte Manager-Freigabe kann im dokumentierten Entwicklungsstand bis `offlineUntil` genutzt werden, ungefähr 14 Tage nach Ausstellung. Danach wird die Regelung bis zu einer gültigen Serverantwort gesperrt. Verlasse dich auf den tatsächlich angezeigten Lizenzstatus; Cloud-Geräte und Prognosen haben zusätzliche Netzwerkabhängigkeiten.
|
||||
|
||||
## Was unterscheidet Lizenz und Ersteinrichtung?
|
||||
|
||||
Die direkte Lizenzbestellung enthält die gewählten Lizenzen. Beim Systemkonfigurator kommen die noch nicht bezahlten Einrichtungskosten hinzu. Die [Preisliste](preise.md) zeigt beide Positionen getrennt. Prüfe vor Abschluss die vollständige Bestellung.
|
||||
|
||||
## Sind Prognoselizenzen automatisch verlängerte Abonnements?
|
||||
|
||||
Der aktuelle Portalstand verwendet datierte Jahresberechtigungen und manuelle Verlängerung, keine automatische Stripe-Abonnementverlängerung. Die Preisseite zeigt die im Katalog hinterlegte Laufzeit. Der intelligente Netzfahrplan benötigt den Manager mit Peak Shaving.
|
||||
|
||||
## Sind die Preise aktuell und inklusive MwSt.?
|
||||
|
||||
Die [Preisliste](preise.md) liest den aktuellen Backend-Katalog. Sie zeigt Netto- und berechnete Bruttopreise einschließlich des aktuell gültigen, im Backend konfigurierten MwSt.-Satzes. Bei einem Abruffehler werden keine Ersatzpreise angezeigt.
|
||||
|
||||
## Aktualisiert ein Git-Update auch meine Anlage?
|
||||
|
||||
Nein. Dokumentationsabgleich und Modulupdate sind getrennt. IP-Symcon-Module werden über die Modulverwaltung aktualisiert, mit Sicherung und Nachkontrolle.
|
||||
|
||||
## Warum zeigt das Energiediagramm keine Werte?
|
||||
|
||||
Prüfe die drei Pflichtzähler, deren Archivierung, die Einheit beziehungsweise den Faktor und den gewählten Zeitraum. Das Modul benötigt Energiezähler, nicht nur momentane Leistungsmessungen.
|
||||
|
||||
## Welche Daten sollte ich bei einem Fehler bereithalten?
|
||||
|
||||
Notiere Modulname, Version beziehungsweise Kanal, Instanzstatus, genaue Fehlermeldung, Zeitpunkt und die letzten Änderungen. Ergänze relevante Messwerte mit Einheiten. Entferne Lizenzcodes, Kontodaten, Passwörter und Tokens aus Screenshots und Protokollen.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,63 @@
|
||||
# Enelix verstehen
|
||||
|
||||
**Enelix ist ein Energiemanagementsystem, kurz EMS.** Es hilft dabei, den Strom einer Anlage gezielt zu nutzen: zum Beispiel Solarstrom für Warmwasser oder das Elektroauto einzusetzen und den Bezug aus dem Stromnetz zu begrenzen.
|
||||
|
||||
Enelix läuft in **IP-Symcon** und besteht aus zwei Bibliotheken: **Enelix EMS** enthält Manager und Verbrauchermodule; **Enelix Utils** ergänzt Geräteanbindungen und Auswertungen. Du richtest einen **Manager** ein und wählst aus, welche **Verbraucher** er steuern darf. Über IP-Symcon erhält er die Messwerte und Geräteverbindungen. Die laufende Regelung übernimmt anschließend das System innerhalb der eingestellten Grenzen.
|
||||
|
||||
Du musst dafür nicht programmieren. Die technische Einrichtung und die Prüfung der Geräteansteuerung gehören jedoch in fachkundige Hände. Diese Einführung erklärt dir erst das Prinzip und danach den Weg zur eigenen Anlage.
|
||||
|
||||
## Ein Beispiel aus dem Alltag
|
||||
|
||||
Mittags erzeugt deine Solaranlage mehr Strom, als das Haus gerade braucht. Statt den gesamten Überschuss einzuspeisen, kann Enelix ihn einem eingerichteten Verbraucher anbieten, etwa dem Warmwassererwärmer oder einer Ladestation.
|
||||
|
||||
**Vereinfachtes Beispiel:** Die Solaranlage liefert 6 kW, im Haus werden 2 kW gebraucht. Damit bleiben zunächst 4 kW Überschuss. Der Manager berücksichtigt, welche Geräte gerade Energie benötigen, welche Leistung sie annehmen können und welche Priorität eingestellt ist. Er verteilt daraus passende Vorgaben. Zieht eine Wolke auf oder steigt der Hausverbrauch, passt er die Verteilung an.
|
||||
|
||||
Nicht jedes Gerät kann beliebig wenig Leistung aufnehmen oder sofort abschalten. Deshalb berücksichtigt die Einrichtung auch Leistungsstufen, Mindestlaufzeiten, Temperaturen und weitere Gerätegrenzen. Die Zahlen im Beispiel erklären nur das Prinzip und sind keine Einstellvorgaben.
|
||||
|
||||
## So setzt sich Enelix zusammen
|
||||
|
||||
| Baustein | Einfach erklärt |
|
||||
| --- | --- |
|
||||
| **IP-Symcon** | Die Softwarebasis deiner Anlage. Hier liegen Messwerte, Geräteanbindungen und die Enelix-Instanzen. |
|
||||
| **Enelix EMS** | Die Bibliothek für das Energiemanagement. Sie enthält den Manager und die passenden Module für Verbraucher und Batterien. |
|
||||
| **Manager** | Die zentrale Regelung. Er beobachtet die Netzleistung und verteilt Vorgaben an die Geräte, die du ihm zugeordnet hast. |
|
||||
| **Verbraucher** | Ein angebundenes Gerät, etwa Warmwassererwärmer, Wärmepumpe oder Ladestation. Sein Modul kennt die gerätespezifischen Grenzen und setzt die Vorgaben um. |
|
||||
| **Enelix Utils** | Eine zweite Bibliothek mit Zusatzfunktionen, zum Beispiel Energiediagramm, Shelly-Anbindung und Verbrauchskostenreport. Sie kann auch unabhängig vom EMS genutzt werden. |
|
||||
| **Lizenzportal** | Hier verwaltest du die benötigten Lizenzen. Die Regelung deiner Geräte läuft in IP-Symcon, nicht auf dieser Dokumentationsseite. |
|
||||
|
||||
Die beiden Bibliotheken sind also zwei zusammenpassende Werkzeugkästen, keine zwei getrennten EMS. Für die Regelung beginnst du mit **Enelix EMS**. **Enelix Utils** kommt dazu, wenn du dessen Zusatzmodule oder die entsprechenden Darstellungen nutzen möchtest.
|
||||
|
||||

|
||||
|
||||
## Was macht der Manager genau?
|
||||
|
||||
1. **Messen:** Er liest, ob deine Anlage gerade Strom aus dem Netz bezieht oder einspeist.
|
||||
2. **Bedarf berücksichtigen:** Die ausgewählten Verbraucher melden ihren Zustand und ihre möglichen Leistungen.
|
||||
3. **Verteilen:** Der Manager berücksichtigt Betriebsart, Prioritäten und eingestellte Grenzen.
|
||||
4. **Umsetzen:** Das jeweilige Verbrauchermodul übersetzt die Vorgabe in die passende Geräteansteuerung.
|
||||
|
||||
Beim **Solarladen beziehungsweise PV-Betrieb** steht die Nutzung des Solarüberschusses im Vordergrund. **Peak Shaving** bedeutet, hohe Bezugsspitzen gegenüber einer eingestellten Grenze zu begrenzen. Diese Funktion benötigt die entsprechende Lizenz und eine passende Einrichtung. Ein Batteriemodul kann zusätzlich Laden und Entladen in die Regelung einbinden.
|
||||
|
||||
Enelix erkennt und steuert nicht automatisch jedes Gerät im Gebäude. Messquellen, Geräteverbindungen und die Zuordnung zum Manager müssen bewusst eingerichtet und geprüft werden.
|
||||
|
||||
## Welche Begriffe brauche ich zum Start?
|
||||
|
||||
- Eine **Bibliothek** ist ein installierbares Paket, hier Enelix EMS oder Enelix Utils.
|
||||
- Ein **Modul** ist die Vorlage für eine Funktion, zum Beispiel „Manager“ oder „Wassererwärmer“.
|
||||
- Eine **Instanz** ist die konkret eingerichtete Verwendung eines Moduls in deiner Anlage. Zwei Ladestationen erhalten zum Beispiel je eine eigene Instanz.
|
||||
- Eine **Variable** enthält einen Wert oder einen bedienbaren Zustand in IP-Symcon, etwa Netzleistung, Temperatur oder eine Freigabe.
|
||||
- Die **Visualisierung** ist die Oberfläche, auf der du deine Anlage im Alltag ansehen und bedienen kannst. Die technische Einrichtung erfolgt in der Verwaltungskonsole.
|
||||
|
||||
## Wie geht es weiter?
|
||||
|
||||
**Du möchtest die erste Anlage einrichten?** Folge den [Ersten Schritten](erste-schritte.md). Sie führen von den Voraussetzungen bis zur kontrollierten Inbetriebnahme und verlinken jeweils die passende Anleitung.
|
||||
|
||||
**Deine Anlage ist bereits eingerichtet?** Unter [Manager bedienen](manager.md#im-alltag) findest du die wichtigsten Zustände und die ersten Prüfungen bei Problemen. Die [häufigen Fragen](faq.md) helfen bei typischen Unklarheiten.
|
||||
|
||||
**Du suchst einzelne Einstellungen?** Die [Verbraucheranleitung](verbraucher.md), die [Utils-Anleitung](utils.md) und die Modulreferenzen in der Navigation gehen ins Detail. Die [Preisseite](preise.md) zeigt die aktuellen Preise aus dem Lizenzportal.
|
||||
|
||||
## Stand und Sicherheit
|
||||
|
||||
Enelix befindet sich im kontrollierten Testbetrieb. Die Software ersetzt keine elektrischen Schutzfunktionen und keine fachkundige Inbetriebnahme. Ein ausgeschalteter Manager ist kein elektrischer Not-Aus.
|
||||
|
||||
Diese Dokumentation folgt dem unten angegebenen Git-Kanal. Prüfe, ob deine installierte Version dazu passt. Ein Update der Dokumentation aktualisiert deine laufende IP-Symcon-Anlage nicht.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Enelix installieren
|
||||
|
||||
## 1. Voraussetzungen prüfen
|
||||
|
||||
- IP-Symcon ab Version 8.0 und Zugriff auf die Verwaltung.
|
||||
- Netzwerkzugriff von IP-Symcon auf die Enelix-Git-Repositories; erforderliche Repository-Berechtigungen müssen vorhanden sein.
|
||||
- Für lizenzierte Funktionen: ein Portal-Konto und eine passende Lizenz. Das Symcon-System muss den Lizenzserver per HTTPS erreichen können.
|
||||
- Funktionsfähige Geräteanbindungen mit bekannten Mess- und Stellvariablen. IP-Adressen, Zugangsdaten und elektrische Grenzen stammen aus deiner Anlage.
|
||||
|
||||
Sichere vor Änderungen die IP-Symcon-Konfiguration einschließlich Instanzen und Attributen. Halte fest, welche bestehende Regelung aktiv ist und wie du sie kontrolliert wiederherstellen kannst. Zwei Regler dürfen nicht unkoordiniert dieselben Ausgänge steuern.
|
||||
|
||||
## 2. Bibliotheken hinzufügen
|
||||
|
||||
Öffne in der IP-Symcon-Verwaltung den Bereich **Module** und füge die benötigten Repository-URLs hinzu:
|
||||
|
||||
| Bibliothek | Repository |
|
||||
| --- | --- |
|
||||
| Enelix EMS | `https://git.belevo.ch/ENELIX/Enelix-EMS.git` |
|
||||
| Enelix Utils | `https://git.belevo.ch/ENELIX/Enelix-Utils.git` |
|
||||
|
||||
Wähle einen für deine Umgebung freigegebenen Kanal. Installiere Utils, wenn du dessen Zusatzmodule oder die entsprechenden Manager-Visualisierungen nutzen möchtest.
|
||||
|
||||
| Branch | Kanal | Einordnung |
|
||||
| --- | --- | --- |
|
||||
| `main` | Stable | stabiler Freigabekanal |
|
||||
| `beta` | Beta | freigegebene Feldtests |
|
||||
| `develop` | Testing | Entwicklung und Integrationstests |
|
||||
|
||||
Die Kanalbezeichnung allein ist keine Anlagenfreigabe. Beachte zusätzlich den Status der jeweiligen Version. Die öffentliche Dokumentation kann einen neueren Stand zeigen als deine Installation.
|
||||
|
||||
Zugangsdaten gehören in die vorgesehene Laufzeitkonfiguration, nicht in Repository-URLs, Skripte oder Screenshots. Kopiere die Modulordner nicht manuell: Sonst kann die Modulverwaltung URL, Branch und Aktualisierungen nicht zuverlässig verwalten.
|
||||
|
||||
## 3. Installation kontrollieren
|
||||
|
||||
Prüfe, dass die Bibliothek ohne Warnsymbol angezeigt wird und URL sowie Branch sichtbar sind. Lege zunächst nur die benötigten Instanzen an. Für das EMS beginnst du mit dem [Manager](manager.md), danach folgen die [Verbraucher](verbraucher.md).
|
||||
|
||||
Lass Manager und Verbraucher während der Grundeinrichtung deaktiviert. Prüfe zuerst Messwerte, Einheiten, Vorzeichen und die Zuordnung der Stellvariablen. Eine gültige Konfiguration ersetzt keinen physischen Funktionstest.
|
||||
|
||||
## 4. Lizenz und Einrichtung
|
||||
|
||||
Im [Lizenzportal](/) kannst du ein Konto anlegen und die erforderlichen Lizenzen wählen. Die [Preisliste](preise.md) unterscheidet Lizenzkosten und Ersteinrichtung. Eine direkte Lizenzbestellung und eine Bestellung über den Systemkonfigurator sind unterschiedliche Abläufe.
|
||||
|
||||
Bei Verwendung des Systemkonfigurators prüfst du die erzeugte Konfiguration vor der Ausführung in IP-Symcon. Führe ein Einrichtungspaket nicht ungeprüft auf einer bestehenden Anlage aus. Bewahre Sicherung und Rücksetzweg auf.
|
||||
|
||||
## 5. Aktualisieren
|
||||
|
||||
1. Änderungsumfang und passenden Kanal prüfen.
|
||||
2. Konfiguration, Instanzen und Attribute sichern; bei Regelungsänderungen einen sicheren Anlagenzustand herstellen.
|
||||
3. Updates über die IP-Symcon-Modulverwaltung beziehen.
|
||||
4. Instanzstatus, Lizenzbindung, Messquellen und Verbraucherzuordnung kontrollieren.
|
||||
5. Änderungen an Ausgängen unter Aufsicht prüfen, bevor der normale Betrieb wieder freigegeben wird.
|
||||
|
||||
Eine neue Manager-Instanz erhält eine neue Installations-ID. Ein Update einer bestehenden Instanz ist deshalb nicht gleichbedeutend mit Löschen und Neuanlegen.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Manager einrichten und bedienen
|
||||
|
||||
Der Manager liest die Netzleistung und verteilt Leistung im PV- oder Peak-Betrieb. Er steuert ausschließlich die ausgewählten Verbraucher.
|
||||
|
||||
## 1. Instanz und Lizenz vorbereiten
|
||||
|
||||
Lege eine Manager-Instanz aus Enelix EMS an. Die Variable `Aktiv` bleibt zunächst ausgeschaltet. Öffne die Konfiguration, trage den Aktivierungscode aus dem Lizenzportal im Bereich **Lizenzierung** ein und wähle **Lizenz prüfen und binden**. Kontrolliere den Lizenzstatus und speichere die Konfiguration anschließend mit **Übernehmen** oder **OK**.
|
||||
|
||||
Ohne gültige Manager-Berechtigung bleibt die Instanz mit Status `203` gesperrt. Ein Code ist an die Installations-ID gebunden; verwende ihn nicht zum Einrichten einer zweiten Anlage.
|
||||
|
||||
## 2. Netzleistung einstellen
|
||||
|
||||
Wähle die Messvariable am Netzanschlusspunkt über `NetzleistungVariableID`. Die Normierung erfolgt mit `Netzleistungsfaktor`:
|
||||
|
||||
- positive Leistung bedeutet Netzbezug;
|
||||
- negative Leistung bedeutet Einspeisung;
|
||||
- die normierte Einheit ist Watt, nicht Kilowatt.
|
||||
|
||||
Prüfe das Vorzeichen bei einem bekannten Betriebszustand. `MesswertMaxAlter` begrenzt das zulässige Alter der Quelle. Eine unveränderte Messung kann weiterhin aktuell sein; entscheidend ist die echte Aktualisierung der Quelle. Fehlende oder veraltete Netzleistung verhindert neue Regelvorgaben.
|
||||
|
||||
## 3. Betriebsgrenzen festlegen
|
||||
|
||||
`SollwertSolarladen` legt die gewünschte Netzleistung im PV-Betrieb fest. Beginne mit der für deine Anlage freigegebenen Einstellung.
|
||||
|
||||
Für Peak Shaving benötigt der Manager die entsprechende Lizenz. `Lastspitzenmodus` kennt **Aus**, **Konstant** und **Monatlich**. Bei konstantem Modus wird `Lastspitzengrenze` in Watt verwendet; im monatlichen Modus werden zwölf Monatswerte gepflegt. **Aus** deaktiviert nur die Peak-Begrenzung, nicht das Solarladen.
|
||||
|
||||
Eine Einspeisebegrenzung benötigt geeignete Mess- und Stellregister der Wechselrichter. Übernimm keine beispielhaften Grenzen als Anlagenwerte. Die vollständigen Einstellungen stehen in der [Manager-Referenz](../module/Manager/README.md).
|
||||
|
||||
## 4. Verbraucher auswählen
|
||||
|
||||
Nutze die automatische Suche oder die manuelle Zuordnung. Gefundene Instanzen müssen gezielt ausgewählt und für die Zuordnung aktiviert werden. Die Verbindung wird im Manager gepflegt; im Verbraucher gibt es keine Manager-ID-Property.
|
||||
|
||||
Prioritäten stellst du am jeweiligen Verbraucher ein. Kleinere Zahlen bedeuten höhere Priorität. Die aktuelle Verteilregel und ihre Beispiele stehen in der [Manager-Referenz](../module/Manager/README.md).
|
||||
|
||||

|
||||
|
||||
## 5. Anlage und Visualisierung ergänzen
|
||||
|
||||
Unter Anlagentopologie werden Wechselrichter, PV-Flächen und Batterien mit ihren Messquellen erfasst. Verwende eindeutige Kennungen; PV-Flächen und Batterien müssen auf einen vorhandenen Wechselrichter verweisen.
|
||||
|
||||
Für die Energieaufzeichnung werden Netz- und PV-Leistung benötigt. Vollständige Energiezähler verbessern die Bilanzierung; ohne vollständigen Zählersatz integriert der Manager Leistungswerte. Energiezähler und Leistungswerte sind nicht austauschbar.
|
||||
|
||||
Die Optionen für Energy Pie, Diagramme, Energy Facts und Energiefluss verwalten die zugehörigen Visualisierungen. Prüfe bestehende angepasste Darstellungen vor Änderungen. Prognosefunktionen sind optional und benötigen passende Berechtigungen sowie eine gültige Topologie.
|
||||
|
||||
## 6. Kontrolliert in Betrieb nehmen
|
||||
|
||||
Prüfe Lizenzstatus, Netzleistung, Verbraucherstatus und Störtext bei ausgeschalteter Regelung. Nimm zunächst einen Verbraucher unter Aufsicht in Betrieb und vergleiche Sollwert mit physischem Verhalten. Prüfe außerdem Abschaltung und Verhalten bei fehlenden Messwerten, bevor weitere Geräte hinzukommen.
|
||||
|
||||
## Im Alltag
|
||||
|
||||
`Aktiv` schaltet die Manager-Regelung ein oder aus. `Betriebsart` zeigt **Inaktiv**, **PV** oder **Peak**. Verbraucher können eigene Mindestzeiten, Temperaturanforderungen und lokale Schutzfunktionen haben. Ein deaktivierter Manager ist deshalb kein universeller elektrischer Not-Aus. Verwende für Arbeiten an der Anlage die vorgesehenen technischen Sicherheitsmaßnahmen.
|
||||
|
||||
Bei Problemen kontrollierst du zuerst `Lizenzstatus`, `NetzleistungGueltig`, `Sammelstoerung`, `Stoertext` und anschließend die betroffene Verbraucherinstanz. Diagnosevariablen und Debug-Logging können gezielt eingeblendet werden; teile keine Lizenzcodes oder Zugangsdaten.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Preise
|
||||
|
||||
Alle im öffentlichen Backend-Katalog geführten Produkte, mit Lizenzpreis, Laufzeit und Ersteinrichtung. Die Beträge werden direkt aus dem Lizenzportal geladen.
|
||||
|
||||
## So liest du die Preisliste
|
||||
|
||||
Die Lizenzpreise gelten pro aufgeführter Einheit. Einrichtungskosten sind einmalige separate Positionen und werden nicht automatisch zum Lizenzpreis addiert. Bei der direkten Lizenzbestellung wird keine Ersteinrichtung berechnet; der Systemkonfigurator berücksichtigt die noch nicht bezahlte Einrichtung.
|
||||
|
||||
Jahresberechtigungen werden mit ihrer Laufzeit ausgewiesen. Für den intelligenten Netzfahrplan ist der Manager mit Peak Shaving erforderlich. Der Verbrauchskostenreport benötigt eine Grundlizenz und die passenden Zählerkontingente.
|
||||
|
||||
Die Bruttowerte sind aus den Einzelpreisen und dem aktuellen Backend-MwSt.-Satz berechnet. Der Checkout berechnet die MwSt. auf den Bestellgesamtbetrag; bei mehreren Positionen können Rundungsdifferenzen gegenüber der Summe einzelner Bruttowerte entstehen. Maßgeblich ist die Bestellung vor dem Abschluss.
|
||||
|
||||
## Weitere Kosten und Verfügbarkeit
|
||||
|
||||
Die Liste umfasst den Enelix-Produktkatalog, keine externen Kosten für IP-Symcon, Hardware, Installationsarbeiten oder Drittanbieter. Die Anzeige eines Katalogprodukts bestätigt keine technische Freigabe für jede Anlage. Prüfe Gerätekompatibilität und den angebotenen Bestellumfang im Portal.
|
||||
|
||||
Das Portal ist derzeit als Entwicklungsumgebung gekennzeichnet; Zahlungen erfolgen im Stripe-Sandbox-Modus. Eine produktive Zahlungsfreigabe wird durch diese Preisliste nicht erteilt.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Enelix Utils nutzen
|
||||
|
||||
Die Utils-Bibliothek enthält eigenständige Zusatzmodule. Installiere sie über die IP-Symcon-Modulverwaltung und lege nur die benötigten Instanzen an. Ein EMS-Manager ist für die Bibliothek nicht erforderlich; einzelne Module besitzen eigene Voraussetzungen und gegebenenfalls eigene Lizenzanforderungen.
|
||||
|
||||
## Energiediagramm
|
||||
|
||||
Wähle die archivierten, fortlaufenden Zähler für Produktion, Einspeisung und Netzbezug. Eine direkte Verbrauchsquelle ist optional. Kontrolliere `Zaehlerfaktor` und die Einheit kWh. Leistungsvariablen in Watt sind keine Energiezähler.
|
||||
|
||||
Wähle einen Zeitraum mit vorhandenen Archivdaten und prüfe Energiebilanz, Eigenverbrauch und Autarkie. Fehlende Archivdaten werden als Diagnose angezeigt. [Energiediagramm-Referenz](../../../Enelix-Utils/docs/module/Energiediagramm/README.md).
|
||||
|
||||
## Shelly Modul
|
||||
|
||||
Richte zuerst den nativen MQTT-Datenfluss in IP-Symcon und MQTT am Shelly-Gerät ein. Das Modul verarbeitet Shelly-NG-Meldungen der Generationen 2, 3 und 4; alte Gen1-Topics sind nicht Teil dieser Anbindung.
|
||||
|
||||
Wähle die gewünschten Datenpunktgruppen. Aktiviere einen Topic-Filter nur, wenn er benötigt wird, und trage dann den tatsächlichen Präfix ein. Prüfe Online-Status und Messwerte, bevor Schaltausgänge getestet werden. Broker- und Gerätezugangsdaten werden nicht im Modul verwaltet. [Shelly-Referenz](../../../Enelix-Utils/docs/module/Shelly-Modul/README.md).
|
||||
|
||||
## Verbrauchskostenreport
|
||||
|
||||
Dieses Modul benötigt seine Grundlizenz sowie passende Kontingente für Strom- und Nebenzähler. Ordne Benutzer und Zähler zu, pflege Tarifzeiträume und Einheiten und prüfe den gewählten Abrechnungszeitraum.
|
||||
|
||||
Die Aktion `CreateReport` erstellt das PDF-Medium `ReportPDF`. Für einen aktivierten QR-Zahlteil sind vollständige strukturierte Empfängerangaben erforderlich. Steuer- und Abrechnungseinstellungen müssen fachlich geprüft werden; ein Code-Standardwert ist keine steuerliche Festlegung. [Verbrauchskostenreport-Referenz](../../../Enelix-Utils/docs/module/Verbrauchskostenreport/README.md).
|
||||
|
||||
## CC100 Hardware
|
||||
|
||||
Das Modul bindet die dokumentierten Ein- und Ausgänge der CC100-Hardware ein. Prüfe Kanalzuordnung, Signalanpassung und physische Verdrahtung anhand der [CC100-Referenz](../../../Enelix-Utils/docs/module/CC100-Hardware/README.md). Teste Ausgänge erst nach Prüfung der angeschlossenen Verbraucher und des sicheren Anlagenzustands.
|
||||
|
||||
## Virtuelle Batterie
|
||||
|
||||
Trage die physischen Batterien mit Kapazitäten, Leistungsgrenzen, Ladezustands- und Leistungsquellen ein. Die virtuelle Batterie ist die einzige Instanz, die deren physische Sollwerte schreibt. Ein EMS-Batteriemodul nutzt die Eigenverbrauchs-Proxyregister; eine VGT-Anbindung nutzt die SDL-Variablen.
|
||||
|
||||
Prüfe Reserven, Zeitüberschreitungen und Messwertalter. Teste Eigenverbrauch und SDL zuerst getrennt, anschließend gemeinsam. SDL hat bei der Verteilung Vorrang. [Referenz zur virtuellen Batterie](../../../Enelix-Utils/docs/module/Virtuelle-Batterie/README.md).
|
||||
|
||||
## VGT-Schnittstelle
|
||||
|
||||
Ordne den MQTT-Parent zu und übernimm den vereinbarten Topic-Suffix. Wähle Geräteart, Messquellen und bedienbare Zielvariable. Im Batteriemodus wird an die virtuelle Batterie angebunden, nicht direkt an physische Register.
|
||||
|
||||
Prüfe zuerst die Rückmeldungen. Steueraufträge und Wiederherstellungsfunktionen dürfen erst im freigegebenen Testumfang erprobt werden. [VGT-Referenz](../../../Enelix-Utils/docs/module/VGT-Schnittstelle/README.md).
|
||||
@@ -0,0 +1,55 @@
|
||||
# Verbraucher einrichten
|
||||
|
||||
Ein Verbraucher übersetzt ein Leistungsangebot und die Manager-Vorgabe in die konkrete Geräteansteuerung. Die Einrichtung erfolgt zuerst am Gerät und anschließend im Manager.
|
||||
|
||||
## Gemeinsamer Ablauf
|
||||
|
||||
1. Passendes Modul als Instanz anlegen und `Aktiv` zunächst ausgeschaltet lassen.
|
||||
2. Messquellen, Geräteanschluss und bedienbare Stellvariablen eintragen. Ein angezeigter Variablenwert allein beweist keine erfolgreiche Geräteansteuerung.
|
||||
3. Leistungsgrenzen, Mindestzeiten und Schutzwerte aus den tatsächlichen Gerätedaten übernehmen.
|
||||
4. `PrioritaetPV` und `PrioritaetPeak` festlegen. Kleinere Werte bedeuten höhere Priorität.
|
||||
5. Konfiguration speichern und Instanzstatus sowie Messwerte prüfen.
|
||||
6. Den Verbraucher im Manager auswählen. Keine Manager-ID im Verbraucher eintragen.
|
||||
7. Unter Aufsicht Freigabe, kleine Sollwerte, Rückmeldung und Abschaltung prüfen.
|
||||
|
||||
`Meldeintervall` bestimmt die Rückmeldungen. `VorgabeTimeout` begrenzt die Gültigkeit einer nicht erneuerten Vorgabe. Mindestlaufzeiten und Schaltsperren können schnelle Änderungen verhindern; ändere sie nicht nur, um eine Reaktion zu erzwingen.
|
||||
|
||||
## Einstufiger Verbraucher
|
||||
|
||||
Trage die elektrische `Nennleistung` und eine Boolean-Schaltvariable mit funktionierender Aktion unter `SchaltkontaktVariableID` ein. Eine optionale `RueckmeldungVariableID` bestätigt den physischen Schaltzustand. Mindest-Ein-/Aus-Zeiten und Tagesmindestlaufzeit müssen zum Gerät passen.
|
||||
|
||||
Bei fälliger Tagesmindestlaufzeit kann das Gerät auch ohne PV-Überschuss Leistung anfordern. Prüfe das gewünschte Peak-Verhalten ausdrücklich. [Vollständige Referenz](../module/Verbraucher-1-Stufig/README.md).
|
||||
|
||||
## Warmwassererwärmer
|
||||
|
||||
Wähle den Temperaturfühler und konfiguriere jede positive Leistungsstufe mit eigener Boolean-Schaltvariable. Der Zustand `0 W` wird automatisch ergänzt. Stufen müssen elektrisch zulässig und eindeutig sein. Prüfe Mindesttemperatur, Hysterese, Zeitplan und Legionellenfunktion fachkundig; die Software ersetzt keine unabhängigen Temperatur- und Überhitzungsschutzfunktionen. [Vollständige Referenz](../module/Wassererwaermer/README.md).
|
||||
|
||||
## Pufferspeicher
|
||||
|
||||
Hinterlege Puffertemperatur, Außentemperatur, Heizkurve und Schaltkontakte der Leistungsstufen. Eine optionale Solltemperaturquelle der Wärmepumpe kann eingebunden werden. Kontrolliere die resultierende Einschaltschwelle und Hysterese. Im dokumentierten Stand bietet der Pufferspeicher im Peak-Betrieb nur `0 W` an. [Vollständige Referenz](../module/Pufferspeicher/README.md).
|
||||
|
||||
## Wärmepumpe
|
||||
|
||||
Wähle Sperre/Erhöhung oder SG Ready, ordne die beiden Kontakte zu und kontrolliere ihre tatsächliche Logik. Hinterlege Nennleistung und die verpflichtende Leistungs- oder Boolean-Betriebsrückmeldung. Prüfe Herstellerfreigabe, Kontaktbelegung und Mindestzeiten. [Vollständige Referenz](../module/Waermepumpe/README.md).
|
||||
|
||||
## Ladestation direkt anbinden
|
||||
|
||||
Für unterstützte go-e-Varianten und smart-me Pico wird **Ladestation Stand-Alone** verwendet. Wähle den richtigen Typ. Bei go-e gehören IP-Adresse oder Hostname ohne Protokoll, Pfad oder Parameter in die Geräteadresse. Pico benötigt seine vorgesehenen Geräte- und Kontodaten.
|
||||
|
||||
Prüfe Stromgrenzen, Fahrzeug-/Phasenerkennung und Sperrzeiten. `Aktiv`, `Ladefreigabe` und `Solarladen` haben unterschiedliche Aufgaben. Kontrolliere sie zusammen mit dem Fahrzeugstatus, wenn keine Ladeleistung angeboten wird. [Vollständige Referenz](../module/Ladestation-Stand-Alone/README.md).
|
||||
|
||||
## Easee über Gateway anbinden
|
||||
|
||||
Richte zuerst ein **Easee Gateway** für das Konto ein. Lege je Ladestation eine Instanz **Ladestation Gateway** mit passender Seriennummer an und verbinde sie mit dem Gateway. Zugangsdaten bleiben im Gateway. Prüfe Verbindung, Fahrzeugstatus und Phasenmeldung, bevor du die Station im Manager aktivierst.
|
||||
|
||||
[Gateway-Referenz](../module/Easee-Gateway/README.md) und [Ladestations-Referenz](../module/Ladestation-Gateway/README.md).
|
||||
|
||||
## Batterie
|
||||
|
||||
Wähle den passenden Batterieadapter. Hinterlege dynamische Lade-/Entladegrenzen, Ladezustand, Netzleistung, Istleistung und die erforderlichen bedienbaren Register. Positive Batterieleistung bedeutet Laden, negative Entladen. Prüfe besonders Watt gegenüber Kilowatt und die Herstellerabbildung.
|
||||
|
||||
Setze Ladezustandsgrenzen und Reserve gemäß Anlagenfreigabe. Bei einer virtuellen Batterie dürfen physische Register nicht gleichzeitig von mehreren Reglern beschrieben werden. Prüfe zunächst ohne aktive Enelix-Regelung alle Vorzeichen und Messwerte. [Vollständige Referenz](../module/Batterie/README.md).
|
||||
|
||||
## Diagnose
|
||||
|
||||
Prüfe nacheinander lokale Freigabe, Managerzuordnung, Lizenzumfang, Geräteverbindung, aktuelle Messwerte und Störtext. Ein Verbraucher kann verfügbar sein und trotzdem wegen einer Mindestzeit nur seine aktuelle Leistung anbieten. Details zu `Verfuegbar`, `AenderungMoeglich` und `SollwertGueltig` stehen in der [gemeinsamen Schnittstelle](../Schnittstelle.md).
|
||||
@@ -0,0 +1,66 @@
|
||||
# V4: separate Wirkenergie-Bezugsquelle (Entwicklung, nicht live)
|
||||
|
||||
## Lihrenmoos - Befund und geplante Zuordnung
|
||||
|
||||
User hat die erfolgreiche Aktivierung der T2-Zaehlerarchivierung fuer 26620
|
||||
im Archiv 16207 bestaetigt. Der exakte Beginn einer lueckenlosen Messhistorie
|
||||
ist daraus NICHT nachgewiesen; keine historische Null setzen.
|
||||
|
||||
Die vorbereitete separate V4-Konfiguration fuer diese Anlage lautet:
|
||||
|
||||
```json
|
||||
[
|
||||
{"VariableID":59607,"ElternID":11490,"Ident":"Energy_0","FaktorZuKWh":1.0,"Messgroesse":"WirkenergieBezug"},
|
||||
{"VariableID":26620,"ElternID":11490,"Ident":"Energy_1","FaktorZuKWh":1.0,"Messgroesse":"WirkenergieBezug"}
|
||||
]
|
||||
```
|
||||
|
||||
Property: NetzfahrplanV4BezugszaehlerQuellen. Diese Datei setzt KEINE Property.
|
||||
IDs duerfen nicht fuer andere Anlagen kopiert werden. Zuordnung ist durch
|
||||
Variablenstruktur, Standardtelegramm und Leistungs-/Energiedifferenz stark
|
||||
plausibilisiert; Abgleich mit Rohtelegramm/Zaehleranzeige steht noch aus.
|
||||
|
||||
53476, dessen gespeicherte Historie, die alte Manager-Property
|
||||
NetzbezugEnergieVariableID und alle vorhandenen Spiegelereignisse bleiben
|
||||
unveraendert. V4 liest seine eigenen Quellen, erzeugt keine Spruenge in alten
|
||||
Summenzaehlern und kann keine Blindenergie durch einen Faktor korrigieren.
|
||||
|
||||
## Vertrag
|
||||
|
||||
Jede Quelle hat eine ausdrueckliche Messgroesse und Umrechnung nach kWh.
|
||||
Variable, Elterninstanz und Ident werden bei jedem Lesen geprueft. Ein
|
||||
fehlender, veralteter, ungueltiger oder zeitlich unpassender Teilwert verwirft
|
||||
die Summe. Ein aktueller echter Nullwert ist gueltig. Zwei aufeinanderfolgende
|
||||
Lesevorgaenge reduzieren das Risiko einer gemischten Momentaufnahme; sie sind
|
||||
kein atomarer Zugriff auf das physische Zaehlertelegramm.
|
||||
|
||||
Die Summe wird unter bezugszaehler in der lokalen V4-Diagnose ausgegeben, mit
|
||||
den Beobachtungszeitpunkten jedes Teilregisters. VariableUpdated ist ein
|
||||
Symcon-Aktualisierungszeitpunkt, kein Beweis einer exakten Abrechnungsgrenze.
|
||||
Die Diagnose hat deshalb billingEvidence=false und historyComplete=false.
|
||||
Eine falsche Messgroessen-Deklaration kann ohne Rohtelegramm nicht automatisch
|
||||
erkannt werden; Installationspruefung bleibt erforderlich.
|
||||
|
||||
Ein aus Quell-IDs, Eltern-ID, Idents und Umrechnungsfaktoren gebildeter
|
||||
stabiler SHA256-Schluessel bindet die spaeteren Messnachweise an genau diese
|
||||
Quelle. Die Reihenfolge von T1/T2 aendert den Schluessel nicht. Quell-/Faktor-
|
||||
wechsel dagegen invalidieren alte Nachweise. symcon:53476 wird nicht mehr
|
||||
als V4-Messnachweis akzeptiert. Der Schluessel beweist Zuordnung, nicht den
|
||||
Wahrheitsgehalt eines gelieferten Monatspeaks.
|
||||
|
||||
## Noch offen
|
||||
|
||||
Der Erzeuger fuer exakte Viertelstundenenergie und den vollstaendigen
|
||||
Monatspeak ist weiterhin nicht implementiert. Keine Ableitung eines bereits
|
||||
bezahlten Monatspeaks aus einer Managergrenze oder unvollstaendiger Historie.
|
||||
Auch 15 Minuten neuer Aufzeichnung beweisen nicht den bisherigen Monatspeak.
|
||||
Die vorbereitete native Summe ist noch nicht im laufenden Symcon installiert.
|
||||
V4 bleibt ohne belastbare Nachweise in awaiting_inputs.
|
||||
|
||||
## Nachweis dieses Schritts
|
||||
|
||||
44 synthetische Offline-Szenarien mit PHP CLI 8.4.23 erfolgreich:
|
||||
20 Quellen-/Summenpruefungen, 24 native Betriebsdatenkonvertierungen.
|
||||
Die vom Server exportierten Betriebsdaten-, Trait- und Fixture-Dateien wurden
|
||||
vor dem Test per SHA256 verglichen. Keine Symcon-Laufzeitpruefung und keine
|
||||
neue Zielcontainer-/vollstaendige PHPUnit-Pruefung in diesem Schritt.
|
||||
@@ -0,0 +1,96 @@
|
||||
# V4: native Betriebsdaten, ausschliesslich Schattenbetrieb
|
||||
|
||||
Stand 2026-10-01: Der optionale Sender und die Diagnose sind implementiert.
|
||||
Kein V4-Plan wird damit uebernommen, kein Aktor geschrieben, kein V1-Sollwert
|
||||
geaendert. Der Sender ist nach Installation standardmaessig AUS.
|
||||
|
||||
## Konfiguration im Manager
|
||||
|
||||
- NetzfahrplanV4SchattenAktiv: bool, Standard false. Eigener 60-s-Timer,
|
||||
unabhaengig vom bestehenden Prognose-Timer. Fehler betreffen nur den Sendestatus.
|
||||
- NetzfahrplanV4NetzladenErlaubt: bool, Standard false. Betrifft ausschliesslich
|
||||
die im Schattenplan angenommene Berechtigung, keine reale Netzladefreigabe.
|
||||
- NetzfahrplanV4BatterieOptionen: JSON-Objekt je Topologie-ID. Optional
|
||||
SOCKapazitaet_kWh und MaxSOC_Prozent (sonst 100). Bei unterschiedlicher Nenn-
|
||||
und Nutzkapazitaet ist SOCKapazitaet_kWh zwingend explizit anzugeben.
|
||||
- NetzfahrplanV4BezugszaehlerQuellen: JSON-Liste, Standard []. Eigene, explizite
|
||||
Wirkenergie-Bezugsquellen fuer V4; KEIN Rueckfall auf den Altzaehler.
|
||||
Details und Lihrenmoos-Kandidat: Netzfahrplan-V4-Bezugszaehler.md.
|
||||
- NetzfahrplanV4MessnachweisVariableID: JSON-Stringvariable, Standard 0.
|
||||
Ein vollstaendiger historischer/registerbasierter Messnachweis-Produzent
|
||||
ist NOCH NICHT integriert. Fehlende Nachweise bleiben fehlend.
|
||||
|
||||
Die Quelle muss der gemeinsame Netzanschluss sein. Topologiebatterien werden
|
||||
nur einer eindeutigen aktiven Batterieinstanz mit derselben SOC- UND
|
||||
Leistungsmessquelle zugeordnet. Externe SDL-Batterien werden nicht automatisch
|
||||
hinzugefuegt. SDL-/Grundlastbereinigung bleibt eine separate, noch offene Aufgabe.
|
||||
BMS-Grenzen werden mit Topologieleistungen begrenzt. Normale Betriebsreserve
|
||||
und technisches Minimum werden nicht verwechselt: die hoehere Grenze gilt.
|
||||
|
||||
Der erweiterte Sender uebermittelt Entladesperre und Wiederfreigabe-SOC separat
|
||||
von der maximalen Entladeleistung. Der neue Python-Rechenkern darf die Sperre
|
||||
im Zukunftsplan erst nach vorherigem Laden bis zur Wiederfreigabe aufheben.
|
||||
SOC unterhalb der Betriebsreserve, aber oberhalb des technischen Minimums,
|
||||
bleibt unveraendert erhalten: Es ist Wiederaufladung statt erfundener Energie
|
||||
vorgesehen. Unterhalb des technischen Minimums wird kein normaler Plan erzeugt.
|
||||
|
||||
Zusaetzlich wird meterObservation mit frischen Empfangswerten der Netzleistung
|
||||
und T1/T2-Summe uebermittelt. Dessen laufende Viertelstundenintegration ist eine
|
||||
explizite Planungsschaetzung, KEIN verifizierter Abrechnungsnachweis. Verwendung
|
||||
von Schaetzungen muss im V4-Dienst ausdruecklich aktiviert werden. Historische
|
||||
Peakannahmen werden getrennt von verifizierten Maxima gespeichert.
|
||||
|
||||
Diese Protokollerweiterung benoetigt zuerst den passenden Backendstand. Der
|
||||
bisherige laufende V4-Container und die Testanlage sind noch nicht aktualisiert.
|
||||
Offline-Szenarien und Quelltextpruefungen ersetzen keinen Symcon-Laufzeittest.
|
||||
|
||||
## Diagnose ohne Netzwerk oder Stellbefehl
|
||||
|
||||
Im Symcon-Skript nach Installation des neuen Modulstands:
|
||||
|
||||
```php
|
||||
<?php
|
||||
print_r(json_decode(ENELIX_GetNetzfahrplanV4Diagnose(17004), true));
|
||||
echo ENELIX_GetNetzfahrplanV4Sendestatus(17004);
|
||||
```
|
||||
|
||||
Die Diagnose enthaelt KEINE Tokens oder sonstigen Zugangsdaten. Sie ist auch
|
||||
bei ausgeschaltetem Sender verfuegbar. Die Instanz-ID ist fuer Lihrenmoos aus
|
||||
der bestehenden Konfiguration bekannt, nicht fuer andere Anlagen zu kopieren.
|
||||
Nicht ungeprueft deployen: Host-CLI- und Zielsystemtests getrennt nachweisen.
|
||||
|
||||
## Messnachweis-Vertrag (Beispiel, KEINE realen Messwerte)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"meterId": "symcon-active-import:<SHA256-der-konfigurierten-Quellen>",
|
||||
"measuredAt": "2026-10-01T12:05:00Z",
|
||||
"measuredPeaks": {
|
||||
"2026-10": {"kw": 18.4, "source": "meter_month_register"}
|
||||
},
|
||||
"quarterPast": {
|
||||
"start": "2026-10-01T12:00:00Z",
|
||||
"measuredSeconds": 300,
|
||||
"importKwh": 0.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Zulaessige Herkunft: meter_month_register, verified_month_history oder
|
||||
verified_new_month. Der Nachweis-Produzent muss deren Wahrheit garantieren.
|
||||
Managergrenzen sind NIE ein gemessener Monatspeak. Der Monatswert muss den
|
||||
vollstaendigen bisherigen Abrechnungsmonat abdecken. Fuer quarterPast muessen
|
||||
Zaehlerzeitpunkt, abgedeckte Sekunden und Entscheidungszeitpunkt exakt passen;
|
||||
veraltete Energie wird nicht hochgerechnet oder als aktuelle Messung ausgegeben.
|
||||
Die kumulative kWh-Variable allein erfuellt diesen Vertrag noch nicht.
|
||||
|
||||
Die V4-API meldet bei fehlendem Monatspeak/quarterPast weiterhin awaiting_inputs.
|
||||
Ein HTTP stored bestaetigt nur den Eingang der Telemetrie, nicht einen gueltigen
|
||||
Fahrplan und schon gar nicht dessen Ausfuehrung. Backend und Portal-Bruecke
|
||||
muessen separat aktiviert werden. Keine neue Modellwahl oder Aktorfreigabe hier.
|
||||
|
||||
## Tests
|
||||
|
||||
24 offline Konvertierungsszenarien und 20 Quellen-/Summen-Szenarien sowie PHPUnit-Strukturpruefungen. Werte sind
|
||||
synthetisch; keine reale HTTP-Anbindung oder Symcon-Laufzeit damit behauptet.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Standardisierte Tests
|
||||
|
||||
## Ziel
|
||||
|
||||
Dieses Repository verwendet zwei verbindliche Testebenen:
|
||||
|
||||
1. PHPUnit prüft reine PHP-Logik und Struktur bei jedem Push.
|
||||
2. Symcon-Modultests prüfen reale Instanzen, Variablen, Actions und Zusammenspiel
|
||||
in IP-Symcon 8.x.
|
||||
|
||||
Alle implementierten Module müssen in `tests/Symcon/manifest.php` eingetragen
|
||||
sein und ein eigenes Skript in `tests/Symcon/modules/` besitzen. Der
|
||||
PHPUnit-Test `SymconTestContractTest` erzwingt diese Regel auch für künftig
|
||||
hinzugefügte Module.
|
||||
|
||||
## Testvertrag
|
||||
|
||||
Ein Modultest gibt eine aufrufbare Funktion mit dieser Signatur zurück:
|
||||
|
||||
```php
|
||||
use Belevo\EnelixEMS\SymconTest\TestContext;
|
||||
|
||||
return static function (TestContext $test): void {
|
||||
$test->runCase('Beschreibung', static function (TestContext $test): void {
|
||||
// Instanz aufbauen, Eingang simulieren und Ergebnis prüfen.
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Das Framework erzeugt für jeden Modultest eine eindeutige Kategorie unterhalb
|
||||
der Objektwurzel. Instanzen und Hilfsobjekte werden ausschließlich dort
|
||||
angelegt. Der Runner entfernt den vollständigen Baum in einem `finally`-Pfad.
|
||||
Ein fehlgeschlagener Cleanup macht den Gesamtlauf rot.
|
||||
|
||||
Tests dürfen keine vorhandenen Objekte verändern oder anhand ihres Namens
|
||||
löschen. Globale Variablenprofile müssen nur dann registriert und entfernt
|
||||
werden, wenn sie vor dem Lauf nicht existierten.
|
||||
|
||||
## Modi
|
||||
|
||||
- `all`: alle registrierten Module
|
||||
- `single`: genau die als Auswahl übergebenen Module
|
||||
- `affected`: die durch geänderte Pfade ermittelten Module
|
||||
|
||||
Verfuegbare Module: `Batterie`, `EaseeGateway`, `LadestationGateway`,
|
||||
`LadestationStandAlone`, `Manager`, `Pufferspeicher`, `VerbraucherEinStufig`,
|
||||
`Waermepumpe` und `Warmwassererwaermer`.
|
||||
|
||||
Der Manager-Test enthält Manager ohne Verbraucher, jeden Verbrauchertyp einzeln und alle aktuell implementierten Verbrauchertypen gemeinsam.
|
||||
|
||||
## Manuelle Ausführung in IP-Symcon
|
||||
|
||||
Das Repository muss über die Modulverwaltung installiert und auf dem zu
|
||||
prüfenden Stand sein. In der Schnellausführung:
|
||||
|
||||
```php
|
||||
require_once IPS_GetKernelDir() . 'modules/Enelix-EMS/tests/Symcon/bootstrap.php';
|
||||
|
||||
$result = enelixEmsRunSymconTests('all');
|
||||
echo $result['console'];
|
||||
```
|
||||
|
||||
Ein Einzeltest wird beispielsweise mit
|
||||
`enelixEmsRunSymconTests('single', 'Manager')` gestartet.
|
||||
|
||||
Auf dem Agent-Server kann derselbe Lauf über JSON-RPC ausgeführt werden:
|
||||
|
||||
```bash
|
||||
tests/Symcon/bin/run-symcon-tests.sh all
|
||||
tests/Symcon/bin/run-symcon-tests.sh single Manager
|
||||
```
|
||||
|
||||
Die URL kann ausschließlich zur Laufzeit über `ENELIX_SYMCON_URL` gesetzt
|
||||
werden. Zugangsdaten gehören nicht in Repository, Skripte oder Logs.
|
||||
|
||||
## Berichte
|
||||
|
||||
Jeder Lauf erzeugt:
|
||||
|
||||
- eine kurze Konsolenzusammenfassung,
|
||||
- `build/symcon-tests/report.json` für Diagnose und Archivierung,
|
||||
- `build/symcon-tests/junit.xml` für CI-Auswertung.
|
||||
|
||||
Zusätzlich schreibt der Runner die Zusammenfassung in das IP-Symcon-Log.
|
||||
|
||||
## CI-Regeln
|
||||
|
||||
Bei jedem Push laufen sämtliche PHPUnit-Tests und die PHP-Syntaxprüfung. Die
|
||||
Symcon-Modultests werden bis zur Verfügbarkeit eines geschützten Runners auf
|
||||
der isolierten IP-Symcon-8.0-Instanz des Agent-Servers ausgeführt. Vor jeder
|
||||
Übernahme nach `beta` ist ein erfolgreicher Lauf im Modus `all` verbindlich.
|
||||
Der erzeugte JSON- und JUnit-Bericht gehört zum Freigabenachweis.
|
||||
|
||||
Eine spätere CI-Automatisierung benötigt einen geschützten Runner mit dem Label
|
||||
`symcon-8`, lokalem Zugriff auf die isolierte IP-Symcon-Instanz und den exakt
|
||||
zu prüfenden Repository-Stand.
|
||||
|
||||
## Checkliste für neue Module
|
||||
|
||||
1. PHPUnit-Tests für die reine Logik ergänzen.
|
||||
2. `tests/Symcon/modules/<Modul>.php` hinzufügen.
|
||||
3. Modul und betroffene Pfade in `tests/Symcon/manifest.php` registrieren.
|
||||
4. Instanz, Pflichtvariablen, Actions, Normalfall und mindestens einen
|
||||
Fehler- oder Grenzfall prüfen.
|
||||
5. Alle Hilfsobjekte über `TestContext` anlegen.
|
||||
6. `composer check`, Einzeltest und Gesamttest erfolgreich ausführen.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Enelix-2-Demoanlage
|
||||
|
||||
Die Demo erzeugt idempotent eine vollständige, spielbare EMS-Anlage in IP-Symcon 8.x:
|
||||
|
||||
- Enelix Manager mit manueller Zuordnung von fünf steuerbaren Teilnehmern,
|
||||
- zwei mehrstufige Boiler (1,2/2,4 kW und 1,5/3,0 kW),
|
||||
- einen einstufigen Entfeuchter (0,9 kW),
|
||||
- einen zweistufigen Pufferspeicher (1,8/3,6 kW) mit Heizkurve,
|
||||
- einen bidirektionalen Batteriespeicher (10 kWh, +/-3,5 kW),
|
||||
- eine technische Anlagentopologie mit Hybridwechselrichter, PV-Fläche und
|
||||
Batteriespeicher für die Prognoseanbindung,
|
||||
- simulierte Istleistungs- und Leistungsbegrenzungsregister des Wechselrichters,
|
||||
- eine aktive anlagenweite Einspeisebegrenzung auf 3.000 W mit 100 W Toleranz,
|
||||
- Prognosetelemetrie aus PV, Hausverbrauch, Netzleistung und Batterie-SOC im
|
||||
60-Sekunden-Intervall,
|
||||
- simulierte PV-Erzeugung, variable Bewölkung, Tageslastgang, Zusatzlast,
|
||||
Außentemperatur, Wärmeverluste, Netzleistung und Speicherzustände,
|
||||
- native Symcon-Instanz `Energy Distribution` für acht Energieflussknoten,
|
||||
- eigene Kachelansichten mit Laufzeitparametern für Boiler, Puffer und Batterie,
|
||||
- kombinierten Verlauf für PV-Leistung, Hausverbrauch, EMS-Leistung und Netzleistung,
|
||||
- kombinierten Verlauf der kumulierten PV-, Haus-, Bezugs- und Einspeiseenergie,
|
||||
- Archivierung der Leistungs-, Energie- und Temperaturwerte.
|
||||
|
||||
## Installation
|
||||
|
||||
Das Repository muss in der IP-Symcon-Modulverwaltung auf dem gewünschten Stand installiert sein.
|
||||
Danach in der Schnellausführung starten:
|
||||
|
||||
```php
|
||||
require_once IPS_GetKernelDir() . 'modules/Enelix-EMS/examples/Demoanlage/bootstrap.php';
|
||||
|
||||
$result = enelixDemoInstall();
|
||||
print_r($result);
|
||||
```
|
||||
|
||||
Ein weiterer Lauf aktualisiert dieselbe Anlage anhand stabiler Idents und erzeugt keine Duplikate.
|
||||
Bestehende, nicht zur Demo gehörende Objekte werden nicht verändert oder gelöscht.
|
||||
|
||||
Der Bootstrap hinterlegt absichtlich keinen Lizenzcode und überschreibt eine
|
||||
bereits konfigurierte Manager-Lizenz nicht. Bei einer neuen Anlage wird die
|
||||
Struktur vollständig erstellt; der Manager bleibt bis zur regulären
|
||||
Lizenzaktivierung im Konfigurationsformular auf Status 203. Solange der Manager
|
||||
nicht freigegeben ist, bleiben seine Zuordnung und die Verbraucher deaktiviert,
|
||||
damit keine Lizenzfehler protokolliert werden. Nach der Aktivierung den
|
||||
Bootstrap erneut ausführen; er ordnet dann alle fünf Teilnehmer zu und startet
|
||||
die Regelung.
|
||||
|
||||
Die Demo übernimmt die einstellbare PV-Spitzenleistung als AC- und
|
||||
DC-Anlagenleistung. Für die feste Beispieltopologie gelten 30 Grad Neigung und
|
||||
Südausrichtung. Der 10-kWh-Speicher nutzt den gemeinsamen Hybridwechselrichter;
|
||||
seine dynamischen Lade- und Entladegrenzen werden beim Bootstrap in die
|
||||
technischen Stammdaten übernommen. Bei aktiver Manager-Lizenz uebertraegt der
|
||||
Manager zusaetzlich PV-Leistung, Hausverbrauch, Netzleistung und Batterie-SOC
|
||||
alle 60 Sekunden an den Enelix-Prognosedienst. Die Demo verwendet ein absolutes
|
||||
Wechselrichter-Leistungsregister in Watt. Der Manager begrenzt die tatsächliche
|
||||
PV-Produktion so, dass die Gesamtanlage höchstens 3.000 W einspeist; innerhalb
|
||||
einer Toleranz von 100 W bleibt der zuletzt gesetzte Stellwert bestehen. Der
|
||||
intelligente Netzfahrplan bleibt deaktiviert, solange dessen Backendfassung nur
|
||||
als ausstehende Version bereitliegt.
|
||||
|
||||
## Bedienung
|
||||
|
||||
In der Kachelvisualisierung öffnet die Startkategorie `Enelix 2 Demoanlage`.
|
||||
Unter `Simulation` lassen sich Tageslauf, Tageszeit, Geschwindigkeit,
|
||||
dynamische Profile, PV-Spitzenleistung, mittlere Bewölkung, Haus-Grundlast und
|
||||
eine ungeregelte Zusatzlast verändern. Bei aktivierten dynamischen Profilen
|
||||
entstehen reproduzierbare Morgen- und Abendspitzen, ziehende Bewölkung und ein
|
||||
Tagesgang der Außentemperatur. Der Manager verteilt den verfügbaren
|
||||
PV-Überschuss nach Priorität auf beide Boiler, Entfeuchter, Pufferspeicher und
|
||||
Batterie. Bei hoher PV-Erzeugung ist die Abregelung an den technischen Variablen
|
||||
`PV-Wechselrichter Istleistung` und `PV-Wechselrichter Leistungsgrenze` sowie an
|
||||
der auf etwa 3.000 W begrenzten Netzeinspeisung sichtbar.
|
||||
|
||||
`Übersicht` zeigt die native Symcon-Energieverteilung. Die Kacheln `Boiler 1`,
|
||||
`Boiler 2`, `Pufferspeicher` und `Batteriespeicher` öffnen direkt die jeweiligen
|
||||
Bedien-, Status- und Diagnosevariablen, ohne zuerst das
|
||||
Instanz-Konfigurationsformular zu zeigen. Positive Batterieleistung bedeutet
|
||||
Laden, negative Leistung Entladen. Unter `Leistung und Energie` liegen die
|
||||
beiden kombinierten Zeitdiagramme und die Energiezähler. Die Energiesummen,
|
||||
Temperaturmodelle und der Ladezustand verwenden die beschleunigte
|
||||
Simulationszeit.
|
||||
|
||||
## Validierung
|
||||
|
||||
- Die fünf Verbraucher müssen Status 102 besitzen; der Manager nach Lizenzaktivierung ebenfalls (vorher erwarteter Status 203).
|
||||
- Der Manager muss fünf Teilnehmer aus vier unterschiedlichen Modultypen melden.
|
||||
- Bei ausreichendem PV-Überschuss müssen thermische Verbraucher schalten und die Batterie laden.
|
||||
- Bei höherem verbleibendem PV-Überschuss muss das Wechselrichterregister die PV-Leistung so begrenzen, dass ungefähr 3.000 W ins Netz eingespeist werden.
|
||||
- Wechselrichter-Istleistung und Leistungsgrenze müssen in der technischen Kategorie vorhanden und im Manager zugeordnet sein.
|
||||
- Puffertemperatur, Außentemperatur und Batterie-Ladezustand müssen sich über den Tageslauf verändern.
|
||||
- Netzleistung, PV-Leistung, Hausverbrauch, EMS-Leistung, Einzelverbräuche, Speicherleistung, Energiesummen und Temperaturen werden archiviert.
|
||||
- Die native Energieverteilung muss acht Knoten enthalten.
|
||||
- Beide Diagrammkacheln müssen mindestens drei beschriftete Zeitreihen anzeigen.
|
||||
- Ein erneuter Installationslauf muss dieselbe Root-ID zurückgeben.
|
||||
|
||||
## Rollback
|
||||
|
||||
Nur die eindeutig gekennzeichnete Demo wird entfernt:
|
||||
|
||||
```php
|
||||
require_once IPS_GetKernelDir() . 'modules/Enelix-EMS/examples/Demoanlage/bootstrap.php';
|
||||
|
||||
enelixDemoRemove(true);
|
||||
```
|
||||
|
||||
Der boolesche Parameter ist eine bewusste Löschbestätigung. Fremde Objektbäume
|
||||
und Systeminstanzen bleiben unberührt.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
--- a/install.php
|
||||
+++ b/install.php
|
||||
@@ -19,6 +19,8 @@
|
||||
}
|
||||
$before=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
|
||||
$report['files']=v4StageInstall(__DIR__,'/var/lib/symcon/modules/Enelix-EMS');
|
||||
+ require_once __DIR__.'/installed_capture_bootstrap.php';
|
||||
+ $report['classLoading']=v4ApplicationLoadInstalledCapture('/var/lib/symcon/modules/Enelix-EMS',__DIR__.'/MANIFEST.json');
|
||||
$controls=[];
|
||||
foreach (IPS_GetModuleList() as $mid) if ((IPS_GetModule($mid)['Prefix']??'')==='MC') $controls=array_merge($controls,IPS_GetInstanceListByModuleID($mid));
|
||||
if (count($controls)!==1||!function_exists('MC_ReloadModule')) throw new RuntimeException('Module Control nicht eindeutig.');
|
||||
@@ -38,7 +40,7 @@
|
||||
$marker=$dir.'/observer-import.json';
|
||||
if (!is_file($marker)&&!IPS_GetProperty($manager,'NetzfahrplanV4MessdatenAktiv')) {
|
||||
if (glob($dir.'/raw-*.jsonl')!==[]) throw new RuntimeException('Messdaten ohne Importabschluss vorhanden; nicht doppelt importieren.');
|
||||
- require_once __DIR__.'/source/libs/NetzfahrplanV4Messaufnahme.php';
|
||||
+ // Reuse the hash-checked installed class; never load the staged copy.
|
||||
$count=0;$last=0;$cutoff=time()-172800;
|
||||
$files=glob('/srv/agent/netplan-v4-separated-observer-stage/data/raw-*.jsonl');sort($files,SORT_STRING);
|
||||
foreach ($files as $file) {
|
||||
--- /dev/null
|
||||
+++ b/installed_capture_bootstrap.php
|
||||
@@ -0,0 +1,48 @@
|
||||
+<?php
|
||||
+
|
||||
+declare(strict_types=1);
|
||||
+
|
||||
+/** Installer-only bootstrap. Load the same files the installed Manager uses.
|
||||
+ * No IPS calls, source edits, state changes, or autoload of an unknown class.
|
||||
+ */
|
||||
+function v4ApplicationLoadInstalledCapture(string $target, string $manifestFile): array
|
||||
+{
|
||||
+ if (is_link($target) || realpath($target) !== $target || !is_dir($target)) {
|
||||
+ throw new RuntimeException('Unerwartetes Modulverzeichnis.');
|
||||
+ }
|
||||
+ $manifest = json_decode((string) file_get_contents($manifestFile), true, 64, JSON_THROW_ON_ERROR);
|
||||
+ if (($manifest['mode'] ?? null) !== 'data_application_only') {
|
||||
+ throw new RuntimeException('Unerwarteter Installationsumfang.');
|
||||
+ }
|
||||
+ $classes = [
|
||||
+ 'Belevo\\EnelixEMS\\NetzfahrplanV4Bilanzierung' => 'libs/NetzfahrplanV4Bilanzierung.php',
|
||||
+ 'Belevo\\EnelixEMS\\NetzfahrplanV4Messaufnahme' => 'libs/NetzfahrplanV4Messaufnahme.php',
|
||||
+ ];
|
||||
+ // Check BOTH origins before loading either file, including the transitive dependency.
|
||||
+ foreach ($classes as $class => $relative) {
|
||||
+ $path = $target . '/' . $relative;
|
||||
+ $expected = $manifest['files'][$relative]['after'] ?? null;
|
||||
+ if (!is_string($expected) || !preg_match('/^[a-f0-9]{64}$/D', $expected)
|
||||
+ || is_link($path) || !is_file($path) || realpath($path) !== $path
|
||||
+ || hash_file('sha256', $path) !== $expected) {
|
||||
+ throw new RuntimeException('Installierte Messbibliothek weicht vom geprueften Paket ab: ' . $relative);
|
||||
+ }
|
||||
+ if (class_exists($class, false)) {
|
||||
+ $origin = (new ReflectionClass($class))->getFileName();
|
||||
+ if ($origin !== $path) {
|
||||
+ throw new RuntimeException('Messbibliothek bereits aus anderem Pfad geladen. Installationsaufruf separat ausfuehren, ohne weitere Sammler-Includes.');
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ foreach ($classes as $class => $relative) {
|
||||
+ $path = $target . '/' . $relative;
|
||||
+ if (!class_exists($class, false)) {
|
||||
+ require_once $path;
|
||||
+ }
|
||||
+ if (!class_exists($class, false) || (new ReflectionClass($class))->getFileName() !== $path) {
|
||||
+ throw new RuntimeException('Installierte Messbibliothek konnte nicht eindeutig geladen werden.');
|
||||
+ }
|
||||
+ }
|
||||
+ return ['source' => 'installed_module_only', 'stagedClassesLoaded' => false,
|
||||
+ 'files' => array_values($classes)];
|
||||
+}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"scope": "confirmed_feedback_trial_disabled",
|
||||
"managerId": 17004,
|
||||
"batteryInstanceId": 44234,
|
||||
"files": {
|
||||
"libs/NetzfahrplanV4Rueckmeldung.php": {
|
||||
"before": "a7459a208c4dd84ffe3a166d266dcd00216f3660fe60e5ee61d5042d0b840631",
|
||||
"after": "0b261f6534c5ca9864018ea4c2caf97e6ce6913d92f13dcc926383b4f401a673"
|
||||
},
|
||||
"libs/NetzfahrplanV4Geraeteabruf.php": {
|
||||
"before": null,
|
||||
"after": "2b69a2ed27554139db6c996c906a4afe430918381e6359d23e8bb3369cd9be47"
|
||||
},
|
||||
"libs/BatterieNetzfahrplanV4RueckmeldungTrait.php": {
|
||||
"before": "24cf0654f1a6bd545d64ce5f115f309b16d8ffca5172ec64fb37c390c6f001a7",
|
||||
"after": "1de81499da07ad814c98ba778ca728e34817bdfcf29cc7e518f7b5f8fd7733b7"
|
||||
}
|
||||
},
|
||||
"testSupport": {
|
||||
"libs/BatterieNetzfahrplanV4TestTrait.php": "80474ff24cf9b2d32dfe481f0ce26bb94584c458b0fa3de232dec3897b64a4b2",
|
||||
"libs/NetzfahrplanV4Regeltest.php": "bdac3e2e54c50a4c1541da8918ddf3ab2f665bb5cc6696848f546e7b05f5f249",
|
||||
"libs/ManagerNetzfahrplanV4TestTrait.php": "c0d9b6336574dd8acf5f84adb12a32cad3b42ae91c5a31d513d0bc638a560f18",
|
||||
"libs/NetzfahrplanV4Planpruefung.php": "24b935d80bad4ce2d9047b6777aaf74a18773236ca91a2844a0907012f4fadf4"
|
||||
},
|
||||
"tests": {
|
||||
"tests/V4Feedback/checks.php": "b1aacc540ba11adabdd140aaded9e2968d1328f261ca15756e6002db3427330a",
|
||||
"tests/V4Feedback/device_read_checks.php": "5c5d9e50dd1d0f80cb01a2d95e63018eeec558ba78c07afde208e3c0ef304cc3",
|
||||
"tests/V4ControlTrial/checks.php": "2f12e94aeb207e6939a645f2f45b5f54e57c18530360ed3b16aa52ff89a1ef39",
|
||||
"tests/V4ControlTrial/additional_checks.php": "df01119f2b9c0befe1d14ae6b6ddf39c75d3687b669142e9b24b5f9f4821f2c3",
|
||||
"tests/V4ControlTrial/feedback_checks.php": "e87d41dff8e3ecd414a59b3dd7a234b34183d37acca8cddaf847b82eae92e60c",
|
||||
"tests/V4Receiver/fixture.php": "0eeb65e63250aff5ca364a82c2b2a71f9f229be2d9d412e3037014979deb793a"
|
||||
},
|
||||
"configurationSha256": "e393c1f01b30febc1d0785233474be43e98d849b152b4b2450b72efa6b73d540",
|
||||
"dependencies": {
|
||||
"libs/NetzfahrplanV4Regeltest.php": "bdac3e2e54c50a4c1541da8918ddf3ab2f665bb5cc6696848f546e7b05f5f249",
|
||||
"libs/ManagerNetzfahrplanV4TestTrait.php": "c0d9b6336574dd8acf5f84adb12a32cad3b42ae91c5a31d513d0bc638a560f18",
|
||||
"libs/ManagerNetzfahrplanV4EmpfangTrait.php": "936939a3e4a5a5d54fdff68f6fc689a9400e8406aecd5ac09ebc8a6b569c0b66",
|
||||
"Batterie/module.php": "3d5fcd83e4e7d999d5ac949fe357b492d5661ffa76639d2ca57a3f3934eabdbe",
|
||||
"Manager/module.php": "cbbb33a8f71dda8cc80a4fc01cd3d6154da8153b5523306b2da723bb44f2dd79"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# V4 confirmed feedback for Lihrenmoos
|
||||
|
||||
This incremental package adds synchronous device reads before V4 feedback is evaluated.
|
||||
|
||||
- Grid input 40348 is refreshed through its M-Bus device.
|
||||
- GoodWe and SolarEdge inputs are refreshed through their ModBus parent instances.
|
||||
- A successful driver call is required; cached variable timestamps alone are not accepted.
|
||||
- Daniel Haefliger accepted the existing `virtual_split` model for the bounded field test on 2026-10-04.
|
||||
- The installer keeps the legacy schedule and both V4 trial permissions disabled.
|
||||
- A real control trial still requires separate server authorization and evidence of an independent device-side watchdog.
|
||||
|
||||
Build a private staging directory:
|
||||
|
||||
```sh
|
||||
python3 examples/V4ConfirmedFeedback/build_stage.py /srv/agent/netplan-v4-confirmed-feedback
|
||||
```
|
||||
|
||||
Run the staged PHP checks before copying the package to the test plant. Execute `install.php` only from the IP-Symcon script editor on Lihrenmoos.
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Create the reviewed Lihrenmoos confirmed-feedback package; never installs into Symcon."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
|
||||
here = Path(__file__).resolve().parent
|
||||
repo = here.parents[1]
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("Usage: python3 build_stage.py NEW_PRIVATE_STAGING_DIRECTORY")
|
||||
target = Path(sys.argv[1]).resolve()
|
||||
allowed_roots = [Path("/srv/agent"), Path("/home/agent/services/qa")]
|
||||
if not any(target != root and target.is_relative_to(root) for root in allowed_roots):
|
||||
raise SystemExit("Use a private agent staging directory, not live module paths")
|
||||
if target.exists():
|
||||
raise SystemExit("Destination must not exist")
|
||||
|
||||
manifest = json.loads((here / "MANIFEST.json").read_text())
|
||||
contents = {}
|
||||
for group in ("files", "testSupport", "tests"):
|
||||
for name, record in manifest[group].items():
|
||||
expected = record["after"] if group == "files" else record
|
||||
source = repo / name
|
||||
if source.is_symlink() or not source.resolve().is_relative_to(repo):
|
||||
raise SystemExit("Unsafe source: " + name)
|
||||
data = source.read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != expected:
|
||||
raise SystemExit("Reviewed source changed: " + name)
|
||||
contents["source/" + name] = data
|
||||
|
||||
for name, expected in manifest["dependencies"].items():
|
||||
if hashlib.sha256((repo / name).read_bytes()).hexdigest() != expected:
|
||||
raise SystemExit("Dependency changed: " + name)
|
||||
|
||||
for name in ("install.php", "file_installer.php", "MANIFEST.json", "feedback-config.json", "README.md"):
|
||||
contents[name] = (here / name).read_bytes()
|
||||
if hashlib.sha256(contents["feedback-config.json"]).hexdigest() != manifest["configurationSha256"]:
|
||||
raise SystemExit("Configuration changed")
|
||||
|
||||
target.mkdir(parents=True, mode=0o700)
|
||||
for name, data in contents.items():
|
||||
path = target / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
path.chmod(0o640)
|
||||
(target / "PACKAGE_HASHES.json").write_text(
|
||||
json.dumps({name: hashlib.sha256(data).hexdigest() for name, data in contents.items()}, indent=2) + "\n"
|
||||
)
|
||||
print("Private confirmed-feedback package created:", target, "; both trial permissions remain disabled.")
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"version": 1,
|
||||
"installationId": "e3a08f9e-af12-4695-99bd-8b51c0520021",
|
||||
"assetId": "anlage01-virtual-ev",
|
||||
"managerId": 17004,
|
||||
"batteryInstanceId": 44234,
|
||||
"mode": "virtual_split",
|
||||
"allowEstimatedForTrial": true,
|
||||
"maxSkewSeconds": 30,
|
||||
"maxAgeSeconds": 60,
|
||||
"idleToleranceW": 50.0,
|
||||
"trackingToleranceW": 200.0,
|
||||
"sources": [
|
||||
{
|
||||
"key": "grid",
|
||||
"variableId": 40348,
|
||||
"parentId": 11490,
|
||||
"ident": "Power_8",
|
||||
"factorToW": 1000,
|
||||
"role": "grid"
|
||||
},
|
||||
{
|
||||
"key": "physical_goodwe1",
|
||||
"variableId": 47725,
|
||||
"parentId": 19742,
|
||||
"ident": "A_6_3_35182",
|
||||
"factorToW": -1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "physical_goodwe2",
|
||||
"variableId": 35724,
|
||||
"parentId": 57658,
|
||||
"ident": "A_6_3_35182",
|
||||
"factorToW": -1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "physical_solaredge",
|
||||
"variableId": 21447,
|
||||
"parentId": 30789,
|
||||
"ident": "Value",
|
||||
"factorToW": 1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "ev_requested",
|
||||
"variableId": 19651,
|
||||
"parentId": 58448,
|
||||
"ident": "Nennleistung_Soll_EV",
|
||||
"factorToW": 1.0,
|
||||
"role": "ev_request"
|
||||
},
|
||||
{
|
||||
"key": "sdl_requested",
|
||||
"variableId": 38943,
|
||||
"parentId": 58448,
|
||||
"ident": "Nennleistung_Soll_SDL",
|
||||
"factorToW": 1.0,
|
||||
"role": "sdl_request"
|
||||
},
|
||||
{
|
||||
"key": "gateway_active",
|
||||
"role": "gateway_active",
|
||||
"variableId": 23483,
|
||||
"parentId": 58448,
|
||||
"ident": "State",
|
||||
"factorToW": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Atomic incremental installer for the confirmed-feedback update. */
|
||||
function v4ConfirmedWrite(string $path, string $content, int $mode = 0644): void
|
||||
{
|
||||
if (is_link($path)) {
|
||||
throw new RuntimeException('Symlink wird nicht ersetzt.');
|
||||
}
|
||||
$temp = tempnam(dirname($path), '.v4-confirmed-');
|
||||
if ($temp === false) {
|
||||
throw new RuntimeException('Temporaere Datei nicht verfuegbar.');
|
||||
}
|
||||
try {
|
||||
if (file_put_contents($temp, $content, LOCK_EX) !== strlen($content)
|
||||
|| !chmod($temp, $mode)
|
||||
|| !rename($temp, $path)) {
|
||||
throw new RuntimeException('Atomarer Dateitausch fehlgeschlagen.');
|
||||
}
|
||||
} finally {
|
||||
if (is_file($temp)) {
|
||||
unlink($temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function v4ConfirmedFiles(string $stage, string $target): array
|
||||
{
|
||||
$allowed = [
|
||||
'libs/NetzfahrplanV4Rueckmeldung.php',
|
||||
'libs/NetzfahrplanV4Geraeteabruf.php',
|
||||
'libs/BatterieNetzfahrplanV4RueckmeldungTrait.php',
|
||||
];
|
||||
$manifest = json_decode((string) file_get_contents($stage . '/MANIFEST.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
if (($manifest['scope'] ?? null) !== 'confirmed_feedback_trial_disabled'
|
||||
|| count($manifest['files'] ?? []) !== count($allowed)
|
||||
|| array_diff(array_keys($manifest['files']), $allowed)
|
||||
|| array_diff($allowed, array_keys($manifest['files']))) {
|
||||
throw new RuntimeException('Unerwarteter Dateiumfang.');
|
||||
}
|
||||
if (realpath($target) !== $target || is_link($target)) {
|
||||
throw new RuntimeException('Modulpfad unerwartet.');
|
||||
}
|
||||
foreach ($manifest['dependencies'] as $name => $hash) {
|
||||
if (!preg_match('~^(libs/[A-Za-z0-9]+\\.php|Batterie/module\\.php|Manager/module\\.php)$~D', $name)
|
||||
|| is_link($target . '/' . $name)
|
||||
|| hash_file('sha256', $target . '/' . $name) !== $hash) {
|
||||
throw new RuntimeException('Abhaengigkeit wurde parallel geaendert: ' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
$before = [];
|
||||
$source = [];
|
||||
$already = true;
|
||||
foreach ($allowed as $name) {
|
||||
$path = $target . '/' . $name;
|
||||
$candidate = $stage . '/source/' . $name;
|
||||
if (is_link($path) || is_link($candidate) || realpath(dirname($path)) !== $target . '/' . dirname($name)) {
|
||||
throw new RuntimeException('Unerwarteter Dateipfad.');
|
||||
}
|
||||
$source[$name] = (string) file_get_contents($candidate);
|
||||
if (hash('sha256', $source[$name]) !== $manifest['files'][$name]['after']) {
|
||||
throw new RuntimeException('Paketpruefsumme geaendert: ' . $name);
|
||||
}
|
||||
token_get_all($source[$name], TOKEN_PARSE);
|
||||
$before[$name] = is_file($path) ? file_get_contents($path) : null;
|
||||
$hash = $before[$name] === null ? null : hash('sha256', $before[$name]);
|
||||
if (!in_array($hash, [$manifest['files'][$name]['before'], $manifest['files'][$name]['after']], true)) {
|
||||
throw new RuntimeException('Paralleler Modulstand; nichts ueberschrieben: ' . $name);
|
||||
}
|
||||
if ($hash !== $manifest['files'][$name]['after']) {
|
||||
$already = false;
|
||||
}
|
||||
}
|
||||
if ($already) {
|
||||
return ['status' => 'already_installed', 'backup' => null, 'filesChanged' => []];
|
||||
}
|
||||
|
||||
$backup = $stage . '/backups/' . gmdate('Ymd\\THis\\Z') . '-' . bin2hex(random_bytes(4));
|
||||
if (!mkdir($backup, 0700, true)) {
|
||||
throw new RuntimeException('Sicherung fehlgeschlagen.');
|
||||
}
|
||||
foreach ($allowed as $name) {
|
||||
if ($before[$name] !== null) {
|
||||
$destination = $backup . '/' . $name;
|
||||
if (!is_dir(dirname($destination))) {
|
||||
mkdir(dirname($destination), 0700, true);
|
||||
}
|
||||
v4ConfirmedWrite($destination, $before[$name], 0600);
|
||||
}
|
||||
}
|
||||
v4ConfirmedWrite(
|
||||
$backup . '/MANIFEST.json',
|
||||
json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . "\n",
|
||||
0600
|
||||
);
|
||||
|
||||
$written = [];
|
||||
try {
|
||||
foreach ($allowed as $name) {
|
||||
if ((is_file($target . '/' . $name) ? file_get_contents($target . '/' . $name) : null) !== $before[$name]) {
|
||||
throw new RuntimeException('Parallele Aenderung waehrend Installation.');
|
||||
}
|
||||
if ($before[$name] === $source[$name]) {
|
||||
continue;
|
||||
}
|
||||
v4ConfirmedWrite($target . '/' . $name, $source[$name]);
|
||||
$written[] = $name;
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
foreach (array_reverse($written) as $name) {
|
||||
$path = $target . '/' . $name;
|
||||
if (!is_link($path) && is_file($path)
|
||||
&& hash_file('sha256', $path) === $manifest['files'][$name]['after']) {
|
||||
if ($before[$name] === null) {
|
||||
unlink($path);
|
||||
} else {
|
||||
v4ConfirmedWrite($path, $before[$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw $error;
|
||||
}
|
||||
|
||||
return ['status' => 'installed', 'backup' => $backup, 'filesChanged' => $written];
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Execute only in Lihrenmoos Symcon. Installs confirmed reads and accepted virtual_split mapping; never starts a trial.
|
||||
if (!function_exists('IPS_GetKernelVersion')) {
|
||||
throw new RuntimeException('Nur im IP-Symcon-Skripteditor ausfuehren.');
|
||||
}
|
||||
require_once __DIR__ . '/file_installer.php';
|
||||
|
||||
$manager = 17004;
|
||||
$battery = 44234;
|
||||
$locked = false;
|
||||
$changedMapping = false;
|
||||
$oldMapping = null;
|
||||
$report = [
|
||||
'scope' => 'confirmed_feedback_trial_disabled',
|
||||
'startedAt' => gmdate('c'),
|
||||
'actuatorPermissionGranted' => false,
|
||||
'serverUpdateRequired' => true,
|
||||
'acceptedModel' => 'virtual_split',
|
||||
];
|
||||
|
||||
try {
|
||||
$hashes = json_decode((string) file_get_contents(__DIR__ . '/PACKAGE_HASHES.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
foreach ($hashes as $name => $hash) {
|
||||
if (str_contains($name, '..') || str_starts_with($name, '/') || is_link(__DIR__ . '/' . $name)
|
||||
|| hash_file('sha256', __DIR__ . '/' . $name) !== $hash) {
|
||||
throw new RuntimeException('Vorbereitetes Paket veraendert.');
|
||||
}
|
||||
}
|
||||
foreach ([
|
||||
$manager => '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||
$battery => '{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
|
||||
] as $id => $module) {
|
||||
if (!IPS_InstanceExists($id) || IPS_GetInstance($id)['ModuleInfo']['ModuleID'] !== $module) {
|
||||
throw new RuntimeException('Modulzuordnung ungueltig.');
|
||||
}
|
||||
if (IPS_HasChanges($id)) {
|
||||
throw new RuntimeException('Offene Aenderungen zuerst speichern oder verwerfen.');
|
||||
}
|
||||
}
|
||||
|
||||
$beforeManager = json_decode(IPS_GetConfiguration($manager), true, 128, JSON_THROW_ON_ERROR);
|
||||
$beforeBattery = json_decode(IPS_GetConfiguration($battery), true, 128, JSON_THROW_ON_ERROR);
|
||||
if (($beforeManager['NetzfahrplanAktiv'] ?? null) !== false
|
||||
|| ($beforeManager['NetzfahrplanV4RegeltestErlaubt'] ?? false) !== false
|
||||
|| ($beforeBattery['NetzfahrplanV4RegeltestErlaubt'] ?? false) !== false) {
|
||||
throw new RuntimeException('Alte Fahrplanausgabe und beide Regeltestfreigaben muessen AUS bleiben.');
|
||||
}
|
||||
|
||||
$configuration = json_decode((string) file_get_contents(__DIR__ . '/feedback-config.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
$manifest = json_decode((string) file_get_contents(__DIR__ . '/MANIFEST.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
if (($configuration['managerId'] ?? null) !== $manager
|
||||
|| ($configuration['batteryInstanceId'] ?? null) !== $battery
|
||||
|| ($configuration['mode'] ?? null) !== 'virtual_split'
|
||||
|| ($configuration['allowEstimatedForTrial'] ?? null) !== true
|
||||
|| hash_file('sha256', __DIR__ . '/feedback-config.json') !== $manifest['configurationSha256']) {
|
||||
throw new RuntimeException('Falsche Rueckmeldekonfiguration.');
|
||||
}
|
||||
$desired = json_encode($configuration, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
|
||||
$previousConfiguration = $configuration;
|
||||
$previousConfiguration['allowEstimatedForTrial'] = false;
|
||||
$previousDesired = json_encode($previousConfiguration, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
|
||||
$oldMapping = $beforeBattery['NetzfahrplanV4RueckmeldungKonfiguration'] ?? '{}';
|
||||
if (!in_array($oldMapping, [$previousDesired, $desired], true)) {
|
||||
throw new RuntimeException('Abweichende Rueckmeldekonfiguration nicht ueberschrieben.');
|
||||
}
|
||||
|
||||
$controls = [];
|
||||
foreach (IPS_GetModuleList() as $module) {
|
||||
if ((IPS_GetModule($module)['Prefix'] ?? '') === 'MC') {
|
||||
$controls = array_merge($controls, IPS_GetInstanceListByModuleID($module));
|
||||
}
|
||||
}
|
||||
if (count($controls) !== 1 || !function_exists('MC_ReloadModule')) {
|
||||
throw new RuntimeException('Module Control nicht eindeutig.');
|
||||
}
|
||||
$locked = IPS_SemaphoreEnter('ENELIX.V4.ConfirmedFeedbackInstall', 3000);
|
||||
if (!$locked) {
|
||||
throw new RuntimeException('Installation bereits aktiv.');
|
||||
}
|
||||
|
||||
$report['files'] = v4ConfirmedFiles(__DIR__, '/var/lib/symcon/modules/Enelix-EMS');
|
||||
if (MC_ReloadModule($controls[0], 'Enelix-EMS') === false) {
|
||||
throw new RuntimeException('Bibliotheks-Reload fehlgeschlagen.');
|
||||
}
|
||||
|
||||
$afterManager = json_decode(IPS_GetConfiguration($manager), true, 128, JSON_THROW_ON_ERROR);
|
||||
$afterBattery = json_decode(IPS_GetConfiguration($battery), true, 128, JSON_THROW_ON_ERROR);
|
||||
foreach ([[$beforeManager, $afterManager], [$beforeBattery, $afterBattery]] as $pair) {
|
||||
foreach ($pair[0] as $key => $value) {
|
||||
if (!array_key_exists($key, $pair[1]) || $pair[1][$key] !== $value) {
|
||||
throw new RuntimeException('Bestehender Parameter beim Reload veraendert: ' . $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('ENELIX_GetV4BatterieRueckmeldung')) {
|
||||
$report['status'] = 'waiting_for_registration';
|
||||
echo "Dateien installiert. Modulregistrierung wird abgeschlossen; diesen Aufruf einmal wiederholen. Keine Stellfreigabe.\n";
|
||||
} else {
|
||||
if ($afterBattery['NetzfahrplanV4RueckmeldungKonfiguration'] !== $desired) {
|
||||
IPS_SetProperty($battery, 'NetzfahrplanV4RueckmeldungKonfiguration', $desired);
|
||||
$changedMapping = true;
|
||||
IPS_ApplyChanges($battery);
|
||||
}
|
||||
$finalManager = json_decode(IPS_GetConfiguration($manager), true, 128, JSON_THROW_ON_ERROR);
|
||||
$finalBattery = json_decode(IPS_GetConfiguration($battery), true, 128, JSON_THROW_ON_ERROR);
|
||||
foreach ([[$beforeManager, $finalManager], [$beforeBattery, $finalBattery]] as $pair) {
|
||||
foreach ($pair[0] as $key => $value) {
|
||||
if ($key !== 'NetzfahrplanV4RueckmeldungKonfiguration'
|
||||
&& (!array_key_exists($key, $pair[1]) || $pair[1][$key] !== $value)) {
|
||||
throw new RuntimeException('Bestehender Parameter unerwartet veraendert: ' . $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (($finalManager['NetzfahrplanAktiv'] ?? null) !== false
|
||||
|| ($finalManager['NetzfahrplanV4RegeltestErlaubt'] ?? null) !== false
|
||||
|| ($finalBattery['NetzfahrplanV4RegeltestErlaubt'] ?? null) !== false) {
|
||||
throw new RuntimeException('Unerwartete Stellfreigabe nach Installation.');
|
||||
}
|
||||
|
||||
$feedback = json_decode(ENELIX_GetV4BatterieRueckmeldung($battery), true, 32, JSON_THROW_ON_ERROR);
|
||||
$report['feedback'] = array_intersect_key($feedback, array_flip([
|
||||
'status', 'kind', 'method', 'reason', 'reasons', 'estimated', 'batteryW', 'gridW',
|
||||
'sourceOldestAt', 'sourceVariableOldestAt', 'confirmedAt', 'checkedAt', 'configHash',
|
||||
'deviceReadConfirmed', 'usableForTrial', 'canDispatch',
|
||||
]));
|
||||
$report['status'] = 'confirmed_feedback_installed_trial_disabled';
|
||||
$report['originalParametersPreserved'] = true;
|
||||
echo "OK: Geraeteabfrage bestaetigt die physischen Messwerte; virtual_split ist fuer den begrenzten Regeltest akzeptiert.\n";
|
||||
echo 'Rueckmeldung: ' . ($feedback['status'] ?? 'available')
|
||||
. '; Geraeteabfrage: ' . (($feedback['deviceReadConfirmed'] ?? false) ? 'bestaetigt' : 'nicht bestaetigt')
|
||||
. '; Modellanteil: ' . (($feedback['estimated'] ?? false) ? 'ja' : 'nein') . ".\n";
|
||||
if (isset($feedback['reason'])) {
|
||||
echo 'Aktueller Hinweis: ' . $feedback['reason'] . "\n";
|
||||
}
|
||||
echo "Beide Regeltestfreigaben AUS. Kein Stellbefehl ohne separat nachgewiesenen Geraete-Watchdog und Serverfreigabe.\n";
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
$report['status'] = 'needs_review';
|
||||
$report['reason'] = substr($error->getMessage(), 0, 260);
|
||||
if ($changedMapping && $oldMapping !== null) {
|
||||
try {
|
||||
IPS_SetProperty($battery, 'NetzfahrplanV4RueckmeldungKonfiguration', $oldMapping);
|
||||
IPS_ApplyChanges($battery);
|
||||
$report['mappingRestored'] = true;
|
||||
} catch (Throwable $ignored) {
|
||||
$report['mappingRestored'] = false;
|
||||
}
|
||||
}
|
||||
echo 'FEHLER: ' . $report['reason'] . "\nKeine V4-Stellfreigabe erteilt.\n";
|
||||
} finally {
|
||||
$report['finishedAt'] = gmdate('c');
|
||||
v4ConfirmedWrite(__DIR__ . '/INSTALL_RESULT.json', json_encode($report, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . "\n", 0644);
|
||||
if ($locked) {
|
||||
IPS_SemaphoreLeave('ENELIX.V4.ConfirmedFeedbackInstall');
|
||||
}
|
||||
echo 'CONFIRMED FEEDBACK INSTALL REPORT: ' . __DIR__ . "/INSTALL_RESULT.json\n";
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"scope": "corrected_feedback_trial_disabled",
|
||||
"managerId": 17004,
|
||||
"batteryInstanceId": 44234,
|
||||
"files": {
|
||||
"libs/BatterieNetzfahrplanV4RueckmeldungTrait.php": {
|
||||
"before": null,
|
||||
"after": "24cf0654f1a6bd545d64ce5f115f309b16d8ffca5172ec64fb37c390c6f001a7"
|
||||
},
|
||||
"libs/BatterieNetzfahrplanV4TestTrait.php": {
|
||||
"before": null,
|
||||
"after": "80474ff24cf9b2d32dfe481f0ce26bb94584c458b0fa3de232dec3897b64a4b2"
|
||||
},
|
||||
"libs/BatterieRegler.php": {
|
||||
"before": "035988c54d4d0cc3a59d234f1c08a8edbf5fcac42e09a6bcea61d3dd8270771b",
|
||||
"after": "beecc9c817b722336367e1c574c1a38afc8f3f5346442170d67c101b980adf7b"
|
||||
},
|
||||
"libs/ManagerNetzfahrplanV4EmpfangTrait.php": {
|
||||
"before": "d083cfa6ae8e8026c134d2b53f34bb6fbe342fb7332a3e7da1b881de33e13e97",
|
||||
"after": "936939a3e4a5a5d54fdff68f6fc689a9400e8406aecd5ac09ebc8a6b569c0b66"
|
||||
},
|
||||
"libs/ManagerNetzfahrplanV4TestTrait.php": {
|
||||
"before": null,
|
||||
"after": "c0d9b6336574dd8acf5f84adb12a32cad3b42ae91c5a31d513d0bc638a560f18"
|
||||
},
|
||||
"libs/NetzfahrplanV4Regeltest.php": {
|
||||
"before": null,
|
||||
"after": "bdac3e2e54c50a4c1541da8918ddf3ab2f665bb5cc6696848f546e7b05f5f249"
|
||||
},
|
||||
"libs/NetzfahrplanV4Rueckmeldung.php": {
|
||||
"before": null,
|
||||
"after": "a7459a208c4dd84ffe3a166d266dcd00216f3660fe60e5ee61d5042d0b840631"
|
||||
},
|
||||
"Batterie/module.php": {
|
||||
"before": "34503645f7e21dd1e5bf0a3f754f6592175dd67f9a263bf431f3801209555401",
|
||||
"after": "3d5fcd83e4e7d999d5ac949fe357b492d5661ffa76639d2ca57a3f3934eabdbe"
|
||||
},
|
||||
"Manager/module.php": {
|
||||
"before": "83e80883c8f9b817c0af5e239b20bcb019c7cf7f7943ee5bcc6493ac5a743532",
|
||||
"after": "cbbb33a8f71dda8cc80a4fc01cd3d6154da8153b5523306b2da723bb44f2dd79"
|
||||
}
|
||||
},
|
||||
"configurationSha256": "f66cfb9ebf5e02259b902f876a4c78756a51b3f2f2c240fd92c0f01161268c56",
|
||||
"dependencies": {
|
||||
"libs/NetzfahrplanV4Planpruefung.php": "24b935d80bad4ce2d9047b6777aaf74a18773236ca91a2844a0907012f4fadf4",
|
||||
"libs/Nachrichtenvertrag.php": "215ad7d4c5240b39b48cb7672f5057afa532aafdb34039862a1a22985a9ee839",
|
||||
"libs/VerbraucherBasisTrait.php": "10fd9f54283c378104bce11d623d5581558355d26fc2f85bf04b316542874507",
|
||||
"libs/VerbraucherSchnittstelle.php": "6329d423dca7b551f01fd884204be45be43f92cd6da6e4faedf2c3c9b4654a15",
|
||||
"libs/ManagerNetzfahrplanV4Trait.php": "e27a3595e4ed8cdb62e2561149ef6a50adb9194e0239eb25c1216aee2716fa88",
|
||||
"libs/ManagerNetzfahrplanV4DatenTrait.php": "59ae0ae283839ca82c58db9c071dcf28d0ab95e44f7a74c6473aca837ce416c4",
|
||||
"libs/NetzfahrplanV4Datenarchiv.php": "57ef7a60c273c5270b3e93ff5528d7d294e708725c9edfe81ac7ed819587f983",
|
||||
"libs/NetzfahrplanV4Messaufnahme.php": "ebe873bb714041e505a4f500b34c9bae3a5a0b93ac459161584ced758507a6e9",
|
||||
"libs/NetzfahrplanV4Bilanzierung.php": "ffda2a03dbb226a4405a2ad66704c080a9dc475ce64282ad3f0d7d60034ab199",
|
||||
"libs/NetzfahrplanV4Betriebsdaten.php": "6ff7d5710995778e7f941020a6f18555307ef51f16867f13efc204915e6dc9d9",
|
||||
"libs/NetzfahrplanV4Bezugszaehler.php": "7aa01ce83a343eb767a889575fa04cece7f1c65cda347723e24dd68da40cea9a",
|
||||
"libs/ManagerEnergieTrait.php": "2f89b0ad31ee6dd99180a7225173036eb6212b2ea7157e57fee23f0f8b441a51",
|
||||
"libs/EnergieMessung.php": "16c64e1cbefde81c25f913aaca6546f0d30cc8f2fddecea0768a3760a98e97d2",
|
||||
"libs/StoerungsSnapshot.php": "c871c1dd58d7fb1ef6c06985de066518688ef080f3cbdadda192d66cedcf49e1",
|
||||
"libs/Anlagentopologie.php": "1b7e95d103a3048ed38c464a424f2f12821a147b269e149fd6b89bca84878c47",
|
||||
"libs/Lizenzpruefung.php": "cb93cb49b93ed3ebb2060dba28b5b2dec2f29548d6ccff1afd9c7cb4624ad77e",
|
||||
"libs/EinspeiseRegler.php": "8b982be5b95275c6dd6611a2bf2b1f89b991ba53cf94e46df0eed3249add65e0",
|
||||
"libs/ManagerRegler.php": "608a4abebbe45da37a16316a530fa3f5ab21cb880bb341acbdcd3a9aaa93c6ee",
|
||||
"libs/ManagerSchnittstelle.php": "0aae8c8c85eb252c577fcb0b5a50a12f3247e4a0bb083579122854f6c35878b3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Recreate a reviewed Lihrenmoos package from this commit; never install in Symcon."""
|
||||
from pathlib import Path
|
||||
import hashlib,json,sys
|
||||
here=Path(__file__).resolve().parent;repo=here.parents[1]
|
||||
if len(sys.argv)!=2:raise SystemExit('Usage: python3 build_stage.py NEW_PRIVATE_STAGING_DIRECTORY')
|
||||
target=Path(sys.argv[1]).resolve()
|
||||
allowed=[Path('/srv/agent'),Path('/home/agent/services/qa')]
|
||||
if not any(target!=p and target.is_relative_to(p) for p in allowed):raise SystemExit('Use a private agent staging directory, not live module paths')
|
||||
if target.exists():raise SystemExit('Destination must not exist')
|
||||
m=json.loads((here/'MANIFEST.json').read_text());contents={}
|
||||
for n,v in m['files'].items():
|
||||
p=repo/n
|
||||
if p.is_symlink() or not p.resolve().is_relative_to(repo):raise SystemExit('Unsafe source')
|
||||
data=p.read_bytes()
|
||||
if hashlib.sha256(data).hexdigest()!=v['after']:raise SystemExit('Reviewed source changed: '+n)
|
||||
contents['source/'+n]=data
|
||||
for n,v in m['dependencies'].items():
|
||||
if hashlib.sha256((repo/n).read_bytes()).hexdigest()!=v:raise SystemExit('Dependency changed: '+n)
|
||||
for n in ['install.php','file_installer.php','MANIFEST.json','feedback-config.json']:contents[n]=(here/n).read_bytes()
|
||||
if hashlib.sha256(contents['feedback-config.json']).hexdigest()!=m['configurationSha256']:raise SystemExit('Configuration changed')
|
||||
target.mkdir(parents=True,mode=0o700)
|
||||
for n,data in contents.items():
|
||||
p=target/n;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(data);p.chmod(0o640)
|
||||
(target/'PACKAGE_HASHES.json').write_text(json.dumps({n:hashlib.sha256(v).hexdigest() for n,v in contents.items()},indent=2)+'\n')
|
||||
print('Private staging package created:',target,'; no running module, settings or control changes.')
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"version": 1,
|
||||
"installationId": "e3a08f9e-af12-4695-99bd-8b51c0520021",
|
||||
"assetId": "anlage01-virtual-ev",
|
||||
"managerId": 17004,
|
||||
"batteryInstanceId": 44234,
|
||||
"mode": "virtual_split",
|
||||
"allowEstimatedForTrial": false,
|
||||
"maxSkewSeconds": 30,
|
||||
"maxAgeSeconds": 60,
|
||||
"idleToleranceW": 50.0,
|
||||
"trackingToleranceW": 200.0,
|
||||
"sources": [
|
||||
{
|
||||
"key": "grid",
|
||||
"variableId": 40348,
|
||||
"parentId": 11490,
|
||||
"ident": "Power_8",
|
||||
"factorToW": 1000,
|
||||
"role": "grid"
|
||||
},
|
||||
{
|
||||
"key": "physical_goodwe1",
|
||||
"variableId": 47725,
|
||||
"parentId": 19742,
|
||||
"ident": "A_6_3_35182",
|
||||
"factorToW": -1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "physical_goodwe2",
|
||||
"variableId": 35724,
|
||||
"parentId": 57658,
|
||||
"ident": "A_6_3_35182",
|
||||
"factorToW": -1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "physical_solaredge",
|
||||
"variableId": 21447,
|
||||
"parentId": 30789,
|
||||
"ident": "Value",
|
||||
"factorToW": 1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "ev_requested",
|
||||
"variableId": 19651,
|
||||
"parentId": 58448,
|
||||
"ident": "Nennleistung_Soll_EV",
|
||||
"factorToW": 1.0,
|
||||
"role": "ev_request"
|
||||
},
|
||||
{
|
||||
"key": "sdl_requested",
|
||||
"variableId": 38943,
|
||||
"parentId": 58448,
|
||||
"ident": "Nennleistung_Soll_SDL",
|
||||
"factorToW": 1.0,
|
||||
"role": "sdl_request"
|
||||
},
|
||||
{
|
||||
"key": "gateway_active",
|
||||
"role": "gateway_active",
|
||||
"variableId": 23483,
|
||||
"parentId": 58448,
|
||||
"ident": "State",
|
||||
"factorToW": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** File-only installation helpers. No module classes loaded from the staging tree. */
|
||||
function v4FeedbackWrite(string $path, string $content, int $mode=0644): void
|
||||
{
|
||||
if (is_link($path)) throw new RuntimeException('Symlink wird nicht ersetzt.');
|
||||
$temp=tempnam(dirname($path),'.v4-feedback-');
|
||||
if ($temp===false) throw new RuntimeException('Temporaere Datei nicht verfuegbar.');
|
||||
try {
|
||||
if (file_put_contents($temp,$content,LOCK_EX)!==strlen($content) || !chmod($temp,$mode) || !rename($temp,$path)) throw new RuntimeException('Atomarer Dateitausch fehlgeschlagen.');
|
||||
} finally { if(is_file($temp))unlink($temp); }
|
||||
}
|
||||
|
||||
function v4FeedbackFiles(string $stage,string $target): array
|
||||
{
|
||||
$allowed=['libs/NetzfahrplanV4Rueckmeldung.php','libs/BatterieNetzfahrplanV4RueckmeldungTrait.php','libs/NetzfahrplanV4Regeltest.php',
|
||||
'libs/BatterieNetzfahrplanV4TestTrait.php','libs/ManagerNetzfahrplanV4TestTrait.php','libs/ManagerNetzfahrplanV4EmpfangTrait.php','libs/BatterieRegler.php','Batterie/module.php','Manager/module.php'];
|
||||
$m=json_decode((string)file_get_contents($stage.'/MANIFEST.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
if (($m['scope']??null)!=='corrected_feedback_trial_disabled' || count($m['files']??[])!==count($allowed) || array_diff(array_keys($m['files']),$allowed)) throw new RuntimeException('Unerwarteter Dateiumfang.');
|
||||
if (realpath($target)!==$target || is_link($target)) throw new RuntimeException('Modulpfad unerwartet.');
|
||||
foreach($m['dependencies'] as $name=>$hash) {
|
||||
if (!preg_match('~^libs/[A-Za-z0-9]+\.php$~D',$name) || is_link($target.'/'.$name) || hash_file('sha256',$target.'/'.$name)!==$hash) throw new RuntimeException('Abhaengigkeit wurde parallel geaendert: '.$name);
|
||||
}
|
||||
$before=[];$source=[];$already=true;
|
||||
// Explicit dependency order. Both top-level modules are replaced only after all dependencies.
|
||||
foreach($allowed as$name) {
|
||||
$path=$target.'/'.$name;$candidate=$stage.'/source/'.$name;
|
||||
if (is_link($path)||is_link($candidate)||realpath(dirname($path))!==$target.'/'.dirname($name)) throw new RuntimeException('Unerwarteter Dateipfad.');
|
||||
$source[$name]=(string)file_get_contents($candidate);
|
||||
if (hash('sha256',$source[$name])!==$m['files'][$name]['after']) throw new RuntimeException('Paketpruefsumme geaendert: '.$name);
|
||||
token_get_all($source[$name],TOKEN_PARSE);
|
||||
$before[$name]=is_file($path)?file_get_contents($path):null;
|
||||
$hash=$before[$name]===null?null:hash('sha256',$before[$name]);
|
||||
if (!in_array($hash,[$m['files'][$name]['before'],$m['files'][$name]['after']],true)) throw new RuntimeException('Paralleler Modulstand; nichts ueberschrieben: '.$name);
|
||||
if($hash!==$m['files'][$name]['after'])$already=false;
|
||||
}
|
||||
if($already)return ['status'=>'already_installed','backup'=>null];
|
||||
$backup=$stage.'/backups/'.gmdate('Ymd\THis\Z').'-'.bin2hex(random_bytes(4));
|
||||
if(!mkdir($backup,0700,true))throw new RuntimeException('Sicherung fehlgeschlagen.');
|
||||
foreach($allowed as$name) {
|
||||
if($before[$name]!==null) {
|
||||
$dest=$backup.'/'.$name;if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);
|
||||
v4FeedbackWrite($dest,$before[$name],0600);
|
||||
}
|
||||
}
|
||||
v4FeedbackWrite($backup.'/MANIFEST.json',json_encode($m,JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT),0600);
|
||||
$written=[];
|
||||
try {
|
||||
foreach($allowed as$name) {
|
||||
if((is_file($target.'/'.$name)?file_get_contents($target.'/'.$name):null)!==$before[$name])throw new RuntimeException('Parallele Aenderung waehrend Installation.');
|
||||
if($before[$name]===$source[$name])continue;
|
||||
v4FeedbackWrite($target.'/'.$name,$source[$name]);$written[]=$name;
|
||||
}
|
||||
} catch(Throwable $e) {
|
||||
foreach(array_reverse($written)as$name) {
|
||||
$p=$target.'/'.$name;
|
||||
if(!is_link($p)&&is_file($p)&&hash_file('sha256',$p)===$m['files'][$name]['after']) {
|
||||
if($before[$name]===null)unlink($p);else v4FeedbackWrite($p,$before[$name]);
|
||||
}
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
return ['status'=>'installed','backup'=>$backup,'filesChanged'=>$written];
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Execute only in Lihrenmoos Symcon. Installs code and readonly feedback mapping; never starts a trial.
|
||||
if(!function_exists('IPS_GetKernelVersion'))throw new RuntimeException('Nur im IP-Symcon-Skripteditor ausfuehren.');
|
||||
require_once __DIR__.'/file_installer.php';
|
||||
$manager=17004;$battery=44234;$locked=false;$changedMapping=false;$oldMapping=null;
|
||||
$report=['scope'=>'corrected_feedback_trial_disabled','startedAt'=>gmdate('c'),'actuatorPermissionGranted'=>false,'serverUpdateRequired'=>false];
|
||||
try {
|
||||
$hashes=json_decode((string)file_get_contents(__DIR__.'/PACKAGE_HASHES.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
foreach($hashes as$name=>$hash) {
|
||||
if(str_contains($name,'..')||str_starts_with($name,'/')||is_link(__DIR__.'/'.$name)||hash_file('sha256',__DIR__.'/'.$name)!==$hash)throw new RuntimeException('Vorbereitetes Paket veraendert.');
|
||||
}
|
||||
foreach([$manager=>'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',$battery=>'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}']as$id=>$module) {
|
||||
if(!IPS_InstanceExists($id)||IPS_GetInstance($id)['ModuleInfo']['ModuleID']!==$module)throw new RuntimeException('Modulzuordnung ungueltig.');
|
||||
if(IPS_HasChanges($id))throw new RuntimeException('Offene Aenderungen zuerst speichern oder verwerfen.');
|
||||
}
|
||||
$beforeM=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
|
||||
$beforeB=json_decode(IPS_GetConfiguration($battery),true,128,JSON_THROW_ON_ERROR);
|
||||
if(($beforeM['NetzfahrplanAktiv']??null)!==false || ($beforeM['NetzfahrplanV4RegeltestErlaubt']??false)!==false || ($beforeB['NetzfahrplanV4RegeltestErlaubt']??false)!==false)throw new RuntimeException('Alte Fahrplanausgabe und beide Regeltestfreigaben muessen AUS bleiben.');
|
||||
$c=json_decode((string)file_get_contents(__DIR__.'/feedback-config.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
$manifest=json_decode((string)file_get_contents(__DIR__.'/MANIFEST.json'),true,32,JSON_THROW_ON_ERROR);
|
||||
if(($c['managerId']??null)!==$manager || ($c['batteryInstanceId']??null)!==$battery || ($c['allowEstimatedForTrial']??null)!==false
|
||||
|| hash_file('sha256',__DIR__.'/feedback-config.json')!==$manifest['configurationSha256'])throw new RuntimeException('Falsche Rueckmeldekonfiguration.');
|
||||
$desired=json_encode($c,JSON_THROW_ON_ERROR|JSON_PRESERVE_ZERO_FRACTION);
|
||||
$oldMapping=$beforeB['NetzfahrplanV4RueckmeldungKonfiguration']??'{}';
|
||||
if(!in_array($oldMapping,['','{}',$desired],true))throw new RuntimeException('Abweichende Rueckmeldekonfiguration nicht ueberschrieben.');
|
||||
$controls=[];foreach(IPS_GetModuleList()as$module)if((IPS_GetModule($module)['Prefix']??'')==='MC')$controls=array_merge($controls,IPS_GetInstanceListByModuleID($module));
|
||||
if(count($controls)!==1||!function_exists('MC_ReloadModule'))throw new RuntimeException('Module Control nicht eindeutig.');
|
||||
$locked=IPS_SemaphoreEnter('ENELIX.V4.FeedbackInstall',3000);if(!$locked)throw new RuntimeException('Installation bereits aktiv.');
|
||||
$report['files']=v4FeedbackFiles(__DIR__,'/var/lib/symcon/modules/Enelix-EMS');
|
||||
if(MC_ReloadModule($controls[0],'Enelix-EMS')===false)throw new RuntimeException('Bibliotheks-Reload fehlgeschlagen.');
|
||||
$afterM=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
|
||||
$afterB=json_decode(IPS_GetConfiguration($battery),true,128,JSON_THROW_ON_ERROR);
|
||||
foreach([[$beforeM,$afterM],[$beforeB,$afterB]]as$pair)foreach($pair[0]as$key=>$value)if(!array_key_exists($key,$pair[1])||$pair[1][$key]!==$value)throw new RuntimeException('Bestehender Parameter beim Reload veraendert: '.$key);
|
||||
if(!array_key_exists('NetzfahrplanV4RueckmeldungKonfiguration',$afterB)||!array_key_exists('NetzfahrplanV4RegeltestErlaubt',$afterM)
|
||||
||!function_exists('ENELIX_GetV4BatterieRueckmeldung')||!function_exists('ENELIX_GetV4ManagerTestStatus')) {
|
||||
$report['status']='waiting_for_registration';
|
||||
echo "Dateien installiert. Modulregistrierung wird abgeschlossen; diesen Aufruf einmal wiederholen. Keine Stellfreigabe.\n";
|
||||
} else {
|
||||
if($afterM['NetzfahrplanV4RegeltestErlaubt']!==false||$afterB['NetzfahrplanV4RegeltestErlaubt']!==false)throw new RuntimeException('Unerwartete Testfreigabe.');
|
||||
if($afterB['NetzfahrplanV4RueckmeldungKonfiguration']!==$desired) {
|
||||
IPS_SetProperty($battery,'NetzfahrplanV4RueckmeldungKonfiguration',$desired);$changedMapping=true;IPS_ApplyChanges($battery);
|
||||
}
|
||||
$finalM=json_decode(IPS_GetConfiguration($manager),true,128,JSON_THROW_ON_ERROR);
|
||||
$finalB=json_decode(IPS_GetConfiguration($battery),true,128,JSON_THROW_ON_ERROR);
|
||||
foreach([[$beforeM,$finalM],[$beforeB,$finalB]]as$pair)foreach($pair[0]as$key=>$value)if($key!=='NetzfahrplanV4RueckmeldungKonfiguration'&&(!array_key_exists($key,$pair[1])||$pair[1][$key]!==$value))throw new RuntimeException('Bestehender Parameter unerwartet veraendert: '.$key);
|
||||
if(($finalM['NetzfahrplanAktiv']??null)!==false||($finalM['NetzfahrplanV4RegeltestErlaubt']??null)!==false||($finalB['NetzfahrplanV4RegeltestErlaubt']??null)!==false)throw new RuntimeException('Unerwartete Stellfreigabe nach Installation.');
|
||||
$f=json_decode(ENELIX_GetV4BatterieRueckmeldung($battery),true,32,JSON_THROW_ON_ERROR);
|
||||
$report['feedback']=array_intersect_key($f,array_flip(['status','kind','method','reason','reasons','estimated','batteryW','gridW','sourceOldestAt','checkedAt','configHash','usableForTrial','canDispatch']));
|
||||
$report['status']='corrected_feedback_installed_trial_disabled';$report['originalParametersPreserved']=true;
|
||||
echo "OK: Korrigierte physische Rueckmeldung mit Manager-Vorschau und Batterietreiber verbunden.\n";
|
||||
echo 'Rueckmeldung: '.($f['status']??'available').'; Modellanteil: '.(($f['estimated']??false)?'ja':'nein').".\n";
|
||||
if(isset($f['reason']))echo 'Aktueller Hinweis: '.$f['reason']."\n";
|
||||
echo "Beide Regeltestfreigaben AUS. Keine V4-Ausgabe aktiviert; vorhandene Regelung, Energiekonten und Datensender bleiben bestehen.\n";
|
||||
}
|
||||
} catch(Throwable $e) {
|
||||
$report['status']='needs_review';$report['reason']=substr($e->getMessage(),0,260);
|
||||
if($changedMapping && $oldMapping!==null) {
|
||||
try{IPS_SetProperty($battery,'NetzfahrplanV4RueckmeldungKonfiguration',$oldMapping);IPS_ApplyChanges($battery);$report['mappingRestored']=true;}catch(Throwable $ignored){$report['mappingRestored']=false;}
|
||||
}
|
||||
echo 'FEHLER: '.$report['reason']."\nKeine V4-Stellfreigabe erteilt.\n";
|
||||
} finally {
|
||||
$report['finishedAt']=gmdate('c');v4FeedbackWrite(__DIR__.'/INSTALL_RESULT.json',json_encode($report,JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT),0644);
|
||||
if($locked)IPS_SemaphoreLeave('ENELIX.V4.FeedbackInstall');
|
||||
echo 'FEEDBACK INSTALL REPORT: '.__DIR__."/INSTALL_RESULT.json\n";
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user