This commit is contained in:
@@ -0,0 +1,172 @@
|
|||||||
|
# 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
|
||||||
|
- 250-W-Grundraster mit 50-W-Feinwerten zwischen -200 W und +200 W
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
|
||||||
|
|
||||||
|
- Im PV-Betrieb steht oberhalb der Reserve das gesamte Raster zur Verfuegung.
|
||||||
|
- Unterhalb der Reserve wird die maximale Ladeleistung angeboten.
|
||||||
|
- Bei vollem Speicher werden nur Entladung und 0 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.
|
||||||
|
|
||||||
|
## 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 und Hysterese 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,229 @@
|
|||||||
|
{
|
||||||
|
"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": "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"
|
||||||
|
}
|
||||||
+1175
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}',
|
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}',
|
||||||
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
|
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
|
||||||
|
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
|
||||||
];
|
];
|
||||||
private const MONATSNAMEN = [
|
private const MONATSNAMEN = [
|
||||||
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ weiteren Module sind als Diskussionsentwürfe dokumentiert.
|
|||||||
## Geplante Module
|
## Geplante Module
|
||||||
|
|
||||||
- Manager (implementiert)
|
- Manager (implementiert)
|
||||||
- Batterie
|
- Batterie (implementiert)
|
||||||
- Wassererwaermer (implementiert)
|
- Wassererwaermer (implementiert)
|
||||||
- Pufferspeicher
|
- Pufferspeicher
|
||||||
- Verbraucher 1-Stufig (implementiert)
|
- Verbraucher 1-Stufig (implementiert)
|
||||||
@@ -62,11 +62,14 @@ der Uebernahme in einen Freigabebranch erfolgreich sein.
|
|||||||
|
|
||||||
- [Zentrale Liste offener Teamentscheidungen](docs/Offene-Punkte.md)
|
- [Zentrale Liste offener Teamentscheidungen](docs/Offene-Punkte.md)
|
||||||
- [Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md)
|
- [Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md)
|
||||||
|
- [Batterieschnittstelle](docs/Schnittstelle-Batterie.md)
|
||||||
- [Manager-Modul inklusive Lizenzierung](docs/module/Manager/README.md)
|
- [Manager-Modul inklusive Lizenzierung](docs/module/Manager/README.md)
|
||||||
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
||||||
|
- [Batteriemodul](docs/module/Batterie/README.md)
|
||||||
- [Verbraucher-1-Stufig-Modul](docs/module/Verbraucher-1-Stufig/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-Stand-Alone-Modul](docs/module/Ladestation-Stand-Alone/README.md)
|
||||||
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
||||||
- [Migration Verbraucher 1-Stufig](docs/migration/Verbraucher-1-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)
|
- [Obere Anschlüsse des Managers](docs/Obere-Anschluesse.md)
|
||||||
- [Modulübersicht](docs/module/README.md)
|
- [Modulübersicht](docs/module/README.md)
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# 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
|
||||||
|
Leistungsangebot enthalten sein. Waehren 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": "Peak",
|
||||||
|
"PrioritaetPV": 0,
|
||||||
|
"PrioritaetPeak": 0,
|
||||||
|
"Leistungswerte_W": [-1500],
|
||||||
|
"AenderungMoeglich": true,
|
||||||
|
"Verfuegbar": true,
|
||||||
|
"Istleistung_W": -250.0,
|
||||||
|
"Leistungsquelle": 2,
|
||||||
|
"Zustand": [
|
||||||
|
{
|
||||||
|
"Kennung": "Sollleistung_W",
|
||||||
|
"Art": "Sollwert",
|
||||||
|
"Wert": -1500,
|
||||||
|
"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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
@@ -132,6 +132,14 @@ Symcon-Datenpunkte registriert `VerbraucherBasisTrait`.
|
|||||||
`Leistungswerte_W`, `Betriebsart` und `Zustand` werden intern gehalten
|
`Leistungswerte_W`, `Betriebsart` und `Zustand` werden intern gehalten
|
||||||
und direkt in die Nachricht geschrieben.
|
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
|
## Easee-Gateway-Transport
|
||||||
|
|
||||||
Der technische JSON-Vertrag zwischen `EaseeGateway` und
|
Der technische JSON-Vertrag zwischen `EaseeGateway` und
|
||||||
|
|||||||
@@ -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
|
||||||
|
- 250-W-Grundraster und 50-W-Feinwerte um 0
|
||||||
|
- 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 |
|
||||||
|
| 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.
|
||||||
+109
-47
@@ -1,59 +1,121 @@
|
|||||||
# Batterie
|
# Batterie
|
||||||
|
|
||||||
> Status: Diskussionsentwurf. Positiv bedeutet Laden, negativ Entladen.
|
> Status: implementiert. Zielplattform ist IP-Symcon ab Version 8.0,
|
||||||
|
> Nachrichtenvertrag 4.0.
|
||||||
|
|
||||||
Die Auswahl des Batterietyps bestimmt den Adapter und die sichtbaren
|
## Verantwortung
|
||||||
Anschlussfelder. Ohne Logging bleibt von den zusätzlichen Variablen nur
|
|
||||||
`Ladestatus` sichtbar.
|
|
||||||
|
|
||||||
## Zusätzliche Variablen
|
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.
|
||||||
|
|
||||||
| Ident | Typ / Zugriff | Beschreibung |
|
Positive Leistung bedeutet Laden, negative Leistung Entladen. Die
|
||||||
| --- | --- | --- |
|
Geraeteanbindung erfolgt ausschliesslich ueber vom Benutzer ausgewaehlte
|
||||||
| `Ladestatus` | Integer / Anzeige | `0` unbekannt, `1` ruhend, `2` laden, `3` entladen. |
|
numerische IP-Symcon-Variablen. Schreibziele muessen eine Aktion besitzen.
|
||||||
| `Ladezustand` | Float / Logging | Physischer Ladezustand in %. |
|
|
||||||
| `LadezustandGueltig` | Boolean / Logging | Gültigkeit unabhängig von der Leistungsmessung. |
|
|
||||||
| `Steuerungsmodus` | Integer / lokal bedienbar | `0` Batterie, `1` Manager, `2` Virtuelle Batterie führt. |
|
|
||||||
| `NotnachladungAktiv` | Boolean / Logging | Aus Ladezustand ermittelter lokaler Bedarf. |
|
|
||||||
| `LadeleistungVerfuegbar` | Float / Logging | Aktuell nutzbare Ladeleistung in W. |
|
|
||||||
| `EntladeleistungVerfuegbar` | Float / Logging | Aktuell nutzbare Entladeleistung als positiver Betrag in W. |
|
|
||||||
| `MaxLadeleistung` | Float / lokal bedienbar | Wirksamer Parameter in W. |
|
|
||||||
| `MaxEntladeleistung` | Float / lokal bedienbar | Wirksamer Parameter in W. |
|
|
||||||
| `Mindestladezustand` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
|
||||||
| `Maximalladezustand` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
|
||||||
| `Nachladeschwelle` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
|
||||||
| `Nachladehysterese` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
|
||||||
| `Notnachladeleistung` | Float / lokal bedienbar | `0` deaktiviert; sonst Leistung in W. |
|
|
||||||
|
|
||||||
## Zusätzliche Properties
|
## Ereignismodell
|
||||||
|
|
||||||
| Ident | Typ | Standard / Beschreibung |
|
Fachliche Neuberechnungen werden ausgeloest durch:
|
||||||
| --- | --- | --- |
|
|
||||||
| `MaxLadeleistung` | Float | `0*` W; einzige einstellbare obere Ladegrenze. |
|
|
||||||
| `MaxEntladeleistung` | Float | `0` W; einzige einstellbare obere Entladegrenze. |
|
|
||||||
| `Mindestladezustand` | Float | `0*` %; Bereich 0–100. |
|
|
||||||
| `Maximalladezustand` | Float | `100` %; grösser als Mindestladezustand. |
|
|
||||||
| `Nachladeschwelle` | Float | `0*` %; zwischen Mindest- und Maximalladezustand. |
|
|
||||||
| `Nachladehysterese` | Float | `2` %; Schwelle plus Hysterese höchstens Maximalladezustand. |
|
|
||||||
| `Notnachladeleistung` | Float | `0` W; sonst höchstens MaxLadeleistung. |
|
|
||||||
| `Batterietyp` | Auswahl | `Unkonfiguriert`, `Herstellerunabhaengig`, `Goodwe`, `SolarEdge`, `SigEnergy`. |
|
|
||||||
| `BatterieInstanzID` | Integer | `0`; für kompatible Herstelleradapter. |
|
|
||||||
| `LadezustandVariableID` | Integer | `0`; nur herstellerunabhängig. |
|
|
||||||
| `IstleistungVariableID` | Integer | `0`; nur herstellerunabhängig. |
|
|
||||||
| `SollleistungVariableID` | Integer | `0`; nur herstellerunabhängig, vorzeichenbehaftet in W. |
|
|
||||||
| `LadezustandMaxAlter` | Integer | `60` s; Aktualität des Ladezustands. |
|
|
||||||
| `VirtuelleBatterieID` | Integer | `0`; Pflicht bei Steuerungsmodus 2. |
|
|
||||||
|
|
||||||
Anschlussart, Batteriespannung, Gerätezuordnung, Leistungsschritt,
|
- Aktualisierung eines der fuenf Messwerte
|
||||||
Sollwertquelle und externe Sollwerte entfallen.
|
- Managerdaten mit Betriebsart oder Sollleistung
|
||||||
|
- lokale Aenderung der Variablen Aktiv
|
||||||
|
- Ablauf von Vorgabe-Timeout oder Aenderungssperre
|
||||||
|
- manuelle Aktualisierung in der Konfigurationsmaske
|
||||||
|
|
||||||
## Zustand
|
Der Meldezyklus verschickt eine Vollmeldung und dient nicht als Regelzyklus.
|
||||||
|
Ein periodischer Berechnungstimer oder Idle-Counter existiert nicht.
|
||||||
|
|
||||||
`Ladezustand_Prozent` als Istwert; `Steuerungsmodus` und
|
## Properties
|
||||||
`NotnachladungAktiv` als Status; `LadezustandFehlt` und `Geraetefehler` als
|
|
||||||
Störungen.
|
|
||||||
|
|
||||||
## Offene Punkte
|
### Gemeinsame Verbraucherproperties
|
||||||
|
|
||||||
- Michis konkrete Fehlerliste nachliefern und gegen den Entwurf prüfen.
|
PrioritaetPV, PrioritaetPeak, Meldeintervall, VorgabeTimeout,
|
||||||
- Datenpunkte und Leistungsgrenzen der drei Herstelleradapter verifizieren.
|
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 |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
Das Raster besteht wie in Enelix 1 aus 250-W-Grundschritten. Um 0 werden
|
||||||
|
zusaetzlich -200, -150, -100, -50, 0, 50, 100, 150 und 200 W angeboten,
|
||||||
|
soweit diese Werte innerhalb der dynamischen Leistungsgrenzen liegen.
|
||||||
|
|
||||||
|
PV und Peak verwenden die bisherige SoC-Logik. 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: Raster, 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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# EMS-Module und Modulentwürfe
|
# EMS-Module und Modulentwürfe
|
||||||
|
|
||||||
> Manager, Warmwassererwaermer, Verbraucher 1-Stufig, Ladestation Stand-Alone,
|
> Manager, Warmwassererwaermer, Batterie, Verbraucher 1-Stufig, Ladestation Stand-Alone,
|
||||||
> Easee Gateway und Ladestation Gateway sind als installierbare IP-Symcon-Module
|
> Easee Gateway und Ladestation Gateway sind als installierbare IP-Symcon-Module
|
||||||
> umgesetzt. Die weiteren Ordner enthalten Besprechungsgrundlagen.
|
> umgesetzt. Die weiteren Ordner enthalten Besprechungsgrundlagen.
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ nur zusätzliche Properties, Variablen, Zustände und offene Punkte.
|
|||||||
| Modul | Rolle |
|
| Modul | Rolle |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [Manager](Manager/README.md) | Leistungsverteilung und Anschlüsse (implementiert) |
|
| [Manager](Manager/README.md) | Leistungsverteilung und Anschlüsse (implementiert) |
|
||||||
| [Batterie](Batterie/README.md) | Laden und Entladen eines Speichers |
|
| [Batterie](Batterie/README.md) | Laden und Entladen eines Speichers (implementiert) |
|
||||||
| [Wassererwärmer](Wassererwaermer/README.md) | Mehrstufige elektrische Erwärmung (implementiert) |
|
| [Wassererwärmer](Wassererwaermer/README.md) | Mehrstufige elektrische Erwärmung (implementiert) |
|
||||||
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
||||||
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher (implementiert) |
|
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher (implementiert) |
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class BatterieRegler
|
||||||
|
{
|
||||||
|
public const TYP_UNKONFIGURIERT = 0;
|
||||||
|
public const TYP_HERSTELLERUNABHAENGIG = 1;
|
||||||
|
public const TYP_GOODWE = 2;
|
||||||
|
public const TYP_SOLAREDGE = 3;
|
||||||
|
public const TYP_SIGENERGY = 4;
|
||||||
|
|
||||||
|
public const STEUERUNG_WECHSELRICHTER = 1;
|
||||||
|
public const STEUERUNG_ENELIX = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt das bisherige Raster: 250-W-Grundschritte und
|
||||||
|
* 50-W-Feinschritte im Bereich von -200 W bis +200 W.
|
||||||
|
*
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public static function leistungsraster(float $maxLadenW, float $maxEntladenW): array
|
||||||
|
{
|
||||||
|
self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW);
|
||||||
|
|
||||||
|
$maxLaden = (int) floor($maxLadenW / 50.0) * 50;
|
||||||
|
$maxEntladen = (int) floor($maxEntladenW / 50.0) * 50;
|
||||||
|
$werte = [0];
|
||||||
|
|
||||||
|
for ($leistung = -$maxEntladen; $leistung <= 0; $leistung += 250) {
|
||||||
|
$werte[] = $leistung;
|
||||||
|
}
|
||||||
|
for ($leistung = 0; $leistung <= $maxLaden; $leistung += 250) {
|
||||||
|
$werte[] = $leistung;
|
||||||
|
}
|
||||||
|
for ($leistung = -200; $leistung <= 200; $leistung += 50) {
|
||||||
|
if ($leistung >= -$maxEntladen && $leistung <= $maxLaden) {
|
||||||
|
$werte[] = $leistung;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$werte = array_values(array_unique($werte));
|
||||||
|
sort($werte, SORT_NUMERIC);
|
||||||
|
|
||||||
|
return $werte;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hystereseAktiv(
|
||||||
|
float $ladezustand,
|
||||||
|
float $reserve,
|
||||||
|
float $hysterese,
|
||||||
|
bool $bisherAktiv
|
||||||
|
): bool {
|
||||||
|
self::pruefeLadezustandsgrenzen($reserve, $hysterese);
|
||||||
|
|
||||||
|
if ($ladezustand > $reserve + $hysterese) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($ladezustand <= $reserve) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bisherAktiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bildet das zustands- und betriebsartabhaengige Angebot aus Enelix 1 ab.
|
||||||
|
*
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public static function leistungsangebot(
|
||||||
|
bool $aktiv,
|
||||||
|
bool $enelixSteuert,
|
||||||
|
bool $peakbetrieb,
|
||||||
|
float $ladezustand,
|
||||||
|
float $reserve,
|
||||||
|
float $minimum,
|
||||||
|
bool $hystereseAktiv,
|
||||||
|
float $netzleistungW,
|
||||||
|
float $istleistungW,
|
||||||
|
float $maxLadenW,
|
||||||
|
float $maxEntladenW
|
||||||
|
): array {
|
||||||
|
self::pruefeLadezustandsgrenzen($reserve, 0.0);
|
||||||
|
if ($minimum < 0.0 || $minimum > 100.0 || $minimum > $reserve) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Mindestladezustand muss zwischen 0 und der Reserve liegen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW);
|
||||||
|
|
||||||
|
if (!$aktiv || !$enelixSteuert) {
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$raster = self::leistungsraster($maxLadenW, $maxEntladenW);
|
||||||
|
if ($peakbetrieb) {
|
||||||
|
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
||||||
|
$ziel = $netzleistungW > 0.0
|
||||||
|
? -min($netzleistungW, $maxEntladenW) + $istleistungW
|
||||||
|
: $netzleistungW;
|
||||||
|
|
||||||
|
return [self::begrenzeLeistung($ziel, $maxLadenW, $maxEntladenW)];
|
||||||
|
}
|
||||||
|
if ($ladezustand > $reserve && $hystereseAktiv) {
|
||||||
|
return self::filtere($raster, static fn (int $wert): bool => $wert <= 0);
|
||||||
|
}
|
||||||
|
if ($ladezustand > $minimum) {
|
||||||
|
return $raster;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::filtere($raster, static fn (int $wert): bool => $wert >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ladezustand > 99.0) {
|
||||||
|
return self::filtere($raster, static fn (int $wert): bool => $wert <= 0);
|
||||||
|
}
|
||||||
|
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
||||||
|
return $raster;
|
||||||
|
}
|
||||||
|
if ($ladezustand >= $reserve && $hystereseAktiv) {
|
||||||
|
return self::filtere($raster, static fn (int $wert): bool => $wert >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(int) round($maxLadenW)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert nur die Registerwerte, die der gewaehlte Batterietyp benoetigt.
|
||||||
|
*
|
||||||
|
* @return array<string, int|float>
|
||||||
|
*/
|
||||||
|
public static function registerwerte(
|
||||||
|
int $batterietyp,
|
||||||
|
int $steuerung,
|
||||||
|
int $sollleistungW
|
||||||
|
): array {
|
||||||
|
self::pruefeBatterietyp($batterietyp);
|
||||||
|
self::pruefeSteuerung($steuerung);
|
||||||
|
|
||||||
|
$management = self::managementCode($batterietyp, $steuerung);
|
||||||
|
if ($steuerung === self::STEUERUNG_WECHSELRICHTER) {
|
||||||
|
if ($batterietyp === self::TYP_GOODWE) {
|
||||||
|
return ['Leistung' => 0, 'Management' => $management];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Ladeleistung' => 0,
|
||||||
|
'Entladeleistung' => 0,
|
||||||
|
'Management' => $management,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$betrag = abs($sollleistungW);
|
||||||
|
if ($batterietyp === self::TYP_HERSTELLERUNABHAENGIG) {
|
||||||
|
return [
|
||||||
|
'Ladeleistung' => $sollleistungW >= 0 ? $betrag : 0,
|
||||||
|
'Entladeleistung' => $sollleistungW < 0 ? $betrag : 0,
|
||||||
|
'Modus' => $sollleistungW >= 0 ? 0 : 1,
|
||||||
|
'Management' => $management,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($batterietyp === self::TYP_GOODWE) {
|
||||||
|
return [
|
||||||
|
'Leistung' => $betrag,
|
||||||
|
'Modus' => $sollleistungW >= 0 ? 11 : 12,
|
||||||
|
'Management' => $management,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($batterietyp === self::TYP_SOLAREDGE) {
|
||||||
|
return [
|
||||||
|
'Ladeleistung' => $sollleistungW >= 0 ? $betrag : 0,
|
||||||
|
'Entladeleistung' => $sollleistungW < 0 ? $betrag : 0,
|
||||||
|
'Modus' => $sollleistungW >= 0 ? 3 : 4,
|
||||||
|
'Management' => $management,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Ladeleistung' => $sollleistungW >= 0 ? $betrag / 1000.0 : 0.0,
|
||||||
|
'Entladeleistung' => $sollleistungW < 0 ? $betrag / 1000.0 : 0.0,
|
||||||
|
'Modus' => $sollleistungW >= 0 ? 3 : 6,
|
||||||
|
'Management' => $management,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public static function erforderlicheRegister(int $batterietyp): array
|
||||||
|
{
|
||||||
|
self::pruefeBatterietyp($batterietyp);
|
||||||
|
|
||||||
|
if ($batterietyp === self::TYP_GOODWE) {
|
||||||
|
return ['Management', 'Modus', 'Leistung'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['Management', 'Modus', 'Ladeleistung', 'Entladeleistung'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function managementCode(int $batterietyp, int $steuerung): int
|
||||||
|
{
|
||||||
|
if ($steuerung === self::STEUERUNG_WECHSELRICHTER) {
|
||||||
|
return in_array(
|
||||||
|
$batterietyp,
|
||||||
|
[self::TYP_GOODWE, self::TYP_SOLAREDGE],
|
||||||
|
true
|
||||||
|
) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($batterietyp === self::TYP_SOLAREDGE) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if ($batterietyp === self::TYP_GOODWE) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<int> $werte
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
private static function filtere(array $werte, callable $filter): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter($werte, $filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function begrenzeLeistung(
|
||||||
|
float $leistungW,
|
||||||
|
float $maxLadenW,
|
||||||
|
float $maxEntladenW
|
||||||
|
): int {
|
||||||
|
return (int) round(min($maxLadenW, max(-$maxEntladenW, $leistungW)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pruefeLeistungsgrenzen(float $maxLadenW, float $maxEntladenW): void
|
||||||
|
{
|
||||||
|
if ($maxLadenW < 0.0 || $maxEntladenW < 0.0) {
|
||||||
|
throw new InvalidArgumentException('Leistungsgrenzen duerfen nicht negativ sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pruefeLadezustandsgrenzen(float $reserve, float $hysterese): void
|
||||||
|
{
|
||||||
|
if ($reserve < 0.0 || $reserve > 100.0) {
|
||||||
|
throw new InvalidArgumentException('Reserve muss zwischen 0 und 100 Prozent liegen.');
|
||||||
|
}
|
||||||
|
if ($hysterese < 0.0 || $reserve + $hysterese > 100.0) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Hysterese muss positiv sein und mit der Reserve hoechstens 100 Prozent ergeben.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pruefeBatterietyp(int $batterietyp): void
|
||||||
|
{
|
||||||
|
if (!in_array($batterietyp, [
|
||||||
|
self::TYP_HERSTELLERUNABHAENGIG,
|
||||||
|
self::TYP_GOODWE,
|
||||||
|
self::TYP_SOLAREDGE,
|
||||||
|
self::TYP_SIGENERGY,
|
||||||
|
], true)) {
|
||||||
|
throw new InvalidArgumentException('Batterietyp ist nicht konfiguriert.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pruefeSteuerung(int $steuerung): void
|
||||||
|
{
|
||||||
|
if (!in_array($steuerung, [
|
||||||
|
self::STEUERUNG_WECHSELRICHTER,
|
||||||
|
self::STEUERUNG_ENELIX,
|
||||||
|
], true)) {
|
||||||
|
throw new InvalidArgumentException('Batteriesteuerung ist ungueltig.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class BatterieModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
private const MODULPFAD = __DIR__ . '/../Batterie/';
|
||||||
|
|
||||||
|
public function testModulIstVollstaendigUndMetadatenSindGueltig(): void
|
||||||
|
{
|
||||||
|
foreach (['module.json', 'form.json', 'module.php', 'README.md'] as $datei) {
|
||||||
|
self::assertFileExists(self::MODULPFAD . $datei);
|
||||||
|
}
|
||||||
|
$metadaten = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'module.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame('{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}', $metadaten['id']);
|
||||||
|
self::assertSame('Batterie', $metadaten['name']);
|
||||||
|
self::assertSame('Enelix', $metadaten['vendor']);
|
||||||
|
self::assertSame(3, $metadaten['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllePropertiesSindImFormularEnthalten(): void
|
||||||
|
{
|
||||||
|
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
preg_match_all(
|
||||||
|
'/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/',
|
||||||
|
$basis . $modul,
|
||||||
|
$treffer
|
||||||
|
);
|
||||||
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
foreach (array_values(array_unique($treffer[1])) as $property) {
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'"name": "' . $property . '"',
|
||||||
|
$formular,
|
||||||
|
$property
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModulIstEreignisbasiertUndVerwendetVertrag40(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
|
||||||
|
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
|
||||||
|
self::assertStringContainsString('public function MessageSink(', $modul);
|
||||||
|
self::assertStringContainsString('RegisterMessage($neueID, self::VM_UPDATE)', $modul);
|
||||||
|
self::assertStringContainsString("'Betriebsart' =>", $modul);
|
||||||
|
self::assertStringNotContainsString('Timer_Do_UserCalc_Battery', $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterTimer('Regelzyklus'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegisterWerdenUeberAusgewaehlteAktionsvariablenGeschrieben(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'RequestAction($this->ReadPropertyInteger($property), $wert)',
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('FormBatterietyp', $modul);
|
||||||
|
self::assertStringContainsString('UpdateFormField', $modul);
|
||||||
|
foreach ([
|
||||||
|
'ManagementRegisterVariableID',
|
||||||
|
'ModusRegisterVariableID',
|
||||||
|
'LeistungsRegisterVariableID',
|
||||||
|
'LadeleistungRegisterVariableID',
|
||||||
|
'EntladeleistungRegisterVariableID',
|
||||||
|
] as $property) {
|
||||||
|
self::assertStringContainsString('"name": "' . $property . '"', $formular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoggingDiagnoseUndAktionsbuttonsSindVorhanden(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
||||||
|
self::assertStringContainsString('"name": "DiagnosevariablenAnzeigen"', $formular);
|
||||||
|
self::assertStringContainsString('Messwerte und Angebot aktualisieren', $formular);
|
||||||
|
self::assertStringContainsString('Register auf sicheren Zustand setzen', $formular);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testManagerKenntDieBatterieModulIDGenauEinmal(): void
|
||||||
|
{
|
||||||
|
$manager = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
1,
|
||||||
|
substr_count($manager, '{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\BatterieRegler;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class BatterieReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testLeistungsrasterBehaeltGrobeUndFeineSchritte(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
-1000, -750, -500, -250, -200, -150, -100, -50,
|
||||||
|
0, 50, 100, 150, 200, 250, 500, 750, 1000,
|
||||||
|
],
|
||||||
|
BatterieRegler::leistungsraster(1000.0, 1000.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHystereseSchaltetAnDenBisherigenGrenzen(): void
|
||||||
|
{
|
||||||
|
self::assertTrue(BatterieRegler::hystereseAktiv(20.0, 20.0, 2.0, false));
|
||||||
|
self::assertTrue(BatterieRegler::hystereseAktiv(21.0, 20.0, 2.0, true));
|
||||||
|
self::assertFalse(BatterieRegler::hystereseAktiv(22.1, 20.0, 2.0, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPvAngebotEntsprichtDemBisherigenSocVerhalten(): void
|
||||||
|
{
|
||||||
|
$voll = BatterieRegler::leistungsraster(1000.0, 1000.0);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
$voll,
|
||||||
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, true, false, 50.0, 20.0, 10.0, false,
|
||||||
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[1000],
|
||||||
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, true, false, 19.0, 20.0, 10.0, true,
|
||||||
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[-1000, -750, -500, -250, -200, -150, -100, -50, 0],
|
||||||
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, true, false, 100.0, 20.0, 10.0, false,
|
||||||
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPeakAngebotBeruecksichtigtNetzUndAktuelleBatterieleistung(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[-500],
|
||||||
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, true, true, 50.0, 20.0, 10.0, false,
|
||||||
|
700.0, 200.0, 1000.0, 1000.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWechselrichtersteuerungBietetNurNull(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, false, false, 50.0, 20.0, 10.0, false,
|
||||||
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHerstellerunabhaengigeRegisterwerte(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Ladeleistung' => 1500,
|
||||||
|
'Entladeleistung' => 0,
|
||||||
|
'Modus' => 0,
|
||||||
|
'Management' => 1,
|
||||||
|
],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_HERSTELLERUNABHAENGIG,
|
||||||
|
BatterieRegler::STEUERUNG_ENELIX,
|
||||||
|
1500
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Ladeleistung' => 0,
|
||||||
|
'Entladeleistung' => 1500,
|
||||||
|
'Modus' => 1,
|
||||||
|
'Management' => 1,
|
||||||
|
],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_HERSTELLERUNABHAENGIG,
|
||||||
|
BatterieRegler::STEUERUNG_ENELIX,
|
||||||
|
-1500
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGoodweRegisterwerte(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
['Leistung' => 750, 'Modus' => 11, 'Management' => 2],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_GOODWE,
|
||||||
|
BatterieRegler::STEUERUNG_ENELIX,
|
||||||
|
750
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
['Leistung' => 750, 'Modus' => 12, 'Management' => 2],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_GOODWE,
|
||||||
|
BatterieRegler::STEUERUNG_ENELIX,
|
||||||
|
-750
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
['Leistung' => 0, 'Management' => 1],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_GOODWE,
|
||||||
|
BatterieRegler::STEUERUNG_WECHSELRICHTER,
|
||||||
|
-750
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSolarEdgeRegisterwerte(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Ladeleistung' => 0,
|
||||||
|
'Entladeleistung' => 500,
|
||||||
|
'Modus' => 4,
|
||||||
|
'Management' => 4,
|
||||||
|
],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_SOLAREDGE,
|
||||||
|
BatterieRegler::STEUERUNG_ENELIX,
|
||||||
|
-500
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSigenergySkaliertLeistungAufKilowatt(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Ladeleistung' => 1.5,
|
||||||
|
'Entladeleistung' => 0.0,
|
||||||
|
'Modus' => 3,
|
||||||
|
'Management' => 1,
|
||||||
|
],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_SIGENERGY,
|
||||||
|
BatterieRegler::STEUERUNG_ENELIX,
|
||||||
|
1500
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Ladeleistung' => 0.0,
|
||||||
|
'Entladeleistung' => 1.5,
|
||||||
|
'Modus' => 6,
|
||||||
|
'Management' => 1,
|
||||||
|
],
|
||||||
|
BatterieRegler::registerwerte(
|
||||||
|
BatterieRegler::TYP_SIGENERGY,
|
||||||
|
BatterieRegler::STEUERUNG_ENELIX,
|
||||||
|
-1500
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUngueltigeGrenzenWerdenAbgewiesen(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
BatterieRegler::leistungsraster(-1.0, 1000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ final class DokumentationsstrukturTest extends TestCase
|
|||||||
public function modulReadmes(): array
|
public function modulReadmes(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['Batterie'],
|
|
||||||
['Waermepumpe'],
|
['Waermepumpe'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -29,6 +28,17 @@ final class DokumentationsstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString('Properties', $inhalt);
|
self::assertStringContainsString('Properties', $inhalt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBatterieDokumentiertDenImplementiertenStand(): void
|
||||||
|
{
|
||||||
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Batterie/README.md');
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: implementiert', $inhalt);
|
||||||
|
self::assertStringContainsString('Properties', $inhalt);
|
||||||
|
self::assertStringContainsString('Registeradapter', $inhalt);
|
||||||
|
self::assertFileExists(__DIR__ . '/../docs/Schnittstelle-Batterie.md');
|
||||||
|
self::assertFileExists(__DIR__ . '/../docs/migration/Batterie.md');
|
||||||
|
}
|
||||||
|
|
||||||
public function testPufferspeicherDokumentiertDenImplementiertenStand(): void
|
public function testPufferspeicherDokumentiertDenImplementiertenStand(): void
|
||||||
{
|
{
|
||||||
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Pufferspeicher/README.md');
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Pufferspeicher/README.md');
|
||||||
|
|||||||
@@ -3,6 +3,16 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'Batterie' => [
|
||||||
|
'file' => __DIR__ . '/modules/Batterie.php',
|
||||||
|
'paths' => [
|
||||||
|
'Batterie/*',
|
||||||
|
'libs/BatterieRegler.php',
|
||||||
|
'libs/VerbraucherBasisTrait.php',
|
||||||
|
'libs/VerbraucherSchnittstelle.php',
|
||||||
|
'libs/Nachrichtenvertrag.php',
|
||||||
|
],
|
||||||
|
],
|
||||||
'EaseeGateway' => [
|
'EaseeGateway' => [
|
||||||
'file' => __DIR__ . '/modules/EaseeGateway.php',
|
'file' => __DIR__ . '/modules/EaseeGateway.php',
|
||||||
'paths' => [
|
'paths' => [
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\SymconTest\TestContext;
|
||||||
|
|
||||||
|
return static function (TestContext $test): void {
|
||||||
|
$numerischesRegister = static function (
|
||||||
|
TestContext $test,
|
||||||
|
string $name,
|
||||||
|
int $typ = 2
|
||||||
|
): int {
|
||||||
|
$variableID = $test->createVariable(
|
||||||
|
$typ,
|
||||||
|
$name,
|
||||||
|
$typ === 1 ? 0 : 0.0
|
||||||
|
);
|
||||||
|
$skriptID = IPS_CreateScript(0);
|
||||||
|
IPS_SetParent($skriptID, $test->rootId());
|
||||||
|
IPS_SetName($skriptID, $name . ' Aktion');
|
||||||
|
IPS_SetHidden($skriptID, true);
|
||||||
|
IPS_SetScriptContent($skriptID, <<<'PHP'
|
||||||
|
<?php
|
||||||
|
|
||||||
|
SetValue((int) $_IPS['VARIABLE'], $_IPS['VALUE']);
|
||||||
|
PHP
|
||||||
|
);
|
||||||
|
IPS_SetVariableCustomAction($variableID, $skriptID);
|
||||||
|
|
||||||
|
return $variableID;
|
||||||
|
};
|
||||||
|
|
||||||
|
$maxLadenID = $test->createVariable(2, 'Max Laden', 2000.0);
|
||||||
|
$maxEntladenID = $test->createVariable(2, 'Max Entladen', 2000.0);
|
||||||
|
$ladezustandID = $test->createVariable(2, 'Ladezustand', 50.0);
|
||||||
|
$netzleistungID = $test->createVariable(2, 'Netzleistung', 0.0);
|
||||||
|
$istleistungID = $test->createVariable(2, 'Batterieleistung', 0.0);
|
||||||
|
$managementID = $numerischesRegister($test, 'Management', 1);
|
||||||
|
$modusID = $numerischesRegister($test, 'Modus', 1);
|
||||||
|
$leistungID = $numerischesRegister($test, 'Leistung', 2);
|
||||||
|
|
||||||
|
$batterieID = $test->createInstance(
|
||||||
|
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
|
||||||
|
'Batterie GoodWe',
|
||||||
|
[
|
||||||
|
'Batterietyp' => 2,
|
||||||
|
'Batteriemanagement' => 2,
|
||||||
|
'MaxLadeleistungVariableID' => $maxLadenID,
|
||||||
|
'MaxEntladeleistungVariableID' => $maxEntladenID,
|
||||||
|
'LadezustandVariableID' => $ladezustandID,
|
||||||
|
'NetzleistungVariableID' => $netzleistungID,
|
||||||
|
'IstleistungVariableID' => $istleistungID,
|
||||||
|
'ManagementRegisterVariableID' => $managementID,
|
||||||
|
'ModusRegisterVariableID' => $modusID,
|
||||||
|
'LeistungsRegisterVariableID' => $leistungID,
|
||||||
|
'ReserveLadezustand' => 20.0,
|
||||||
|
'MindestLadezustand' => 10.0,
|
||||||
|
'LadezustandHysterese' => 2.0,
|
||||||
|
'MesswertMaxAlter' => 120,
|
||||||
|
'Aenderungssperre' => 0,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$managerID = $test->createInstance(
|
||||||
|
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||||
|
'Hilfsmanager Batterie',
|
||||||
|
[
|
||||||
|
'AutomatischeSuche' => false,
|
||||||
|
'VerbraucherZuordnung' => json_encode([
|
||||||
|
['InstanzID' => $batterieID, 'Aktiv' => true],
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$managerdatenSenden = static function (
|
||||||
|
string $betriebsart,
|
||||||
|
?int $leistung
|
||||||
|
) use ($managerID, $batterieID): void {
|
||||||
|
IPS_RequestAction($batterieID, 'ManagerdatenEmpfangen', json_encode([
|
||||||
|
'Kopf' => [
|
||||||
|
'Version' => '4.0',
|
||||||
|
'AbsenderID' => $managerID,
|
||||||
|
'EmpfaengerID' => $batterieID,
|
||||||
|
'Zeitpunkt' => time(),
|
||||||
|
],
|
||||||
|
'Betriebsart' => $betriebsart,
|
||||||
|
'Sollleistung_W' => $leistung,
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
};
|
||||||
|
|
||||||
|
$test->runCase(
|
||||||
|
'Instanz und Diagnosevariablen werden angelegt',
|
||||||
|
static function (TestContext $test) use ($batterieID): void {
|
||||||
|
$test->assertInstanceStatus($batterieID);
|
||||||
|
foreach ([
|
||||||
|
'Aktiv',
|
||||||
|
'Ladestatus',
|
||||||
|
'Istleistung',
|
||||||
|
'Sollleistung',
|
||||||
|
'Ladezustand',
|
||||||
|
'LadezustandGueltig',
|
||||||
|
'HystereseAktiv',
|
||||||
|
'LeistungsangebotDiagnose',
|
||||||
|
'LetzterRegisterbefehl',
|
||||||
|
] as $ident) {
|
||||||
|
$test->objectByIdent($ident, $batterieID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$test->runCase(
|
||||||
|
'GoodWe-Laden schreibt Leistung Modus und Management direkt',
|
||||||
|
static function (TestContext $test) use (
|
||||||
|
$batterieID,
|
||||||
|
$managerdatenSenden,
|
||||||
|
$managementID,
|
||||||
|
$modusID,
|
||||||
|
$leistungID
|
||||||
|
): void {
|
||||||
|
IPS_RequestAction($batterieID, 'Aktiv', true);
|
||||||
|
$managerdatenSenden('PV', null);
|
||||||
|
$managerdatenSenden('PV', 500);
|
||||||
|
|
||||||
|
$test->waitUntil(
|
||||||
|
static fn (): bool => (float) GetValue($leistungID) === 500.0
|
||||||
|
&& (int) GetValue($modusID) === 11
|
||||||
|
&& (int) GetValue($managementID) === 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$test->runCase(
|
||||||
|
'GoodWe-Entladen verwendet negativen Managerwert und Modus 12',
|
||||||
|
static function (TestContext $test) use (
|
||||||
|
$managerdatenSenden,
|
||||||
|
$modusID,
|
||||||
|
$leistungID
|
||||||
|
): void {
|
||||||
|
$managerdatenSenden('PV', -500);
|
||||||
|
$test->waitUntil(
|
||||||
|
static fn (): bool => (float) GetValue($leistungID) === 500.0
|
||||||
|
&& (int) GetValue($modusID) === 12
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$test->runCase(
|
||||||
|
'Istleistungsereignis aktualisiert den Ladestatus ohne Regelzyklus',
|
||||||
|
static function (TestContext $test) use (
|
||||||
|
$batterieID,
|
||||||
|
$istleistungID
|
||||||
|
): void {
|
||||||
|
SetValue($istleistungID, -250.0);
|
||||||
|
$ladestatusID = $test->objectByIdent('Ladestatus', $batterieID);
|
||||||
|
$test->waitUntil(
|
||||||
|
static fn (): bool => (int) GetValue($ladestatusID) === 3
|
||||||
|
);
|
||||||
|
|
||||||
|
SetValue($istleistungID, 250.0);
|
||||||
|
$test->waitUntil(
|
||||||
|
static fn (): bool => (int) GetValue($ladestatusID) === 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$test->runCase(
|
||||||
|
'Wechselrichtersteuerung setzt Leistung auf null und Automatikmodus',
|
||||||
|
static function (TestContext $test) use (
|
||||||
|
$batterieID,
|
||||||
|
$managementID,
|
||||||
|
$leistungID
|
||||||
|
): void {
|
||||||
|
$test->configureInstance($batterieID, ['Batteriemanagement' => 1]);
|
||||||
|
$test->waitUntil(
|
||||||
|
static fn (): bool => (float) GetValue($leistungID) === 0.0
|
||||||
|
&& (int) GetValue($managementID) === 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -59,6 +59,7 @@ final class SymconTestContractTest extends TestCase
|
|||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
|
'Batterie',
|
||||||
'LadestationGateway',
|
'LadestationGateway',
|
||||||
'LadestationStandAlone',
|
'LadestationStandAlone',
|
||||||
'Manager',
|
'Manager',
|
||||||
@@ -82,6 +83,7 @@ final class SymconTestContractTest extends TestCase
|
|||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
|
'Batterie',
|
||||||
'EaseeGateway',
|
'EaseeGateway',
|
||||||
'LadestationGateway',
|
'LadestationGateway',
|
||||||
'LadestationStandAlone',
|
'LadestationStandAlone',
|
||||||
|
|||||||
Reference in New Issue
Block a user