Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18c34b32e7 | ||
|
|
b111a5c501 | ||
|
|
ef708564d8 | ||
|
|
cf2ce3aff4 | ||
|
|
708a8ecea2 | ||
|
|
40f41a2fb8 | ||
|
|
2d24ce0900 | ||
|
|
f47c908798 | ||
|
|
72d96aeb81 | ||
|
|
dcf51bd8be | ||
|
|
efa8fe698c | ||
|
|
f250dea59a | ||
|
|
1e6481330a | ||
|
|
dbd728dd03 |
@@ -21,6 +21,12 @@ jobs:
|
|||||||
- name: Abhaengigkeiten installieren
|
- name: Abhaengigkeiten installieren
|
||||||
run: composer install --no-interaction --no-progress --prefer-dist
|
run: composer install --no-interaction --no-progress --prefer-dist
|
||||||
|
|
||||||
|
- name: Composer-Metadaten pruefen
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Abhaengigkeiten auf Sicherheitsmeldungen pruefen
|
||||||
|
run: composer audit --no-interaction
|
||||||
|
|
||||||
- name: Unit-Tests und Syntaxpruefung ausfuehren
|
- name: Unit-Tests und Syntaxpruefung ausfuehren
|
||||||
run: composer check
|
run: composer check
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -8,7 +8,7 @@ IP-Symcon-Registervariablen.
|
|||||||
## Funktionen
|
## Funktionen
|
||||||
|
|
||||||
- positive Leistung bedeutet Laden, negative Leistung bedeutet Entladen
|
- positive Leistung bedeutet Laden, negative Leistung bedeutet Entladen
|
||||||
- 250-W-Grundraster mit 50-W-Feinwerten zwischen -200 W und +200 W
|
- stufenlose Leistungsbereiche in ganzen Watt innerhalb der dynamischen Grenzen
|
||||||
- unterschiedliche Leistungsangebote fuer PV- und Peakbetrieb
|
- unterschiedliche Leistungsangebote fuer PV- und Peakbetrieb
|
||||||
- Reserve- und Mindestladezustand mit konfigurierbarer Hysterese
|
- Reserve- und Mindestladezustand mit konfigurierbarer Hysterese
|
||||||
- direkte Registeransteuerung fuer herstellerunabhaengige Batterien, GoodWe,
|
- direkte Registeransteuerung fuer herstellerunabhaengige Batterien, GoodWe,
|
||||||
@@ -86,9 +86,11 @@ LadezustandHysterese ersetzt die bisher fest im Code hinterlegte
|
|||||||
|
|
||||||
Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
|
Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
|
||||||
|
|
||||||
- Im PV-Betrieb steht oberhalb der Reserve das gesamte Raster zur Verfuegung.
|
- Im PV-Betrieb steht oberhalb der Reserve der durchgaengige Bereich von der
|
||||||
- Unterhalb der Reserve wird die maximale Ladeleistung angeboten.
|
maximalen Entlade- bis zur maximalen Ladeleistung zur Verfuegung.
|
||||||
- Bei vollem Speicher werden nur Entladung und 0 angeboten.
|
- Unterhalb der Reserve wird die maximale Ladeleistung 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
|
- Im Peakbetrieb wird oberhalb der Reserve der aus Netz- und aktueller
|
||||||
Batterieleistung berechnete Entladewert angeboten.
|
Batterieleistung berechnete Entladewert angeboten.
|
||||||
- Innerhalb der Reserve-Hysterese und unterhalb des Mindestladezustands werden
|
- Innerhalb der Reserve-Hysterese und unterhalb des Mindestladezustands werden
|
||||||
|
|||||||
+10
-9
@@ -293,10 +293,9 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$sollleistung = $daten['Sollleistung_W'];
|
$sollleistung = $daten['Sollleistung_W'];
|
||||||
if ($sollleistung === null) {
|
if ($sollleistung === null) {
|
||||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||||
&& !in_array(
|
&& !Nachrichtenvertrag::enthaeltLeistungswert(
|
||||||
(int) $this->leseZustand('Sollleistung'),
|
|
||||||
$this->leseLeistungsangebot(),
|
$this->leseLeistungsangebot(),
|
||||||
true
|
(int) $this->leseZustand('Sollleistung')
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
$this->setzeZustand('SollwertGueltig', false);
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
@@ -306,7 +305,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
|
if (!Nachrichtenvertrag::enthaeltLeistungswert(
|
||||||
|
$this->leseLeistungsangebot(),
|
||||||
|
$sollleistung
|
||||||
|
)) {
|
||||||
throw new InvalidArgumentException(
|
throw new InvalidArgumentException(
|
||||||
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
||||||
);
|
);
|
||||||
@@ -394,10 +396,9 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||||
&& !in_array(
|
&& !Nachrichtenvertrag::enthaeltLeistungswert(
|
||||||
(int) $this->leseZustand('Sollleistung'),
|
|
||||||
$angebot,
|
$angebot,
|
||||||
true
|
(int) $this->leseZustand('Sollleistung')
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
$this->setzeZustand('SollwertGueltig', false);
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
@@ -1073,7 +1074,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return list<int> */
|
/** @return list<int|array{Von_W: int, Bis_W: int}> */
|
||||||
private function leseLeistungsangebot(): array
|
private function leseLeistungsangebot(): array
|
||||||
{
|
{
|
||||||
$angebot = json_decode(
|
$angebot = json_decode(
|
||||||
@@ -1086,7 +1087,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
return [0];
|
return [0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values(array_map('intval', $angebot));
|
return array_values($angebot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setzeStoerung(string $text): void
|
private function setzeStoerung(string $text): void
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Stufenlose Leistungsbereiche fuer Batteriespeicher.
|
||||||
|
- Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert.
|
||||||
|
- 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.
|
||||||
@@ -53,7 +53,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$this->RegisterPropertyInteger('Betriebsmodus', EaseeLadestatus::BETRIEBSMODUS_EASEE);
|
$this->RegisterPropertyInteger('Betriebsmodus', EaseeLadestatus::BETRIEBSMODUS_EASEE);
|
||||||
$this->RegisterPropertyString('Ladestationskennung', '');
|
$this->RegisterPropertyString('Ladestationskennung', '');
|
||||||
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
|
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
|
||||||
$this->RegisterPropertyBoolean('Ladefreigabe', false);
|
$this->RegisterPropertyBoolean('Ladefreigabe', true);
|
||||||
$this->RegisterPropertyBoolean('Solarladen', true);
|
$this->RegisterPropertyBoolean('Solarladen', true);
|
||||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$this->RegisterPropertyString('Passwort', '');
|
$this->RegisterPropertyString('Passwort', '');
|
||||||
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
|
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
|
||||||
$this->RegisterPropertyInteger('Abfrageintervall', 5);
|
$this->RegisterPropertyInteger('Abfrageintervall', 5);
|
||||||
$this->RegisterPropertyBoolean('Ladefreigabe', false);
|
$this->RegisterPropertyBoolean('Ladefreigabe', true);
|
||||||
$this->RegisterPropertyBoolean('Solarladen', true);
|
$this->RegisterPropertyBoolean('Solarladen', true);
|
||||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||||
|
|
||||||
|
|||||||
+528
-6
@@ -127,6 +127,24 @@
|
|||||||
],
|
],
|
||||||
"values": []
|
"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",
|
"type": "CheckBox",
|
||||||
"name": "AutomatischeSuche",
|
"name": "AutomatischeSuche",
|
||||||
@@ -206,6 +224,379 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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": "List",
|
||||||
|
"name": "AnlagenWechselrichter",
|
||||||
|
"caption": "Wechselrichter",
|
||||||
|
"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": "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",
|
"type": "ExpansionPanel",
|
||||||
"caption": "Erweiterte Einstellungen",
|
"caption": "Erweiterte Einstellungen",
|
||||||
@@ -258,14 +649,133 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"onChange": "IPS_RequestAction($id, 'FormEnergyPieAnzeigen', $EnergyPieAnzeigen);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "EnergiediagrammeAnzeigen",
|
||||||
|
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen",
|
||||||
|
"onChange": "IPS_RequestAction($id, 'FormEnergiediagrammeAnzeigen', $EnergiediagrammeAnzeigen);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "FunFactsAnzeigen",
|
||||||
|
"caption": "Energy Facts unter dem Manager anzeigen",
|
||||||
|
"onChange": "IPS_RequestAction($id, 'FormFunFactsAnzeigen', $FunFactsAnzeigen);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "EnergieflussAnzeigen",
|
||||||
|
"caption": "Energiefluss unter dem Manager anzeigen",
|
||||||
|
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "ExpansionPanel",
|
"type": "ExpansionPanel",
|
||||||
"caption": "Prognose / Forecast",
|
"caption": "Prognose / Forecast",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "ValidationTextBox",
|
"type": "CheckBox",
|
||||||
"name": "PrognoseAnschluss",
|
"name": "PrognoseAktiv",
|
||||||
"caption": "Konfiguration als JSON"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -285,9 +795,21 @@
|
|||||||
"caption": "Stoerueberwachung",
|
"caption": "Stoerueberwachung",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "ValidationTextBox",
|
"type": "CheckBox",
|
||||||
"name": "StoermeldeAnschluss",
|
"name": "StoerueberwachungAktiv",
|
||||||
"caption": "Konfiguration als JSON"
|
"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."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1594
-39
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,10 @@ Energiemanagement, Manager und steuerbare Verbraucher fuer IP-Symcon.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Das Repository befindet sich im Aufbau. Der gemeinsame Nachrichtenvertrag, die
|
Version 0.1 Build 1 ist die erste Beta fuer kontrollierte Anlagen- und
|
||||||
Verbraucherbasis, der Manager, der mehrstufige Warmwassererwaermer, der
|
Feldtests. Der gemeinsame Nachrichtenvertrag, die Verbraucherbasis, der Manager
|
||||||
Pufferspeicher, die Batterie, der 1-stufige Verbraucher, die Ladestation
|
und alle unten aufgefuehrten Module sind implementiert. Produktiver Einsatz
|
||||||
Stand-Alone sowie das Easee Gateway mit Ladestation Gateway sind implementiert. Die weiteren
|
setzt eine anlagenspezifische Pruefung, Sicherung und Rueckfallplanung voraus.
|
||||||
Module sind als Diskussionsentwuerfe dokumentiert.
|
|
||||||
|
|
||||||
## Module
|
## Module
|
||||||
|
|
||||||
@@ -98,10 +97,12 @@ der Uebernahme in einen Freigabebranch erfolgreich sein.
|
|||||||
|
|
||||||
## Dokumentation
|
## Dokumentation
|
||||||
|
|
||||||
|
- [Changelog](CHANGELOG.md)
|
||||||
- [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)
|
- [Batterieschnittstelle](docs/Schnittstelle-Batterie.md)
|
||||||
- [ADR 0004: Betriebsartabhaengige Leistungsangebote](docs/adr/0004-betriebsartabhaengige-leistungsangebote.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)
|
- [Manager-Modul inklusive Lizenzierung](docs/module/Manager/README.md)
|
||||||
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
||||||
- [Pufferspeicher-Modul](docs/module/Pufferspeicher/README.md)
|
- [Pufferspeicher-Modul](docs/module/Pufferspeicher/README.md)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "belevo/enelix-ems",
|
"name": "belevo/enelix-ems",
|
||||||
"description": "Energiemanagement und steuerbare Verbraucher fuer IP-Symcon",
|
"description": "Energiemanagement und steuerbare Verbraucher fuer IP-Symcon",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
|
"license": "proprietary",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.0"
|
"php": ">=8.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+47
-21
@@ -1,34 +1,60 @@
|
|||||||
# Obere Anschlüsse des Managers
|
# Obere Anschlüsse des Managers
|
||||||
|
|
||||||
> Status: Lizenzierung implementiert; die übrigen Anbieterformate sind noch
|
> Status: Lizenzierung, Prognose und Stoerueberwachung sind an
|
||||||
> nicht festgelegt.
|
> `license.enelix.ch` angebunden. SDL/VGT bleibt als optionaler Anschluss
|
||||||
|
> vorbereitet.
|
||||||
|
|
||||||
Prognose, SDL/VGT und Stoerueberwachung sind optional. Die Lizenzierung ist fuer
|
Die Anschluesse werden ausschliesslich im Manager konfiguriert. Sie erzeugen
|
||||||
den Betrieb des Managers erforderlich. Sie werden im Manager konfiguriert und erzeugen
|
keine Abhaengigkeit zwischen Enelix EMS und Enelix Utils.
|
||||||
keine Abhängigkeit zwischen Enelix EMS und Enelix Utils.
|
|
||||||
|
|
||||||
| Anschluss | Aufgabe | Gegenstelle |
|
| Anschluss | Aufgabe | Gegenstelle |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| SDL/VGT | Zeitlich gültige Leistungsaufträge und Rückmeldungen | Optional das unabhängige Utils-Modul VGT-Schnittstelle |
|
| SDL/VGT | Zeitlich gueltige Leistungsauftraege und Rueckmeldungen | Optional das unabhaengige Utils-Modul VGT-Schnittstelle |
|
||||||
| Prognose / Forecast | Vorhersage für Produktion und bei Bedarf Verbrauch | Externer Anbieter; kein Repo-Modul |
|
| 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` |
|
| Lizenzierung | Pruefung freigeschalteter Manager-Funktionen | `POST https://license.enelix.ch/api/v1/licenses/activate` |
|
||||||
| Störüberwachung | Weitergabe von Zuständen und Störungen | Externer Empfänger; kein Repo-Modul |
|
| Stoerueberwachung | Vollsnapshot aktiver Manager- und Geraetestoerungen | `PUT https://license.enelix.ch/api/v1/installations/{id}/faults` |
|
||||||
|
|
||||||
## Gemeinsame Grundsätze
|
## Stoerungsdatenfluss
|
||||||
|
|
||||||
- Die Auswahl der Gegenstelle erfolgt ausschliesslich im Manager.
|
1. Jedes Geraetemodul meldet seinen Zustand ueber den bestehenden
|
||||||
- Anbieterformate werden an diesem Anschluss übersetzt und gelangen nicht in
|
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.
|
die Verbraucher-Schnittstelle.
|
||||||
- Jeder Anschluss meldet im Manager `NichtVerwendet`, `Verbunden` oder `Fehler`.
|
- Jeder Anschluss meldet `NichtVerwendet`, `Wartet`, `Verbunden` oder
|
||||||
- Fehlende optionale Anschlüsse dürfen die lokale EMS-Grundfunktion nicht
|
`Fehler`.
|
||||||
|
- Fehlende optionale Anschluesse duerfen die lokale EMS-Grundfunktion nicht
|
||||||
blockieren.
|
blockieren.
|
||||||
- Eine fehlende oder abgelaufene Lizenz blockiert die Managerregelung. Eine
|
- Zugangstoken werden weder als Property noch in Diagnosevariablen oder Logs
|
||||||
bestaetigte Entwicklungs-Lease bleibt bei Verbindungsunterbruch bis
|
ausgegeben.
|
||||||
`offlineUntil` gueltig.
|
|
||||||
|
|
||||||
## Noch festzulegen
|
## Noch festzulegen
|
||||||
|
|
||||||
- Anbieter und Datenformat für Prognose und Störüberwachung
|
- Aufbau, Gueltigkeitszeitraum und Rueckmeldung eines SDL-Auftrags
|
||||||
- Aufbau, Gültigkeitszeitraum und Rückmeldung eines SDL-Auftrags
|
- Produkt- und Zahlungsmodell der jaehrlichen Stoerueberwachungslizenz
|
||||||
- Authentisierung der noch optionalen Anschlüsse
|
- Verhalten nach Ablauf einer spaeter aktivierten Stoerueberwachungslizenz
|
||||||
- Ablösung der Entwicklungs-Lease durch den signierten produktiven Lizenzvertrag
|
|
||||||
|
|||||||
+10
-14
@@ -49,7 +49,7 @@ Prioritaeten, Fehlerverhalten und ein pruefbares Ergebnis.
|
|||||||
| OP-001 | Entschieden | Lizenzierung | Betriebsort des Lizenzservers |
|
| OP-001 | Entschieden | Lizenzierung | Betriebsort des Lizenzservers |
|
||||||
| OP-002 | Erledigt | Grundeinrichtung | Generator und kostenpflichtige Ersteinrichtung |
|
| OP-002 | Erledigt | Grundeinrichtung | Generator und kostenpflichtige Ersteinrichtung |
|
||||||
| OP-003 | Offen | Lizenzportal | VGT-Integration in license.enelix.ch |
|
| OP-003 | Offen | Lizenzportal | VGT-Integration in license.enelix.ch |
|
||||||
| OP-004 | Offen | Lizenzierung | Lizenzmodell des Verbrauchskostenreports |
|
| OP-004 | Entschieden | Lizenzierung | Lizenzmodell des Verbrauchskostenreports |
|
||||||
| OP-005 | Offen | Teststrategie | Standort fuer Feldtests |
|
| OP-005 | Offen | Teststrategie | Standort fuer Feldtests |
|
||||||
|
|
||||||
## Offene Punkte
|
## Offene Punkte
|
||||||
@@ -66,19 +66,6 @@ Prioritaeten, Fehlerverhalten und ein pruefbares Ergebnis.
|
|||||||
- **Ziel-Sprint:** Offen
|
- **Ziel-Sprint:** Offen
|
||||||
- **Ergebnis/Verweis:** Offen
|
- **Ergebnis/Verweis:** Offen
|
||||||
|
|
||||||
### OP-004: Lizenzmodell des Verbrauchskostenreports
|
|
||||||
|
|
||||||
- **Entscheidungsfrage:** Soll der Verbrauchskostenreport nach dem Lizenzmodell
|
|
||||||
des Managers oder mit einer einzigen, von der Anzahl angeschlossener Zaehler
|
|
||||||
unabhaengigen Modullizenz angeboten werden?
|
|
||||||
- **Kontext:** Fuer den Verbrauchskostenreport muss zwischen dem Lizenzmodell des
|
|
||||||
Managers und einer zaehlerunabhaengigen Modullizenz entschieden werden.
|
|
||||||
- **Auswirkung:** Die Entscheidung legt fest, ob die Anzahl angeschlossener
|
|
||||||
Zaehler den Lizenzumfang des Verbrauchskostenreports beeinflusst.
|
|
||||||
- **Verantwortlich:** Offen
|
|
||||||
- **Ziel-Sprint:** Offen
|
|
||||||
- **Ergebnis/Verweis:** Offen
|
|
||||||
|
|
||||||
### OP-005: Standort fuer Feldtests
|
### OP-005: Standort fuer Feldtests
|
||||||
|
|
||||||
- **Entscheidungsfrage:** An welchem Standort koennen die Module aus Enelix EMS
|
- **Entscheidungsfrage:** An welchem Standort koennen die Module aus Enelix EMS
|
||||||
@@ -114,6 +101,15 @@ Prioritaeten, Fehlerverhalten und ein pruefbares Ergebnis.
|
|||||||
- **Ergebnis/Verweis:** Umsetzung im Lizenzportal unter
|
- **Ergebnis/Verweis:** Umsetzung im Lizenzportal unter
|
||||||
`https://license.enelix.ch`.
|
`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
|
## Vorlage fuer neue Punkte
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ Sollleistung_W verwendet folgende Semantik:
|
|||||||
- null: nur Betriebsart synchronisieren und neues Angebot anfordern
|
- null: nur Betriebsart synchronisieren und neues Angebot anfordern
|
||||||
|
|
||||||
Ein konkreter Wert muss im zuletzt fuer dieselbe Betriebsart gemeldeten
|
Ein konkreter Wert muss im zuletzt fuer dieselbe Betriebsart gemeldeten
|
||||||
Leistungsangebot enthalten sein. Waehren der Aenderungssperre wird nur die
|
Einzelwert oder Leistungsbereich enthalten sein. Waehrend der Aenderungssperre
|
||||||
Wiederholung des aktuellen Sollwerts akzeptiert.
|
wird nur die Wiederholung des aktuellen Sollwerts akzeptiert.
|
||||||
|
|
||||||
## Batterie an Manager
|
## Batterie an Manager
|
||||||
|
|
||||||
@@ -56,10 +56,10 @@ Beispiel:
|
|||||||
"EmpfaengerID": 10001,
|
"EmpfaengerID": 10001,
|
||||||
"Zeitpunkt": 1788825602
|
"Zeitpunkt": 1788825602
|
||||||
},
|
},
|
||||||
"Betriebsart": "Peak",
|
"Betriebsart": "PV",
|
||||||
"PrioritaetPV": 0,
|
"PrioritaetPV": 0,
|
||||||
"PrioritaetPeak": 0,
|
"PrioritaetPeak": 0,
|
||||||
"Leistungswerte_W": [-1500],
|
"Leistungswerte_W": [{"Von_W": -5000, "Bis_W": 5000}],
|
||||||
"AenderungMoeglich": true,
|
"AenderungMoeglich": true,
|
||||||
"Verfuegbar": true,
|
"Verfuegbar": true,
|
||||||
"Istleistung_W": -250.0,
|
"Istleistung_W": -250.0,
|
||||||
@@ -68,7 +68,7 @@ Beispiel:
|
|||||||
{
|
{
|
||||||
"Kennung": "Sollleistung_W",
|
"Kennung": "Sollleistung_W",
|
||||||
"Art": "Sollwert",
|
"Art": "Sollwert",
|
||||||
"Wert": -1500,
|
"Wert": -537,
|
||||||
"Einheit": "W"
|
"Einheit": "W"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,7 +83,9 @@ Beispiel:
|
|||||||
|
|
||||||
Ist die Leistungsmessung ungueltig, wird Istleistung_W als null und
|
Ist die Leistungsmessung ungueltig, wird Istleistung_W als null und
|
||||||
Leistungsquelle als 0 gemeldet. Bei einem gueltigen Messwert ist
|
Leistungsquelle als 0 gemeldet. Bei einem gueltigen Messwert ist
|
||||||
Leistungsquelle 2.
|
Leistungsquelle 2. Frei regelbare Lade- und Entladeangebote werden als
|
||||||
|
inklusive Bereiche in ganzen Watt gemeldet. Feste Schutz- und Peakvorgaben
|
||||||
|
bleiben einzelne Leistungswerte.
|
||||||
|
|
||||||
## Ereignisse
|
## Ereignisse
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -11,7 +11,6 @@ Verhaltensreferenz.
|
|||||||
- positive Sollleistung fuer Laden und negative Sollleistung fuer Entladen
|
- positive Sollleistung fuer Laden und negative Sollleistung fuer Entladen
|
||||||
- dynamische maximale Lade- und Entladeleistung aus Variablen
|
- dynamische maximale Lade- und Entladeleistung aus Variablen
|
||||||
- Ladezustand, Netzleistung und aktuelle Batterieleistung als Eingaben
|
- Ladezustand, Netzleistung und aktuelle Batterieleistung als Eingaben
|
||||||
- 250-W-Grundraster und 50-W-Feinwerte um 0
|
|
||||||
- Reserve-, Mindestladezustands- und Hystereselogik
|
- Reserve-, Mindestladezustands- und Hystereselogik
|
||||||
- getrennte Angebote fuer PV und Peak
|
- getrennte Angebote fuer PV und Peak
|
||||||
- Wechselrichter- oder Enelix-Steuerung
|
- Wechselrichter- oder Enelix-Steuerung
|
||||||
@@ -25,6 +24,7 @@ Verhaltensreferenz.
|
|||||||
| zyklischer Timer Do_UserCalc | Messwert- und Managerereignisse |
|
| zyklischer Timer Do_UserCalc | Messwert- und Managerereignisse |
|
||||||
| IdleCounter in Zyklen | Aenderungssperre in Sekunden mit Einmaltimer |
|
| IdleCounter in Zyklen | Aenderungssperre in Sekunden mit Einmaltimer |
|
||||||
| PowerSteps als Modulvariable | Leistungswerte_W direkt im Vertrag |
|
| 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 |
|
| interne Stellwertvariablen | ausgewaehlte Registervariablen mit RequestAction |
|
||||||
| fest verdrahtete Herstellerhilfsvariablen | dynamisch sichtbare Registerauswahl |
|
| fest verdrahtete Herstellerhilfsvariablen | dynamisch sichtbare Registerauswahl |
|
||||||
| fest codierte 2-%-Hysterese | Property LadezustandHysterese |
|
| fest codierte 2-%-Hysterese | Property LadezustandHysterese |
|
||||||
|
|||||||
@@ -73,13 +73,17 @@ zuletzt Management geschrieben.
|
|||||||
|
|
||||||
## Leistungsangebot
|
## Leistungsangebot
|
||||||
|
|
||||||
Das Raster besteht wie in Enelix 1 aus 250-W-Grundschritten. Um 0 werden
|
Die Batterie meldet innerhalb der dynamischen Lade- und Entladegrenzen
|
||||||
zusaetzlich -200, -150, -100, -50, 0, 50, 100, 150 und 200 W angeboten,
|
inklusive Leistungsbereiche in ganzen Watt. Der Manager kann dadurch jeden
|
||||||
soweit diese Werte innerhalb der dynamischen Leistungsgrenzen liegen.
|
ganzzahligen Sollwert innerhalb des aktuell erlaubten Bereichs vorgeben. Die
|
||||||
|
Grenzen werden abgerundet, damit keine dynamische Maximalleistung ueberschritten
|
||||||
|
wird.
|
||||||
|
|
||||||
PV und Peak verwenden die bisherige SoC-Logik. Die Hysterese wird als
|
PV und Peak verwenden weiterhin die bisherige SoC-Logik. Feste Schutz- oder
|
||||||
persistenter Modulzustand gefuehrt. Bei Wechselrichtersteuerung, lokaler
|
Peakvorgaben bleiben Einzelwerte; frei regelbare Angebote werden als Bereiche
|
||||||
Deaktivierung oder ungueltigen Messwerten lautet das Angebot [0].
|
gemeldet. Die Hysterese wird als persistenter Modulzustand gefuehrt. Bei
|
||||||
|
Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen Messwerten
|
||||||
|
lautet das Angebot [0].
|
||||||
|
|
||||||
## Variablen
|
## Variablen
|
||||||
|
|
||||||
@@ -115,7 +119,7 @@ Transport und gemeinsame Felder folgen
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
- BatterieReglerTest: Raster, Hysterese, PV/Peak und Herstellerabbildung
|
- BatterieReglerTest: Leistungsbereiche, Hysterese, PV/Peak und Herstellerabbildung
|
||||||
- BatterieModulstrukturTest: Metadaten, Formular, Ereignismodell und Manager-ID
|
- BatterieModulstrukturTest: Metadaten, Formular, Ereignismodell und Manager-ID
|
||||||
- Symcon/modules/Batterie.php: reale Modulinstanz und Registeraktionen unter
|
- Symcon/modules/Batterie.php: reale Modulinstanz und Registeraktionen unter
|
||||||
IP-Symcon 8.0
|
IP-Symcon 8.0
|
||||||
|
|||||||
@@ -8,15 +8,28 @@ kommen aus den Verbrauchermeldungen.
|
|||||||
|
|
||||||
## Variablen
|
## Variablen
|
||||||
|
|
||||||
Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netzleistung`.
|
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 |
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Aktiv` | Boolean / bedienbar | Regelung ein/aus; Start `false`. |
|
| `Aktiv` | Boolean / bedienbar | Regelung ein/aus; Start `false`. |
|
||||||
| `Betriebsart` | String / Anzeige | `Inaktiv`, `PV` oder `Peak`. |
|
| `Betriebsart` | String / Anzeige | `Inaktiv`, `PV` oder `Peak`. |
|
||||||
| `Netzleistung` | Float / Anzeige | Aktuelle Netzleistung in W; positiv Bezug, negativ Einspeisung. |
|
| `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. |
|
| `NetzleistungGueltig` | Boolean / Logging | Messquelle vorhanden und aktuell. |
|
||||||
| `WirksameLastspitzengrenze` | Float / Logging | Aktuelle feste oder monatliche Grenze in W. |
|
| `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. |
|
| `Verteilbudget` | Float / Logging | Aktuell verfügbares Budget in W. |
|
||||||
| `VerbraucherAnzahl` | Integer / Logging | Anzahl zugeordneter Verbraucher. |
|
| `VerbraucherAnzahl` | Integer / Logging | Anzahl zugeordneter Verbraucher. |
|
||||||
| `VerbraucherVerfuegbar` | Integer / Logging | Anzahl aktuell verfügbarer Verbraucher. |
|
| `VerbraucherVerfuegbar` | Integer / Logging | Anzahl aktuell verfügbarer Verbraucher. |
|
||||||
@@ -47,12 +60,34 @@ Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netz
|
|||||||
| `Monatsgrenzen` | String/JSON | Editierbare Liste mit zwölf Monatswerten in W. |
|
| `Monatsgrenzen` | String/JSON | Editierbare Liste mit zwölf Monatswerten in W. |
|
||||||
| `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. |
|
| `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. |
|
||||||
| `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
|
| `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
|
||||||
| `PrognoseAnschluss` | String/JSON | Optionaler Forecast-Anschluss. |
|
| `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. |
|
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
|
||||||
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. |
|
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. |
|
||||||
| `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. |
|
| `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. |
|
||||||
| `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. |
|
| `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. |
|
||||||
| `LoggingEin` | Boolean | `false`; laufende Meldungen im Debug-Fenster ausgeben. |
|
| `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
|
Es gibt keine Sollwertquellenauswahl und keine zweite Prioritätseinstellung im
|
||||||
Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt.
|
Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt.
|
||||||
@@ -60,6 +95,115 @@ Die Monatsgrenzen werden in den Grundeinstellungen über eine Schaltfläche
|
|||||||
ein- und ausgeblendet. Die automatische Verbrauchersuche kann dort erneut
|
ein- und ausgeblendet. Die automatische Verbrauchersuche kann dort erneut
|
||||||
ausgeführt werden, ohne andere ungespeicherte Formulareingaben zu verlieren.
|
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.
|
||||||
|
|
||||||
|
## 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
|
## Lizenzierung
|
||||||
|
|
||||||
Der Manager arbeitet nur mit einer gueltigen Manager-Lizenz. Das Lizenzfeld steht
|
Der Manager arbeitet nur mit einer gueltigen Manager-Lizenz. Das Lizenzfeld steht
|
||||||
@@ -94,8 +238,11 @@ wird vom Lizenzserver abgelehnt.
|
|||||||
|
|
||||||
| Berechtigung | Freigegebene Funktion |
|
| Berechtigung | Freigegebene Funktion |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `manager_standard` | Manager-Grundregelung ohne Lastspitzenmodus. |
|
| `manager_standard` | Manager-Grundregelung ohne Peak Shaving. |
|
||||||
| `manager_peak` | Manager-Grundregelung einschliesslich konstantem oder monatlichem Lastspitzenmodus. |
|
| `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
|
Wird mit `manager_standard` ein Lastspitzenmodus aktiviert, bleibt der Manager
|
||||||
mit dem Hinweis `Peak Shaving ist nicht lizenziert` gesperrt. `manager_peak`
|
mit dem Hinweis `Peak Shaving ist nicht lizenziert` gesperrt. `manager_peak`
|
||||||
|
|||||||
+11
-11
@@ -42,9 +42,9 @@ werden, wenn sie vor dem Lauf nicht existierten.
|
|||||||
- `single`: genau die als Auswahl übergebenen Module
|
- `single`: genau die als Auswahl übergebenen Module
|
||||||
- `affected`: die durch geänderte Pfade ermittelten Module
|
- `affected`: die durch geänderte Pfade ermittelten Module
|
||||||
|
|
||||||
Verfuegbare Module: `EaseeGateway`, `LadestationGateway`,
|
Verfuegbare Module: `Batterie`, `EaseeGateway`, `LadestationGateway`,
|
||||||
`LadestationStandAlone`, `Manager`, `Pufferspeicher`, `VerbraucherEinStufig`
|
`LadestationStandAlone`, `Manager`, `Pufferspeicher`, `VerbraucherEinStufig`,
|
||||||
und `Warmwassererwaermer`.
|
`Waermepumpe` und `Warmwassererwaermer`.
|
||||||
|
|
||||||
Der Manager-Test enthält Manager ohne Verbraucher, jeden Verbrauchertyp einzeln und alle aktuell implementierten Verbrauchertypen gemeinsam.
|
Der Manager-Test enthält Manager ohne Verbraucher, jeden Verbrauchertyp einzeln und alle aktuell implementierten Verbrauchertypen gemeinsam.
|
||||||
|
|
||||||
@@ -85,15 +85,15 @@ Zusätzlich schreibt der Runner die Zusammenfassung in das IP-Symcon-Log.
|
|||||||
|
|
||||||
## CI-Regeln
|
## CI-Regeln
|
||||||
|
|
||||||
Bei jedem Push laufen sämtliche PHPUnit-Tests. Auf `develop` werden zusätzlich
|
Bei jedem Push laufen sämtliche PHPUnit-Tests und die PHP-Syntaxprüfung. Die
|
||||||
die von den geänderten Pfaden betroffenen Symcon-Modultests ausgeführt. Bei
|
Symcon-Modultests werden bis zur Verfügbarkeit eines geschützten Runners auf
|
||||||
gemeinsamen Framework- oder Vertragsänderungen läuft die vollständige Suite.
|
der isolierten IP-Symcon-8.0-Instanz des Agent-Servers ausgeführt. Vor jeder
|
||||||
Für `beta` ist immer `all` vorgeschrieben.
|
Übernahme nach `beta` ist ein erfolgreicher Lauf im Modus `all` verbindlich.
|
||||||
|
Der erzeugte JSON- und JUnit-Bericht gehört zum Freigabenachweis.
|
||||||
|
|
||||||
Die Symcon-Jobs benötigen einen geschützten Runner mit dem Label `symcon-8`,
|
Eine spätere CI-Automatisierung benötigt einen geschützten Runner mit dem Label
|
||||||
lokalem Zugriff auf eine isolierte IP-Symcon-8.x-Instanz und den installierten
|
`symcon-8`, lokalem Zugriff auf die isolierte IP-Symcon-Instanz und den exakt
|
||||||
Repository-Stand. Ohne diesen Runner ist die Richtlinie dokumentiert, aber
|
zu prüfenden Repository-Stand.
|
||||||
nicht technisch durchsetzbar.
|
|
||||||
|
|
||||||
## Checkliste für neue Module
|
## Checkliste für neue Module
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ Die Demo erzeugt idempotent eine vollständige, spielbare EMS-Anlage in IP-Symco
|
|||||||
- einen einstufigen Entfeuchter (0,9 kW),
|
- einen einstufigen Entfeuchter (0,9 kW),
|
||||||
- einen zweistufigen Pufferspeicher (1,8/3,6 kW) mit Heizkurve,
|
- einen zweistufigen Pufferspeicher (1,8/3,6 kW) mit Heizkurve,
|
||||||
- einen bidirektionalen Batteriespeicher (10 kWh, +/-3,5 kW),
|
- 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,
|
- simulierte PV-Erzeugung, variable Bewölkung, Tageslastgang, Zusatzlast,
|
||||||
Außentemperatur, Wärmeverluste, Netzleistung und Speicherzustände,
|
Außentemperatur, Wärmeverluste, Netzleistung und Speicherzustände,
|
||||||
- native Symcon-Instanz `Energy Distribution` für acht Energieflussknoten,
|
- native Symcon-Instanz `Energy Distribution` für acht Energieflussknoten,
|
||||||
@@ -39,6 +45,19 @@ damit keine Lizenzfehler protokolliert werden. Nach der Aktivierung den
|
|||||||
Bootstrap erneut ausführen; er ordnet dann alle fünf Teilnehmer zu und startet
|
Bootstrap erneut ausführen; er ordnet dann alle fünf Teilnehmer zu und startet
|
||||||
die Regelung.
|
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
|
## Bedienung
|
||||||
|
|
||||||
In der Kachelvisualisierung öffnet die Startkategorie `Enelix 2 Demoanlage`.
|
In der Kachelvisualisierung öffnet die Startkategorie `Enelix 2 Demoanlage`.
|
||||||
@@ -48,7 +67,9 @@ eine ungeregelte Zusatzlast verändern. Bei aktivierten dynamischen Profilen
|
|||||||
entstehen reproduzierbare Morgen- und Abendspitzen, ziehende Bewölkung und ein
|
entstehen reproduzierbare Morgen- und Abendspitzen, ziehende Bewölkung und ein
|
||||||
Tagesgang der Außentemperatur. Der Manager verteilt den verfügbaren
|
Tagesgang der Außentemperatur. Der Manager verteilt den verfügbaren
|
||||||
PV-Überschuss nach Priorität auf beide Boiler, Entfeuchter, Pufferspeicher und
|
PV-Überschuss nach Priorität auf beide Boiler, Entfeuchter, Pufferspeicher und
|
||||||
Batterie.
|
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`,
|
`Übersicht` zeigt die native Symcon-Energieverteilung. Die Kacheln `Boiler 1`,
|
||||||
`Boiler 2`, `Pufferspeicher` und `Batteriespeicher` öffnen direkt die jeweiligen
|
`Boiler 2`, `Pufferspeicher` und `Batteriespeicher` öffnen direkt die jeweiligen
|
||||||
@@ -64,6 +85,8 @@ Simulationszeit.
|
|||||||
- Die fünf Verbraucher müssen Status 102 besitzen; der Manager nach Lizenzaktivierung ebenfalls (vorher erwarteter Status 203).
|
- 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.
|
- 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 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.
|
- 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.
|
- Netzleistung, PV-Leistung, Hausverbrauch, EMS-Leistung, Einzelverbräuche, Speicherleistung, Energiesummen und Temperaturen werden archiviert.
|
||||||
- Die native Energieverteilung muss acht Knoten enthalten.
|
- Die native Energieverteilung muss acht Knoten enthalten.
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ function enelixDemoInstall(): array
|
|||||||
$batteryManagement = enelixDemoEnsureVariable($technical, 'BatterieManagement', 'Batterie Managementregister', 1, '', 0, 340);
|
$batteryManagement = enelixDemoEnsureVariable($technical, 'BatterieManagement', 'Batterie Managementregister', 1, '', 0, 340);
|
||||||
$batteryMode = enelixDemoEnsureVariable($technical, 'BatterieModus', 'Batterie Modusregister', 1, '', 0, 350);
|
$batteryMode = enelixDemoEnsureVariable($technical, 'BatterieModus', 'Batterie Modusregister', 1, '', 0, 350);
|
||||||
$batteryRegisterPower = enelixDemoEnsureVariable($technical, 'BatterieRegisterLeistung', 'Batterie Leistungsregister', 2, 'ENELIX.Demo.PowerPositive', 0.0, 360);
|
$batteryRegisterPower = enelixDemoEnsureVariable($technical, 'BatterieRegisterLeistung', 'Batterie Leistungsregister', 2, 'ENELIX.Demo.PowerPositive', 0.0, 360);
|
||||||
|
$inverterActual = enelixDemoEnsureVariable($technical, 'WechselrichterIstleistung', 'PV-Wechselrichter Istleistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 370);
|
||||||
|
$inverterLimit = enelixDemoEnsureVariable($technical, 'WechselrichterLeistungsgrenze', 'PV-Wechselrichter Leistungsgrenze', 2, 'ENELIX.Demo.PowerPositive', max(0.0, (float) GetValue($peak)), 380);
|
||||||
$chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 390);
|
$chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 390);
|
||||||
IPS_SetHidden($chartUpdated, true);
|
IPS_SetHidden($chartUpdated, true);
|
||||||
|
|
||||||
@@ -136,10 +138,10 @@ function enelixDemoInstall(): array
|
|||||||
foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact, $buffer1, $buffer2] as $actorId) {
|
foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact, $buffer1, $buffer2] as $actorId) {
|
||||||
IPS_SetVariableCustomAction($actorId, $actorScript);
|
IPS_SetVariableCustomAction($actorId, $actorScript);
|
||||||
}
|
}
|
||||||
foreach ([$batteryManagement, $batteryMode, $batteryRegisterPower] as $registerId) {
|
foreach ([$batteryManagement, $batteryMode, $batteryRegisterPower, $inverterLimit] as $registerId) {
|
||||||
IPS_SetVariableCustomAction($registerId, $registerScript);
|
IPS_SetVariableCustomAction($registerId, $registerScript);
|
||||||
}
|
}
|
||||||
foreach ([$temp1, $temp2, $bufferTemp, $outsideTemp, $batteryMaxCharge, $batteryMaxDischarge, $batterySoc, $batteryActual] as $sensorId) {
|
foreach ([$temp1, $temp2, $bufferTemp, $outsideTemp, $batteryMaxCharge, $batteryMaxDischarge, $batterySoc, $batteryActual, $inverterActual] as $sensorId) {
|
||||||
SetValue($sensorId, GetValue($sensorId));
|
SetValue($sensorId, GetValue($sensorId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,9 +279,53 @@ function enelixDemoInstall(): array
|
|||||||
static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true],
|
static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true],
|
||||||
[$boiler1, $boiler2, $dryer, $buffer, $battery]
|
[$boiler1, $boiler2, $dryer, $buffer, $battery]
|
||||||
);
|
);
|
||||||
|
$pvNennleistungKw = max(0.1, (float) GetValue($peak) / 1000.0);
|
||||||
|
$wechselrichterKonfiguration = [[
|
||||||
|
'ID' => 'demo-hybrid-1',
|
||||||
|
'Name' => 'Demo-Hybridwechselrichter',
|
||||||
|
'Typ' => 'hybrid',
|
||||||
|
'ACNennleistung_kW' => $pvNennleistungKw,
|
||||||
|
'IstleistungVariableID' => $inverterActual,
|
||||||
|
'Istleistungsfaktor' => 1.0,
|
||||||
|
'BegrenzungVariableID' => $inverterLimit,
|
||||||
|
'Begrenzungsart' => 'watt',
|
||||||
|
]];
|
||||||
|
$pvKonfiguration = [[
|
||||||
|
'ID' => 'demo-pv-1',
|
||||||
|
'Name' => 'Demo-PV-Anlage',
|
||||||
|
'DCLeistung_kWp' => $pvNennleistungKw,
|
||||||
|
'Neigung_Grad' => 30.0,
|
||||||
|
'Azimut_Grad' => 0.0,
|
||||||
|
'WechselrichterID' => 'demo-hybrid-1',
|
||||||
|
'MPPT' => 'A',
|
||||||
|
'Modulanzahl' => 0,
|
||||||
|
'Modulleistung_Wp' => 0.0,
|
||||||
|
]];
|
||||||
|
$batterieKonfiguration = [[
|
||||||
|
'ID' => 'demo-battery-1',
|
||||||
|
'Name' => 'Batteriespeicher 10 kWh',
|
||||||
|
'Nennkapazitaet_kWh' => 10.0,
|
||||||
|
'Nutzkapazitaet_kWh' => 10.0,
|
||||||
|
'MaxLadeleistung_kW' => max(0.1, (float) GetValue($batteryMaxCharge) / 1000.0),
|
||||||
|
'MaxEntladeleistung_kW' => max(
|
||||||
|
0.1,
|
||||||
|
(float) GetValue($batteryMaxDischarge) / 1000.0
|
||||||
|
),
|
||||||
|
'WechselrichterID' => 'demo-hybrid-1',
|
||||||
|
'Kopplung' => 'hybrid',
|
||||||
|
'LeistungVariableID' => $batteryActual,
|
||||||
|
'Leistungsfaktor' => 1.0,
|
||||||
|
'SOCVariableID' => $batterySoc,
|
||||||
|
]];
|
||||||
$managerProperties = [
|
$managerProperties = [
|
||||||
'NetzleistungVariableID' => $grid,
|
'NetzleistungVariableID' => $grid,
|
||||||
'Netzleistungsfaktor' => 1.0,
|
'Netzleistungsfaktor' => 1.0,
|
||||||
|
'MesswerteAnzeigen' => true,
|
||||||
|
'EnergyPieAnzeigen' => true,
|
||||||
|
'EnergiediagrammeAnzeigen' => true,
|
||||||
|
'FunFactsAnzeigen' => true,
|
||||||
|
'EnergieflussAnzeigen' => true,
|
||||||
|
'PrognoseSendeintervall' => 60,
|
||||||
'MesswertMaxAlter' => 10,
|
'MesswertMaxAlter' => 10,
|
||||||
'AutomatischeSuche' => false,
|
'AutomatischeSuche' => false,
|
||||||
'SuchbereichID' => $root,
|
'SuchbereichID' => $root,
|
||||||
@@ -288,6 +334,19 @@ function enelixDemoInstall(): array
|
|||||||
'VerbraucherTimeout' => 15,
|
'VerbraucherTimeout' => 15,
|
||||||
'SollwertSolarladen' => 0.0,
|
'SollwertSolarladen' => 0.0,
|
||||||
'Umschaltdifferenz' => 0.0,
|
'Umschaltdifferenz' => 0.0,
|
||||||
|
'EinspeisebegrenzungAktiv' => true,
|
||||||
|
'Einspeisegrenze' => 3000.0,
|
||||||
|
'PrognoseAktiv' => false,
|
||||||
|
'NetzfahrplanAktiv' => false,
|
||||||
|
'AnlagenWechselrichter' => json_encode(
|
||||||
|
$wechselrichterKonfiguration,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
),
|
||||||
|
'AnlagenPVFlaechen' => json_encode($pvKonfiguration, JSON_THROW_ON_ERROR),
|
||||||
|
'AnlagenBatterien' => json_encode(
|
||||||
|
$batterieKonfiguration,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
),
|
||||||
'DiagnosevariablenAnzeigen' => true,
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
'LoggingEin' => false,
|
'LoggingEin' => false,
|
||||||
];
|
];
|
||||||
@@ -311,6 +370,7 @@ function enelixDemoInstall(): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
IPS_RequestAction($manager, 'Aktiv', $managerReady);
|
IPS_RequestAction($manager, 'Aktiv', $managerReady);
|
||||||
|
IPS_RequestAction($manager, 'MesswerteAufzeichnen', true);
|
||||||
|
|
||||||
$boiler1List = enelixDemoEnsureInstance(
|
$boiler1List = enelixDemoEnsureInstance(
|
||||||
$dashboard,
|
$dashboard,
|
||||||
@@ -430,7 +490,11 @@ function enelixDemoTick(string $source = 'Manual'): void
|
|||||||
$sun = ($hour >= 6.0 && $hour <= 18.0)
|
$sun = ($hour >= 6.0 && $hour <= 18.0)
|
||||||
? max(0.0, sin(M_PI * ($hour - 6.0) / 12.0))
|
? max(0.0, sin(M_PI * ($hour - 6.0) / 12.0))
|
||||||
: 0.0;
|
: 0.0;
|
||||||
$pv = $peak * $sun * (1.0 - ($environment['cloud'] * 0.0085));
|
$pvAvailable = $peak * $sun * (1.0 - ($environment['cloud'] * 0.0085));
|
||||||
|
$inverterActual = enelixDemoRequiredId($technical, 'WechselrichterIstleistung');
|
||||||
|
$inverterLimit = enelixDemoRequiredId($technical, 'WechselrichterLeistungsgrenze');
|
||||||
|
$pv = enelixDemoLimitPvPower($pvAvailable, (float) GetValue($inverterLimit));
|
||||||
|
SetValue($inverterActual, round($pv, 1));
|
||||||
|
|
||||||
$contacts = [
|
$contacts = [
|
||||||
'boiler11' => enelixDemoRequiredId($technical, 'Boiler1Stufe1'),
|
'boiler11' => enelixDemoRequiredId($technical, 'Boiler1Stufe1'),
|
||||||
@@ -495,6 +559,11 @@ function enelixDemoTick(string $source = 'Manual'): void
|
|||||||
IPS_RequestAction($consumerId, 'Melden', true);
|
IPS_RequestAction($consumerId, 'Melden', true);
|
||||||
}
|
}
|
||||||
IPS_RequestAction($manager, 'Regeln', true);
|
IPS_RequestAction($manager, 'Regeln', true);
|
||||||
|
$pv = enelixDemoLimitPvPower(
|
||||||
|
$pvAvailable,
|
||||||
|
(float) GetValue($inverterLimit)
|
||||||
|
);
|
||||||
|
SetValue($inverterActual, round($pv, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
$battery = $managerReady
|
$battery = $managerReady
|
||||||
@@ -551,6 +620,11 @@ function enelixDemoTick(string $source = 'Manual'): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enelixDemoLimitPvPower(float $availablePower, float $limitPower): float
|
||||||
|
{
|
||||||
|
return min(max(0.0, $availablePower), max(0.0, $limitPower));
|
||||||
|
}
|
||||||
|
|
||||||
function enelixDemoRenderFlow(array $state): string
|
function enelixDemoRenderFlow(array $state): string
|
||||||
{
|
{
|
||||||
$format = static fn (float $value): string => number_format($value, 0, ',', "'");
|
$format = static fn (float $value): string => number_format($value, 0, ',', "'");
|
||||||
|
|||||||
+2
-2
@@ -7,6 +7,6 @@
|
|||||||
"version": "8.0"
|
"version": "8.0"
|
||||||
},
|
},
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"build": 0,
|
"build": 1,
|
||||||
"date": 0
|
"date": 1790662541
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use JsonException;
|
||||||
|
|
||||||
|
final class Anlagentopologie
|
||||||
|
{
|
||||||
|
public const VERSION = '1.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public static function ausJson(
|
||||||
|
string $wechselrichterJson,
|
||||||
|
string $pvFlaechenJson,
|
||||||
|
string $batterienJson,
|
||||||
|
string $installationID
|
||||||
|
): array {
|
||||||
|
$wechselrichter = self::dekodiereListe($wechselrichterJson, 'AnlagenWechselrichter');
|
||||||
|
$pvFlaechen = self::dekodiereListe($pvFlaechenJson, 'AnlagenPVFlaechen');
|
||||||
|
$batterien = self::dekodiereListe($batterienJson, 'AnlagenBatterien');
|
||||||
|
|
||||||
|
$inverters = [];
|
||||||
|
$inverterById = [];
|
||||||
|
foreach ($wechselrichter as $index => $eintrag) {
|
||||||
|
$pfad = 'Wechselrichter ' . ($index + 1);
|
||||||
|
$id = self::kennung($eintrag['ID'] ?? null, $pfad . ': ID');
|
||||||
|
if (isset($inverterById[$id])) {
|
||||||
|
throw new InvalidArgumentException($pfad . ': ID ist mehrfach vorhanden.');
|
||||||
|
}
|
||||||
|
$typ = self::auswahl(
|
||||||
|
$eintrag['Typ'] ?? null,
|
||||||
|
['pv', 'battery', 'hybrid'],
|
||||||
|
$pfad . ': Typ'
|
||||||
|
);
|
||||||
|
$acRatedKw = self::positiveZahl(
|
||||||
|
$eintrag['ACNennleistung_kW'] ?? null,
|
||||||
|
$pfad . ': AC-Nennleistung'
|
||||||
|
);
|
||||||
|
$inverter = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => self::name($eintrag['Name'] ?? null, $id),
|
||||||
|
'type' => $typ,
|
||||||
|
'acRatedKw' => $acRatedKw,
|
||||||
|
];
|
||||||
|
$inverters[] = $inverter;
|
||||||
|
$inverterById[$id] = $inverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pvArrays = [];
|
||||||
|
$pvIds = [];
|
||||||
|
foreach ($pvFlaechen as $index => $eintrag) {
|
||||||
|
$pfad = 'PV-Flaeche ' . ($index + 1);
|
||||||
|
$id = self::eindeutigeKennung($eintrag['ID'] ?? null, $pvIds, $pfad);
|
||||||
|
$inverterId = self::kennung(
|
||||||
|
$eintrag['WechselrichterID'] ?? null,
|
||||||
|
$pfad . ': Wechselrichter-ID'
|
||||||
|
);
|
||||||
|
$inverter = self::wechselrichter($inverterById, $inverterId, $pfad);
|
||||||
|
if (!in_array($inverter['type'], ['pv', 'hybrid'], true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
$pfad . ': benoetigt einen PV- oder Hybridwechselrichter.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$moduleCount = self::optionalePositiveGanzzahl(
|
||||||
|
$eintrag['Modulanzahl'] ?? null,
|
||||||
|
$pfad . ': Modulanzahl'
|
||||||
|
);
|
||||||
|
$modulePowerWp = self::optionalePositiveZahl(
|
||||||
|
$eintrag['Modulleistung_Wp'] ?? null,
|
||||||
|
$pfad . ': Modulleistung'
|
||||||
|
);
|
||||||
|
if (($moduleCount === null) !== ($modulePowerWp === null)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
$pfad . ': Modulanzahl und Modulleistung muessen gemeinsam gepflegt werden.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$pvArrays[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => self::name($eintrag['Name'] ?? null, $id),
|
||||||
|
'dcKwp' => self::positiveZahl(
|
||||||
|
$eintrag['DCLeistung_kWp'] ?? null,
|
||||||
|
$pfad . ': DC-Leistung'
|
||||||
|
),
|
||||||
|
'tiltDeg' => self::zahlImBereich(
|
||||||
|
$eintrag['Neigung_Grad'] ?? null,
|
||||||
|
0.0,
|
||||||
|
90.0,
|
||||||
|
$pfad . ': Neigung'
|
||||||
|
),
|
||||||
|
'azimuthDeg' => self::zahlImBereich(
|
||||||
|
$eintrag['Azimut_Grad'] ?? null,
|
||||||
|
-180.0,
|
||||||
|
180.0,
|
||||||
|
$pfad . ': Azimut'
|
||||||
|
),
|
||||||
|
'inverterId' => $inverterId,
|
||||||
|
'mpptInput' => self::optionalerText($eintrag['MPPT'] ?? null),
|
||||||
|
'moduleCount' => $moduleCount,
|
||||||
|
'modulePowerWp' => $modulePowerWp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$batteryItems = [];
|
||||||
|
$batteryIds = [];
|
||||||
|
foreach ($batterien as $index => $eintrag) {
|
||||||
|
$pfad = 'Batterie ' . ($index + 1);
|
||||||
|
$id = self::eindeutigeKennung($eintrag['ID'] ?? null, $batteryIds, $pfad);
|
||||||
|
$inverterId = self::kennung(
|
||||||
|
$eintrag['WechselrichterID'] ?? null,
|
||||||
|
$pfad . ': Wechselrichter-ID'
|
||||||
|
);
|
||||||
|
$inverter = self::wechselrichter($inverterById, $inverterId, $pfad);
|
||||||
|
$kopplung = self::auswahl(
|
||||||
|
$eintrag['Kopplung'] ?? null,
|
||||||
|
['ac', 'dc', 'hybrid'],
|
||||||
|
$pfad . ': Kopplung'
|
||||||
|
);
|
||||||
|
if ($kopplung === 'ac' && !in_array($inverter['type'], ['battery', 'hybrid'], true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
$pfad . ': AC-Kopplung benoetigt einen Batterie- oder Hybridwechselrichter.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($kopplung !== 'ac' && $inverter['type'] !== 'hybrid') {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
$pfad . ': DC- und Hybridkopplung benoetigen einen Hybridwechselrichter.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$nominalCapacityKwh = self::positiveZahl(
|
||||||
|
$eintrag['Nennkapazitaet_kWh'] ?? null,
|
||||||
|
$pfad . ': Nennkapazitaet'
|
||||||
|
);
|
||||||
|
$usableCapacityKwh = self::positiveZahl(
|
||||||
|
$eintrag['Nutzkapazitaet_kWh'] ?? null,
|
||||||
|
$pfad . ': Nutzkapazitaet'
|
||||||
|
);
|
||||||
|
if ($usableCapacityKwh > $nominalCapacityKwh) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
$pfad . ': Nutzkapazitaet darf die Nennkapazitaet nicht uebersteigen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$batteryItems[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => self::name($eintrag['Name'] ?? null, $id),
|
||||||
|
'nominalCapacityKwh' => $nominalCapacityKwh,
|
||||||
|
'usableCapacityKwh' => $usableCapacityKwh,
|
||||||
|
'maxChargeKw' => self::positiveZahl(
|
||||||
|
$eintrag['MaxLadeleistung_kW'] ?? null,
|
||||||
|
$pfad . ': maximale Ladeleistung'
|
||||||
|
),
|
||||||
|
'maxDischargeKw' => self::positiveZahl(
|
||||||
|
$eintrag['MaxEntladeleistung_kW'] ?? null,
|
||||||
|
$pfad . ': maximale Entladeleistung'
|
||||||
|
),
|
||||||
|
'inverterId' => $inverterId,
|
||||||
|
'coupling' => $kopplung,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'version' => self::VERSION,
|
||||||
|
'installationId' => trim($installationID),
|
||||||
|
'inverters' => $inverters,
|
||||||
|
'pvArrays' => $pvArrays,
|
||||||
|
'batteries' => $batteryItems,
|
||||||
|
'sharedAcLimits' => self::gemeinsameAcGrenzen(
|
||||||
|
$inverters,
|
||||||
|
$pvArrays,
|
||||||
|
$batteryItems
|
||||||
|
),
|
||||||
|
'summary' => self::zusammenfassung($inverters, $pvArrays, $batteryItems),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array<string, mixed>> */
|
||||||
|
private static function dekodiereListe(string $json, string $name): array
|
||||||
|
{
|
||||||
|
$daten = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($daten)
|
||||||
|
|| ($daten !== [] && array_keys($daten) !== range(0, count($daten) - 1))
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException($name . ' muss eine JSON-Liste sein.');
|
||||||
|
}
|
||||||
|
foreach ($daten as $eintrag) {
|
||||||
|
if (!is_array($eintrag)) {
|
||||||
|
throw new InvalidArgumentException($name . ' enthaelt einen ungueltigen Eintrag.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $daten;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, bool> $verwendet */
|
||||||
|
private static function eindeutigeKennung($wert, array &$verwendet, string $pfad): string
|
||||||
|
{
|
||||||
|
$id = self::kennung($wert, $pfad . ': ID');
|
||||||
|
if (isset($verwendet[$id])) {
|
||||||
|
throw new InvalidArgumentException($pfad . ': ID ist mehrfach vorhanden.');
|
||||||
|
}
|
||||||
|
$verwendet[$id] = true;
|
||||||
|
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function kennung($wert, string $feld): string
|
||||||
|
{
|
||||||
|
if (!is_string($wert)
|
||||||
|
|| preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/', trim($wert)) !== 1
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
$feld . ' muss 1 bis 64 Zeichen aus Buchstaben, Zahlen, Punkt, Minus oder Unterstrich enthalten.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($wert);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function name($wert, string $fallback): string
|
||||||
|
{
|
||||||
|
if ($wert === null || $wert === '') {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
if (!is_string($wert) || strlen(trim($wert)) > 120) {
|
||||||
|
throw new InvalidArgumentException('Name muss ein Text mit maximal 120 Zeichen sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($wert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $werte */
|
||||||
|
private static function auswahl($wert, array $werte, string $feld): string
|
||||||
|
{
|
||||||
|
if (!is_string($wert) || !in_array($wert, $werte, true)) {
|
||||||
|
throw new InvalidArgumentException($feld . ' ist ungueltig.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wert;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function positiveZahl($wert, string $feld): float
|
||||||
|
{
|
||||||
|
if (!is_int($wert) && !is_float($wert)) {
|
||||||
|
throw new InvalidArgumentException($feld . ' muss eine Zahl sein.');
|
||||||
|
}
|
||||||
|
if (!is_finite((float) $wert) || (float) $wert <= 0.0) {
|
||||||
|
throw new InvalidArgumentException($feld . ' muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) $wert;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function optionalePositiveZahl($wert, string $feld): ?float
|
||||||
|
{
|
||||||
|
if ($wert === null || $wert === '' || $wert === 0 || $wert === 0.0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::positiveZahl($wert, $feld);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function optionalePositiveGanzzahl($wert, string $feld): ?int
|
||||||
|
{
|
||||||
|
if ($wert === null || $wert === '' || $wert === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!is_int($wert) || $wert <= 0) {
|
||||||
|
throw new InvalidArgumentException($feld . ' muss eine positive Ganzzahl sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wert;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function zahlImBereich(
|
||||||
|
$wert,
|
||||||
|
float $minimum,
|
||||||
|
float $maximum,
|
||||||
|
string $feld
|
||||||
|
): float {
|
||||||
|
if ((!is_int($wert) && !is_float($wert))
|
||||||
|
|| !is_finite((float) $wert)
|
||||||
|
|| (float) $wert < $minimum
|
||||||
|
|| (float) $wert > $maximum
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
sprintf('%s muss zwischen %g und %g liegen.', $feld, $minimum, $maximum)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) $wert;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function optionalerText($wert): ?string
|
||||||
|
{
|
||||||
|
if ($wert === null || $wert === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!is_string($wert) || strlen(trim($wert)) > 64) {
|
||||||
|
throw new InvalidArgumentException('MPPT muss ein Text mit maximal 64 Zeichen sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($wert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, mixed>> $wechselrichter
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function wechselrichter(array $wechselrichter, string $id, string $pfad): array
|
||||||
|
{
|
||||||
|
if (!isset($wechselrichter[$id])) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
$pfad . ': referenzierter Wechselrichter ist nicht vorhanden.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wechselrichter[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $inverters
|
||||||
|
* @param list<array<string, mixed>> $pvArrays
|
||||||
|
* @param list<array<string, mixed>> $batteries
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private static function gemeinsameAcGrenzen(
|
||||||
|
array $inverters,
|
||||||
|
array $pvArrays,
|
||||||
|
array $batteries
|
||||||
|
): array {
|
||||||
|
$result = [];
|
||||||
|
foreach ($inverters as $inverter) {
|
||||||
|
if ($inverter['type'] !== 'hybrid') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pvIds = [];
|
||||||
|
foreach ($pvArrays as $pvArray) {
|
||||||
|
if ($pvArray['inverterId'] === $inverter['id']) {
|
||||||
|
$pvIds[] = $pvArray['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$batteryIds = [];
|
||||||
|
foreach ($batteries as $battery) {
|
||||||
|
if ($battery['inverterId'] === $inverter['id']) {
|
||||||
|
$batteryIds[] = $battery['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($pvIds === [] && $batteryIds === []) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$result[] = [
|
||||||
|
'inverterId' => $inverter['id'],
|
||||||
|
'acRatedKw' => $inverter['acRatedKw'],
|
||||||
|
'pvArrayIds' => $pvIds,
|
||||||
|
'batteryIds' => $batteryIds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $inverters
|
||||||
|
* @param list<array<string, mixed>> $pvArrays
|
||||||
|
* @param list<array<string, mixed>> $batteries
|
||||||
|
* @return array<string, float>
|
||||||
|
*/
|
||||||
|
private static function zusammenfassung(
|
||||||
|
array $inverters,
|
||||||
|
array $pvArrays,
|
||||||
|
array $batteries
|
||||||
|
): array {
|
||||||
|
$pvInverterIds = [];
|
||||||
|
$pvDcKwp = 0.0;
|
||||||
|
foreach ($pvArrays as $pvArray) {
|
||||||
|
$pvDcKwp += $pvArray['dcKwp'];
|
||||||
|
$pvInverterIds[$pvArray['inverterId']] = true;
|
||||||
|
}
|
||||||
|
$pvAcKw = 0.0;
|
||||||
|
foreach ($inverters as $inverter) {
|
||||||
|
if (isset($pvInverterIds[$inverter['id']])) {
|
||||||
|
$pvAcKw += $inverter['acRatedKw'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$usableBatteryKwh = 0.0;
|
||||||
|
$maxBatteryChargeKw = 0.0;
|
||||||
|
$maxBatteryDischargeKw = 0.0;
|
||||||
|
foreach ($batteries as $battery) {
|
||||||
|
$usableBatteryKwh += $battery['usableCapacityKwh'];
|
||||||
|
$maxBatteryChargeKw += $battery['maxChargeKw'];
|
||||||
|
$maxBatteryDischargeKw += $battery['maxDischargeKw'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pvDcKwp' => $pvDcKwp,
|
||||||
|
'pvAcKw' => $pvAcKw,
|
||||||
|
'usableBatteryKwh' => $usableBatteryKwh,
|
||||||
|
'maxBatteryChargeKw' => $maxBatteryChargeKw,
|
||||||
|
'maxBatteryDischargeKw' => $maxBatteryDischargeKw,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-48
@@ -17,38 +17,6 @@ final class BatterieRegler
|
|||||||
public const STEUERUNG_WECHSELRICHTER = 1;
|
public const STEUERUNG_WECHSELRICHTER = 1;
|
||||||
public const STEUERUNG_ENELIX = 2;
|
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(
|
public static function hystereseAktiv(
|
||||||
float $ladezustand,
|
float $ladezustand,
|
||||||
float $reserve,
|
float $reserve,
|
||||||
@@ -68,9 +36,10 @@ final class BatterieRegler
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bildet das zustands- und betriebsartabhaengige Angebot aus Enelix 1 ab.
|
* Bildet das zustands- und betriebsartabhaengige Angebot als
|
||||||
|
* durchgaengige Leistungsbereiche in ganzen Watt ab.
|
||||||
*
|
*
|
||||||
* @return list<int>
|
* @return list<int|array{Von_W: int, Bis_W: int}>
|
||||||
*/
|
*/
|
||||||
public static function leistungsangebot(
|
public static function leistungsangebot(
|
||||||
bool $aktiv,
|
bool $aktiv,
|
||||||
@@ -97,7 +66,8 @@ final class BatterieRegler
|
|||||||
return [0];
|
return [0];
|
||||||
}
|
}
|
||||||
|
|
||||||
$raster = self::leistungsraster($maxLadenW, $maxEntladenW);
|
$maxLaden = (int) floor($maxLadenW);
|
||||||
|
$maxEntladen = (int) floor($maxEntladenW);
|
||||||
if ($peakbetrieb) {
|
if ($peakbetrieb) {
|
||||||
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
||||||
$ziel = $netzleistungW > 0.0
|
$ziel = $netzleistungW > 0.0
|
||||||
@@ -107,26 +77,26 @@ final class BatterieRegler
|
|||||||
return [self::begrenzeLeistung($ziel, $maxLadenW, $maxEntladenW)];
|
return [self::begrenzeLeistung($ziel, $maxLadenW, $maxEntladenW)];
|
||||||
}
|
}
|
||||||
if ($ladezustand > $reserve && $hystereseAktiv) {
|
if ($ladezustand > $reserve && $hystereseAktiv) {
|
||||||
return self::filtere($raster, static fn (int $wert): bool => $wert <= 0);
|
return self::leistungsbereich(-$maxEntladen, 0);
|
||||||
}
|
}
|
||||||
if ($ladezustand > $minimum) {
|
if ($ladezustand > $minimum) {
|
||||||
return $raster;
|
return self::leistungsbereich(-$maxEntladen, $maxLaden);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::filtere($raster, static fn (int $wert): bool => $wert >= 0);
|
return self::leistungsbereich(0, $maxLaden);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($ladezustand > 99.0) {
|
if ($ladezustand > 99.0) {
|
||||||
return self::filtere($raster, static fn (int $wert): bool => $wert <= 0);
|
return self::leistungsbereich(-$maxEntladen, 0);
|
||||||
}
|
}
|
||||||
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
||||||
return $raster;
|
return self::leistungsbereich(-$maxEntladen, $maxLaden);
|
||||||
}
|
}
|
||||||
if ($ladezustand >= $reserve && $hystereseAktiv) {
|
if ($ladezustand >= $reserve && $hystereseAktiv) {
|
||||||
return self::filtere($raster, static fn (int $wert): bool => $wert >= 0);
|
return self::leistungsbereich(0, $maxLaden);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [(int) round($maxLadenW)];
|
return [$maxLaden];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,12 +190,14 @@ final class BatterieRegler
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param list<int> $werte
|
/** @return list<int|array{Von_W: int, Bis_W: int}> */
|
||||||
* @return list<int>
|
private static function leistungsbereich(int $vonW, int $bisW): array
|
||||||
*/
|
|
||||||
private static function filtere(array $werte, callable $filter): array
|
|
||||||
{
|
{
|
||||||
return array_values(array_filter($werte, $filter));
|
if ($vonW === $bisW) {
|
||||||
|
return [$vonW];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [['Von_W' => $vonW, 'Bis_W' => $bisW]];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function begrenzeLeistung(
|
private static function begrenzeLeistung(
|
||||||
@@ -233,7 +205,10 @@ final class BatterieRegler
|
|||||||
float $maxLadenW,
|
float $maxLadenW,
|
||||||
float $maxEntladenW
|
float $maxEntladenW
|
||||||
): int {
|
): int {
|
||||||
return (int) round(min($maxLadenW, max(-$maxEntladenW, $leistungW)));
|
$maxLaden = (int) floor($maxLadenW);
|
||||||
|
$maxEntladen = (int) floor($maxEntladenW);
|
||||||
|
|
||||||
|
return (int) round(min($maxLaden, max(-$maxEntladen, $leistungW)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function pruefeLeistungsgrenzen(float $maxLadenW, float $maxEntladenW): void
|
private static function pruefeLeistungsgrenzen(float $maxLadenW, float $maxEntladenW): void
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verteilt eine gemeinsame Einspeisegrenze auf regelbare PV-Wechselrichter.
|
||||||
|
*
|
||||||
|
* Netzleistung: positiv Bezug, negativ Einspeisung.
|
||||||
|
*/
|
||||||
|
final class EinspeiseRegler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param list<array{ID: string, Nennleistung_W: float|int, Istleistung_W: float|int}> $wechselrichter
|
||||||
|
* @param array<string, float|int> $letzteGrenzen
|
||||||
|
* @return array{
|
||||||
|
* Netzabweichung_W: float,
|
||||||
|
* ZielGesamtleistung_W: float,
|
||||||
|
* Abregelbedarf_W: float,
|
||||||
|
* Grenzen_W: array<string, float>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function verteile(
|
||||||
|
float $netzleistung,
|
||||||
|
float $einspeisegrenze,
|
||||||
|
array $wechselrichter,
|
||||||
|
array $letzteGrenzen = []
|
||||||
|
): array {
|
||||||
|
if ($einspeisegrenze < 0.0) {
|
||||||
|
throw new InvalidArgumentException('Einspeisegrenze darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
if ($wechselrichter === []) {
|
||||||
|
throw new InvalidArgumentException('Mindestens ein regelbarer Wechselrichter ist erforderlich.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nennleistungGesamt = 0.0;
|
||||||
|
$istleistungGesamt = 0.0;
|
||||||
|
$bisherigeGrenzeGesamt = 0.0;
|
||||||
|
$normalisiert = [];
|
||||||
|
$kennungen = [];
|
||||||
|
|
||||||
|
foreach ($wechselrichter as $eintrag) {
|
||||||
|
$id = $eintrag['ID'] ?? null;
|
||||||
|
$nennleistung = $eintrag['Nennleistung_W'] ?? null;
|
||||||
|
$istleistung = $eintrag['Istleistung_W'] ?? null;
|
||||||
|
if (!is_string($id) || trim($id) === '' || isset($kennungen[$id])) {
|
||||||
|
throw new InvalidArgumentException('Wechselrichter-ID fehlt oder ist mehrfach vorhanden.');
|
||||||
|
}
|
||||||
|
if (!is_numeric($nennleistung) || (float) $nennleistung <= 0.0) {
|
||||||
|
throw new InvalidArgumentException('Wechselrichter-Nennleistung muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
if (!is_numeric($istleistung) || (float) $istleistung < 0.0) {
|
||||||
|
throw new InvalidArgumentException('Wechselrichter-Istleistung darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nennleistung = (float) $nennleistung;
|
||||||
|
$istleistung = min((float) $istleistung, $nennleistung);
|
||||||
|
$bisherigeGrenze = $letzteGrenzen[$id] ?? $nennleistung;
|
||||||
|
if (!is_numeric($bisherigeGrenze)) {
|
||||||
|
throw new InvalidArgumentException('Gespeicherte Wechselrichtergrenze ist ungueltig.');
|
||||||
|
}
|
||||||
|
$bisherigeGrenze = max(0.0, min((float) $bisherigeGrenze, $nennleistung));
|
||||||
|
$normalisiert[] = [
|
||||||
|
'ID' => $id,
|
||||||
|
'Nennleistung_W' => $nennleistung,
|
||||||
|
'Istleistung_W' => $istleistung,
|
||||||
|
];
|
||||||
|
$kennungen[$id] = true;
|
||||||
|
$nennleistungGesamt += $nennleistung;
|
||||||
|
$istleistungGesamt += $istleistung;
|
||||||
|
$bisherigeGrenzeGesamt += $bisherigeGrenze;
|
||||||
|
}
|
||||||
|
|
||||||
|
$netzabweichung = $netzleistung + $einspeisegrenze;
|
||||||
|
$zielGesamt = $bisherigeGrenzeGesamt;
|
||||||
|
$nachIstleistungVerteilen = false;
|
||||||
|
if ($netzabweichung < 0.0) {
|
||||||
|
$nachIstleistungVerteilen = $istleistungGesamt > 0.0;
|
||||||
|
$zielGesamt = min($bisherigeGrenzeGesamt, $istleistungGesamt) + $netzabweichung;
|
||||||
|
} elseif ($netzabweichung > 0.0) {
|
||||||
|
$zielGesamt = $bisherigeGrenzeGesamt + $netzabweichung;
|
||||||
|
}
|
||||||
|
$zielGesamt = max(0.0, min($zielGesamt, $nennleistungGesamt));
|
||||||
|
|
||||||
|
$grenzen = [];
|
||||||
|
foreach ($normalisiert as $eintrag) {
|
||||||
|
$anteil = $nachIstleistungVerteilen
|
||||||
|
? $eintrag['Istleistung_W'] / $istleistungGesamt
|
||||||
|
: $eintrag['Nennleistung_W'] / $nennleistungGesamt;
|
||||||
|
$grenzen[$eintrag['ID']] = round(
|
||||||
|
min($eintrag['Nennleistung_W'], $zielGesamt * $anteil),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Netzabweichung_W' => $netzabweichung,
|
||||||
|
'ZielGesamtleistung_W' => round($zielGesamt, 3),
|
||||||
|
'Abregelbedarf_W' => round(max(0.0, $istleistungGesamt - $zielGesamt), 3),
|
||||||
|
'Grenzen_W' => $grenzen,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stellwert(float $grenzeW, float $nennleistungW, string $art): float
|
||||||
|
{
|
||||||
|
if ($nennleistungW <= 0.0 || $grenzeW < 0.0) {
|
||||||
|
throw new InvalidArgumentException('Leistungsgrenzen sind ungueltig.');
|
||||||
|
}
|
||||||
|
$grenzeW = min($grenzeW, $nennleistungW);
|
||||||
|
if ($art === 'percent') {
|
||||||
|
return round($grenzeW / $nennleistungW * 100.0, 2);
|
||||||
|
}
|
||||||
|
if ($art === 'watt') {
|
||||||
|
return round($grenzeW, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Begrenzungsart muss percent oder watt sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
final class EnergieMessung
|
||||||
|
{
|
||||||
|
private const WATTSEKUNDEN_PRO_KWH = 3600000.0;
|
||||||
|
|
||||||
|
public static function hausleistung(float $pv, float $netz, float $batterie): float
|
||||||
|
{
|
||||||
|
return max(
|
||||||
|
0.0,
|
||||||
|
self::endlich($pv) + self::endlich($netz) - self::endlich($batterie)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hausenergie(
|
||||||
|
float $pv,
|
||||||
|
float $netzbezug,
|
||||||
|
float $netzeinspeisung,
|
||||||
|
float $batterieLaden,
|
||||||
|
float $batterieEntladen
|
||||||
|
): float {
|
||||||
|
return max(
|
||||||
|
0.0,
|
||||||
|
self::endlich($pv)
|
||||||
|
+ self::endlich($netzbezug)
|
||||||
|
+ self::endlich($batterieEntladen)
|
||||||
|
- self::endlich($netzeinspeisung)
|
||||||
|
- self::endlich($batterieLaden)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{PV: float, Haus: float, Netz: float, Batterie: float} $leistungen
|
||||||
|
* @return array{
|
||||||
|
* PVEnergie: float,
|
||||||
|
* Hausenergie: float,
|
||||||
|
* NetzbezugEnergie: float,
|
||||||
|
* EinspeisungEnergie: float,
|
||||||
|
* BatterieLadenEnergie: float,
|
||||||
|
* BatterieEntladenEnergie: float
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function energieDeltas(array $leistungen, int $sekunden): array
|
||||||
|
{
|
||||||
|
$faktor = max(0, $sekunden) / self::WATTSEKUNDEN_PRO_KWH;
|
||||||
|
$pv = self::endlich($leistungen['PV']);
|
||||||
|
$haus = self::endlich($leistungen['Haus']);
|
||||||
|
$netz = self::endlich($leistungen['Netz']);
|
||||||
|
$batterie = self::endlich($leistungen['Batterie']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'PVEnergie' => max(0.0, $pv) * $faktor,
|
||||||
|
'Hausenergie' => max(0.0, $haus) * $faktor,
|
||||||
|
'NetzbezugEnergie' => max(0.0, $netz) * $faktor,
|
||||||
|
'EinspeisungEnergie' => max(0.0, -$netz) * $faktor,
|
||||||
|
'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor,
|
||||||
|
'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function endlich(float $wert): float
|
||||||
|
{
|
||||||
|
return is_finite($wert) ? $wert : 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ final class Lizenzpruefung
|
|||||||
{
|
{
|
||||||
public const MANAGER_STANDARD = 'manager_standard';
|
public const MANAGER_STANDARD = 'manager_standard';
|
||||||
public const MANAGER_PEAK = 'manager_peak';
|
public const MANAGER_PEAK = 'manager_peak';
|
||||||
|
public const PROGNOSE_PV = 'forecast_pv';
|
||||||
|
public const PROGNOSE_VERBRAUCH = 'forecast_load';
|
||||||
|
public const NETZFAHRPLAN = 'grid_schedule';
|
||||||
|
|
||||||
public static function normalisiereCode(string $code): string
|
public static function normalisiereCode(string $code): string
|
||||||
{
|
{
|
||||||
@@ -114,6 +117,42 @@ final class Lizenzpruefung
|
|||||||
return $menge;
|
return $menge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rechnet freie, bestehende Einzellizenzen auf nicht separat lizenzierte Waermepumpen an.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $lease
|
||||||
|
* @param array<string, int> $verwendeteMengen
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
public static function beruecksichtigeBestandslizenzFuerWaermepumpen(
|
||||||
|
array $lease,
|
||||||
|
array $verwendeteMengen
|
||||||
|
): array {
|
||||||
|
$waermepumpen = $verwendeteMengen['heat_pump'] ?? 0;
|
||||||
|
if ($waermepumpen <= 0) {
|
||||||
|
return $verwendeteMengen;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nichtSeparatLizenziert = max(
|
||||||
|
0,
|
||||||
|
$waermepumpen - self::berechtigungsMenge($lease, 'heat_pump')
|
||||||
|
);
|
||||||
|
$verbraucher = $verwendeteMengen['consumer_single'] ?? 0;
|
||||||
|
$freieBestandslizenzen = max(
|
||||||
|
0,
|
||||||
|
self::berechtigungsMenge($lease, 'consumer_single') - $verbraucher
|
||||||
|
);
|
||||||
|
$angerechnet = min($nichtSeparatLizenziert, $freieBestandslizenzen);
|
||||||
|
if ($angerechnet === 0) {
|
||||||
|
return $verwendeteMengen;
|
||||||
|
}
|
||||||
|
|
||||||
|
$verwendeteMengen['heat_pump'] = $waermepumpen - $angerechnet;
|
||||||
|
$verwendeteMengen['consumer_single'] = $verbraucher + $angerechnet;
|
||||||
|
|
||||||
|
return $verwendeteMengen;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $lease
|
* @param array<string, mixed> $lease
|
||||||
* @param array<string, int> $verwendeteMengen
|
* @param array<string, int> $verwendeteMengen
|
||||||
|
|||||||
@@ -0,0 +1,542 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
trait ManagerEnergieTrait
|
||||||
|
{
|
||||||
|
private function konfiguriereEnergiearchiv(): void
|
||||||
|
{
|
||||||
|
$archivID = $this->archivID();
|
||||||
|
if ($archivID === 0) {
|
||||||
|
$this->protokolliere('Energiearchiv', 'Kein Archiv Control vorhanden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$aktiv = $this->ReadPropertyBoolean('EnergieaufzeichnungAktiv');
|
||||||
|
$geaendert = [];
|
||||||
|
$leistungstyp = $this->verdichtungstyp(
|
||||||
|
$this->ReadPropertyInteger('LeistungsaufzeichnungMinuten')
|
||||||
|
);
|
||||||
|
$energietyp = $this->verdichtungstyp(
|
||||||
|
$this->ReadPropertyInteger('EnergieaufzeichnungMinuten')
|
||||||
|
);
|
||||||
|
foreach (self::LEISTUNGS_VARIABLEN as $ident) {
|
||||||
|
$variableID = $this->GetIDForIdent($ident);
|
||||||
|
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
|
||||||
|
AC_SetAggregationType($archivID, $variableID, 0);
|
||||||
|
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
||||||
|
$regeln = [-1 => $leistungstyp];
|
||||||
|
$loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate');
|
||||||
|
if ($loeschen > 0) {
|
||||||
|
$regeln[$loeschen] = 7;
|
||||||
|
}
|
||||||
|
if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) {
|
||||||
|
$geaendert[] = $variableID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::ENERGIE_VARIABLEN as $ident) {
|
||||||
|
$variableID = $this->GetIDForIdent($ident);
|
||||||
|
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
|
||||||
|
AC_SetAggregationType($archivID, $variableID, 1);
|
||||||
|
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
||||||
|
$regeln = [-1 => $energietyp];
|
||||||
|
$verdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate');
|
||||||
|
if ($verdichten > 0) {
|
||||||
|
$regeln[$verdichten] = 3;
|
||||||
|
}
|
||||||
|
$loeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate');
|
||||||
|
if ($loeschen > 0) {
|
||||||
|
$regeln[$loeschen] = 7;
|
||||||
|
}
|
||||||
|
if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) {
|
||||||
|
$geaendert[] = $variableID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IPS_ApplyChanges($archivID);
|
||||||
|
foreach (array_unique($geaendert) as $variableID) {
|
||||||
|
AC_ReAggregateVariable($archivID, $variableID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, int> $regeln */
|
||||||
|
private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool
|
||||||
|
{
|
||||||
|
ksort($regeln);
|
||||||
|
$aktuell = [];
|
||||||
|
$eintraege = AC_GetCompaction($archivID, $variableID);
|
||||||
|
if (is_array($eintraege)) {
|
||||||
|
foreach ($eintraege as $eintrag) {
|
||||||
|
$monat = (int) ($eintrag['MonthOffset'] ?? -999);
|
||||||
|
$typ = (int) ($eintrag['CompactionType'] ?? -1);
|
||||||
|
if ($monat >= -1 && $typ >= 0) {
|
||||||
|
$aktuell[$monat] = $typ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($aktuell);
|
||||||
|
if ($aktuell === $regeln) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (array_keys($aktuell) as $monat) {
|
||||||
|
AC_SetCompaction($archivID, $variableID, $monat, -1);
|
||||||
|
}
|
||||||
|
foreach ($regeln as $monat => $typ) {
|
||||||
|
AC_SetCompaction($archivID, $variableID, $monat, $typ);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function verdichtungstyp(int $minuten): int
|
||||||
|
{
|
||||||
|
return [1 => 0, 5 => 1, 60 => 2][$minuten] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function archivID(): int
|
||||||
|
{
|
||||||
|
$archive = IPS_GetInstanceListByModuleID(self::ARCHIVE_MODULE_ID);
|
||||||
|
|
||||||
|
return $archive === [] ? 0 : (int) $archive[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function synchronisiereEnergievisualisierungen(): void
|
||||||
|
{
|
||||||
|
$this->synchronisiereEnergyPie($this->ReadPropertyBoolean('EnergyPieAnzeigen'));
|
||||||
|
$this->synchronisiereEnergiediagramme(
|
||||||
|
$this->ReadPropertyBoolean('EnergiediagrammeAnzeigen')
|
||||||
|
);
|
||||||
|
$this->synchronisiereFunFacts($this->ReadPropertyBoolean('FunFactsAnzeigen'));
|
||||||
|
$this->synchronisiereEnergiefluss($this->ReadPropertyBoolean('EnergieflussAnzeigen'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function synchronisiereEnergyPie(bool $anzeigen): void
|
||||||
|
{
|
||||||
|
if ($anzeigen) {
|
||||||
|
$this->erstelleEnergyPie();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->entferneInstanzUnterobjekt('EnergyPie', self::ENERGY_PIE_MODULE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function synchronisiereEnergiediagramme(bool $anzeigen): void
|
||||||
|
{
|
||||||
|
if ($anzeigen) {
|
||||||
|
$this->erstelleEnergiediagramme();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->entferneDiagrammUnterobjekt('Leistungsdiagramm');
|
||||||
|
$this->entferneDiagrammUnterobjekt('Energiediagramm');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function synchronisiereFunFacts(bool $anzeigen): void
|
||||||
|
{
|
||||||
|
if ($anzeigen) {
|
||||||
|
$this->erstelleFunFacts();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$variableID = $this->findeUnterobjekt('EnergieFunFacts');
|
||||||
|
if ($variableID === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((int) IPS_GetObject($variableID)['ObjectType'] !== 2) {
|
||||||
|
throw new RuntimeException('Unterobjekt EnergieFunFacts ist keine Variable.');
|
||||||
|
}
|
||||||
|
$this->UnregisterVariable('EnergieFunFacts');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function synchronisiereEnergiefluss(bool $anzeigen): void
|
||||||
|
{
|
||||||
|
if ($anzeigen) {
|
||||||
|
$this->erstelleEnergiefluss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$instanzID = $this->findeUnterobjekt('Energiefluss');
|
||||||
|
if ($instanzID === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$objekt = IPS_GetObject($instanzID);
|
||||||
|
$modulID = $objekt['ObjectType'] === 1
|
||||||
|
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||||
|
: '';
|
||||||
|
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
|
||||||
|
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
|
||||||
|
}
|
||||||
|
IPS_SetHidden($instanzID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entferneInstanzUnterobjekt(string $ident, string $modulID): void
|
||||||
|
{
|
||||||
|
$instanzID = $this->findeUnterobjekt($ident);
|
||||||
|
if ($instanzID === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$objekt = IPS_GetObject($instanzID);
|
||||||
|
$istModulID = $objekt['ObjectType'] === 1
|
||||||
|
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||||
|
: '';
|
||||||
|
if ($istModulID !== $modulID) {
|
||||||
|
throw new RuntimeException('Unterobjekt ' . $ident . ' hat einen unpassenden Typ.');
|
||||||
|
}
|
||||||
|
IPS_DeleteInstance($instanzID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entferneDiagrammUnterobjekt(string $ident): void
|
||||||
|
{
|
||||||
|
$medienID = $this->findeUnterobjekt($ident);
|
||||||
|
if ($medienID === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$objekt = IPS_GetObject($medienID);
|
||||||
|
if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) {
|
||||||
|
throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.');
|
||||||
|
}
|
||||||
|
IPS_DeleteMedia($medienID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function erstelleEnergyPie(): string
|
||||||
|
{
|
||||||
|
if (!in_array(self::ENERGY_PIE_MODULE_ID, IPS_GetModuleList(), true)) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
'Das Modul Energy Pie aus Enelix Utils ist nicht installiert.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$instanzID = $this->findeUnterobjekt('EnergyPie');
|
||||||
|
if ($instanzID === 0) {
|
||||||
|
$instanzID = IPS_CreateInstance(self::ENERGY_PIE_MODULE_ID);
|
||||||
|
IPS_SetParent($instanzID, $this->InstanceID);
|
||||||
|
IPS_SetIdent($instanzID, 'EnergyPie');
|
||||||
|
} else {
|
||||||
|
$objekt = IPS_GetObject($instanzID);
|
||||||
|
$modulID = $objekt['ObjectType'] === 1
|
||||||
|
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||||
|
: '';
|
||||||
|
if ($modulID !== self::ENERGY_PIE_MODULE_ID) {
|
||||||
|
throw new RuntimeException('Unterobjekt EnergyPie hat einen unpassenden Typ.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IPS_SetName($instanzID, 'Energy Pie');
|
||||||
|
IPS_SetPosition($instanzID, 500);
|
||||||
|
IPS_SetHidden($instanzID, false);
|
||||||
|
foreach ([
|
||||||
|
'ProduktionZaehlerID' => $this->GetIDForIdent('PVEnergie'),
|
||||||
|
'EinspeisungZaehlerID' => $this->GetIDForIdent('EinspeisungEnergie'),
|
||||||
|
'NetzbezugZaehlerID' => $this->GetIDForIdent('NetzbezugEnergie'),
|
||||||
|
'VerbrauchZaehlerID' => $this->GetIDForIdent('Hausenergie'),
|
||||||
|
'Zaehlerfaktor' => 1.0,
|
||||||
|
] as $property => $wert) {
|
||||||
|
IPS_SetProperty($instanzID, $property, $wert);
|
||||||
|
}
|
||||||
|
IPS_ApplyChanges($instanzID);
|
||||||
|
|
||||||
|
return 'Energy Pie wurde erstellt oder aktualisiert.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function erstelleEnergiefluss(): string
|
||||||
|
{
|
||||||
|
if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) {
|
||||||
|
throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.');
|
||||||
|
}
|
||||||
|
$instanzID = $this->findeUnterobjekt('Energiefluss');
|
||||||
|
if ($instanzID === 0) {
|
||||||
|
$instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID);
|
||||||
|
IPS_SetParent($instanzID, $this->InstanceID);
|
||||||
|
IPS_SetIdent($instanzID, 'Energiefluss');
|
||||||
|
} else {
|
||||||
|
$objekt = IPS_GetObject($instanzID);
|
||||||
|
$modulID = $objekt['ObjectType'] === 1
|
||||||
|
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||||
|
: '';
|
||||||
|
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
|
||||||
|
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IPS_SetName($instanzID, 'Energiefluss');
|
||||||
|
IPS_SetPosition($instanzID, 540);
|
||||||
|
IPS_SetHidden($instanzID, false);
|
||||||
|
$knoten = [
|
||||||
|
$this->energieflussKnoten('PVLeistungArchiv', 'PV-Anlage', 'solar'),
|
||||||
|
$this->energieflussKnoten('NetzleistungArchiv', 'Stromnetz', 'grid'),
|
||||||
|
$this->energieflussKnoten('HausverbrauchLeistungArchiv', 'Hausverbrauch', 'consumer'),
|
||||||
|
$this->energieflussKnoten('BatterieleistungArchiv', 'Batteriespeicher', 'battery'),
|
||||||
|
];
|
||||||
|
IPS_SetProperty($instanzID, 'Calculation', true);
|
||||||
|
IPS_SetProperty(
|
||||||
|
$instanzID,
|
||||||
|
'Config',
|
||||||
|
json_encode($knoten, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
|
||||||
|
);
|
||||||
|
IPS_SetProperty($instanzID, 'EnableLimits', false);
|
||||||
|
IPS_SetProperty($instanzID, 'Restrictions', '[]');
|
||||||
|
IPS_ApplyChanges($instanzID);
|
||||||
|
|
||||||
|
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function energieflussKnoten(string $ident, string $name, string $typ): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'VariableConfigType' => 0,
|
||||||
|
'VariableID' => $this->GetIDForIdent($ident),
|
||||||
|
'SecondaryVariableID' => 1,
|
||||||
|
'Name' => $name,
|
||||||
|
'Type' => $typ,
|
||||||
|
'Grouped' => false,
|
||||||
|
'CalculationType' => 0,
|
||||||
|
'InfoVariableID' => 1,
|
||||||
|
'Invert' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function erstelleEnergiediagramme(): string
|
||||||
|
{
|
||||||
|
$leistung = [
|
||||||
|
$this->chartDatensatz('PVLeistungArchiv', 'PV', '#F4C542', '~Power'),
|
||||||
|
$this->chartDatensatz('HausverbrauchLeistungArchiv', 'Haus', '#22A06B', '~Power'),
|
||||||
|
$this->chartDatensatz('NetzleistungArchiv', 'Netz', '#E95D5D', '~Power'),
|
||||||
|
$this->chartDatensatz('BatterieleistungArchiv', 'Batterie', '#8B5CF6', '~Power'),
|
||||||
|
];
|
||||||
|
$energie = [
|
||||||
|
$this->chartDatensatz('PVEnergie', 'PV', '#F4C542', '~Electricity'),
|
||||||
|
$this->chartDatensatz('Hausenergie', 'Haus', '#22A06B', '~Electricity'),
|
||||||
|
$this->chartDatensatz('NetzbezugEnergie', 'Netzbezug', '#E95D5D', '~Electricity'),
|
||||||
|
$this->chartDatensatz('EinspeisungEnergie', 'Einspeisung', '#2F80ED', '~Electricity'),
|
||||||
|
$this->chartDatensatz('BatterieLadenEnergie', 'Batterie laden', '#8B5CF6', '~Electricity'),
|
||||||
|
$this->chartDatensatz('BatterieEntladenEnergie', 'Batterie entladen', '#F59E0B', '~Electricity'),
|
||||||
|
];
|
||||||
|
$this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
|
||||||
|
$this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
|
||||||
|
|
||||||
|
return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.';
|
||||||
|
}
|
||||||
|
/** @param list<array<string, mixed>> $datensaetze */
|
||||||
|
private function erstelleDiagramm(
|
||||||
|
string $ident,
|
||||||
|
string $name,
|
||||||
|
array $datensaetze,
|
||||||
|
int $position
|
||||||
|
): int {
|
||||||
|
$medienID = $this->findeUnterobjekt($ident);
|
||||||
|
if ($medienID === 0) {
|
||||||
|
$medienID = IPS_CreateMedia(4);
|
||||||
|
IPS_SetParent($medienID, $this->InstanceID);
|
||||||
|
IPS_SetIdent($medienID, $ident);
|
||||||
|
}
|
||||||
|
$objekt = IPS_GetObject($medienID);
|
||||||
|
if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) {
|
||||||
|
throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.');
|
||||||
|
}
|
||||||
|
IPS_SetName($medienID, $name);
|
||||||
|
IPS_SetPosition($medienID, $position);
|
||||||
|
IPS_SetHidden($medienID, false);
|
||||||
|
IPS_SetMediaFile($medienID, 'media/' . $medienID . '.chart', false);
|
||||||
|
IPS_SetMediaContent(
|
||||||
|
$medienID,
|
||||||
|
base64_encode(json_encode(['datasets' => $datensaetze], JSON_THROW_ON_ERROR))
|
||||||
|
);
|
||||||
|
|
||||||
|
return $medienID;
|
||||||
|
}
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function chartDatensatz(
|
||||||
|
string $ident,
|
||||||
|
string $titel,
|
||||||
|
string $farbe,
|
||||||
|
string $profil
|
||||||
|
): array {
|
||||||
|
return [
|
||||||
|
'variableID' => $this->GetIDForIdent($ident),
|
||||||
|
'fillColor' => 'clear',
|
||||||
|
'strokeColor' => $farbe,
|
||||||
|
'timeOffset' => 0,
|
||||||
|
'visible' => true,
|
||||||
|
'title' => $titel,
|
||||||
|
'type' => 'line',
|
||||||
|
'side' => 'left',
|
||||||
|
'profile' => $profil,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findeUnterobjekt(string $ident): int
|
||||||
|
{
|
||||||
|
$objektID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
|
|
||||||
|
return is_int($objektID) && $objektID > 0 ? $objektID : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function erstelleFunFacts(): string
|
||||||
|
{
|
||||||
|
$this->RegisterVariableString('EnergieFunFacts', 'Energy Facts', '~HTMLBox', 530);
|
||||||
|
$variableID = $this->GetIDForIdent('EnergieFunFacts');
|
||||||
|
IPS_SetHidden($variableID, false);
|
||||||
|
$this->aktualisiereFunFacts($variableID);
|
||||||
|
|
||||||
|
return 'Energy Facts wurden erstellt oder aktualisiert.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiereFunFacts(int $variableID): void
|
||||||
|
{
|
||||||
|
$archivID = $this->archivID();
|
||||||
|
if ($archivID === 0) {
|
||||||
|
SetValue($variableID, '<div style="padding:16px">Archiv nicht verfuegbar.</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$heute = strtotime('today');
|
||||||
|
$jahr = mktime(0, 0, 0, 1, 1, (int) date('Y'));
|
||||||
|
$jetzt = time();
|
||||||
|
$pvID = $this->GetIDForIdent('PVEnergie');
|
||||||
|
$hausID = $this->GetIDForIdent('Hausenergie');
|
||||||
|
$bezugID = $this->GetIDForIdent('NetzbezugEnergie');
|
||||||
|
$exportID = $this->GetIDForIdent('EinspeisungEnergie');
|
||||||
|
$pvHeute = $this->energieSumme($archivID, $pvID, $heute, $jetzt);
|
||||||
|
$pvJahr = $this->energieSumme($archivID, $pvID, $jahr, $jetzt);
|
||||||
|
$pvTotal = max(0.0, (float) GetValue($pvID));
|
||||||
|
[$besterTag, $besterTagZeit] = $this->maximalerTageswert($archivID, $pvID);
|
||||||
|
[$maxPV, $maxPVZeit] = $this->maximaleLeistung(
|
||||||
|
$archivID,
|
||||||
|
$this->GetIDForIdent('PVLeistungArchiv')
|
||||||
|
);
|
||||||
|
[$maxHausTag, $maxHausTagZeit] = $this->maximalerTageswert($archivID, $hausID);
|
||||||
|
[$maxHaus, $maxHausZeit] = $this->maximaleLeistung(
|
||||||
|
$archivID,
|
||||||
|
$this->GetIDForIdent('HausverbrauchLeistungArchiv')
|
||||||
|
);
|
||||||
|
[$maxBezug, $maxBezugZeit] = $this->maximalerTageswert($archivID, $bezugID);
|
||||||
|
[$maxExport, $maxExportZeit] = $this->maximalerTageswert($archivID, $exportID);
|
||||||
|
|
||||||
|
$werte = [
|
||||||
|
'{{PV_HEUTE}}' => $this->formatiereEnergie($pvHeute),
|
||||||
|
'{{PV_JAHR}}' => $this->formatiereEnergie($pvJahr),
|
||||||
|
'{{PV_TOTAL}}' => $this->formatiereEnergie($pvTotal),
|
||||||
|
'{{BESTER_TAG}}' => $this->formatiereEnergie($besterTag),
|
||||||
|
'{{BESTER_TAG_DATUM}}' => $this->formatiereDatum($besterTagZeit),
|
||||||
|
'{{MAX_PV}}' => $this->formatiereLeistung($maxPV),
|
||||||
|
'{{MAX_PV_DATUM}}' => $this->formatiereDatum($maxPVZeit),
|
||||||
|
'{{MAX_HAUS_TAG}}' => $this->formatiereEnergie($maxHausTag),
|
||||||
|
'{{MAX_HAUS_TAG_DATUM}}' => $this->formatiereDatum($maxHausTagZeit),
|
||||||
|
'{{MAX_HAUS}}' => $this->formatiereLeistung($maxHaus),
|
||||||
|
'{{MAX_HAUS_DATUM}}' => $this->formatiereDatum($maxHausZeit),
|
||||||
|
'{{MAX_BEZUG}}' => $this->formatiereEnergie($maxBezug),
|
||||||
|
'{{MAX_BEZUG_DATUM}}' => $this->formatiereDatum($maxBezugZeit),
|
||||||
|
'{{MAX_EXPORT}}' => $this->formatiereEnergie($maxExport),
|
||||||
|
'{{MAX_EXPORT_DATUM}}' => $this->formatiereDatum($maxExportZeit),
|
||||||
|
'{{HEIZOEL}}' => number_format($pvTotal / 10.0, 0, ',', "'") . ' Liter',
|
||||||
|
'{{FLUGKM}}' => number_format($pvTotal * 2.4, 0, ',', "'") . ' km',
|
||||||
|
];
|
||||||
|
SetValue($variableID, strtr($this->funFactsHtml(), $werte));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function energieSumme(int $archivID, int $variableID, int $start, int $ende): float
|
||||||
|
{
|
||||||
|
$werte = @AC_GetAggregatedValues($archivID, $variableID, 1, $start, $ende, 0);
|
||||||
|
if (!is_array($werte)) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_sum(array_map(
|
||||||
|
static fn (array $wert): float => max(0.0, (float) ($wert['Avg'] ?? 0.0)),
|
||||||
|
$werte
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{0: float, 1: int} */
|
||||||
|
private function maximalerTageswert(int $archivID, int $variableID): array
|
||||||
|
{
|
||||||
|
$werte = @AC_GetAggregatedValues($archivID, $variableID, 1, 0, time(), 0);
|
||||||
|
$maximum = 0.0;
|
||||||
|
$zeit = 0;
|
||||||
|
if (is_array($werte)) {
|
||||||
|
foreach ($werte as $wert) {
|
||||||
|
$kandidat = max(0.0, (float) ($wert['Avg'] ?? 0.0));
|
||||||
|
if ($kandidat > $maximum) {
|
||||||
|
$maximum = $kandidat;
|
||||||
|
$zeit = (int) ($wert['TimeStamp'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$maximum, $zeit];
|
||||||
|
}
|
||||||
|
/** @return array{0: float, 1: int} */
|
||||||
|
private function maximaleLeistung(int $archivID, int $variableID): array
|
||||||
|
{
|
||||||
|
$werte = @AC_GetAggregatedValues($archivID, $variableID, 1, 0, time(), 0);
|
||||||
|
$maximum = 0.0;
|
||||||
|
$zeit = 0;
|
||||||
|
if (is_array($werte)) {
|
||||||
|
foreach ($werte as $wert) {
|
||||||
|
$kandidat = max(0.0, (float) ($wert['Max'] ?? 0.0));
|
||||||
|
if ($kandidat > $maximum) {
|
||||||
|
$maximum = $kandidat;
|
||||||
|
$zeit = (int) ($wert['MaxTime'] ?? $wert['TimeStamp'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$maximum, $zeit];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatiereEnergie(float $wert): string
|
||||||
|
{
|
||||||
|
if ($wert >= 1000.0) {
|
||||||
|
return number_format($wert / 1000.0, 2, ',', "'") . ' MWh';
|
||||||
|
}
|
||||||
|
|
||||||
|
return number_format($wert, 1, ',', "'") . ' kWh';
|
||||||
|
}
|
||||||
|
private function formatiereLeistung(float $wert): string
|
||||||
|
{
|
||||||
|
if ($wert >= 10000.0) {
|
||||||
|
return number_format($wert / 1000.0, 1, ',', "'") . ' kW';
|
||||||
|
}
|
||||||
|
|
||||||
|
return number_format($wert, 0, ',', "'") . ' W';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatiereDatum(int $zeit): string
|
||||||
|
{
|
||||||
|
return $zeit > 0 ? date('d.m.Y', $zeit) : 'Noch keine Daten';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function funFactsHtml(): string
|
||||||
|
{
|
||||||
|
return <<<'HTML'
|
||||||
|
<style>
|
||||||
|
.enx-facts{box-sizing:border-box;font-family:system-ui,-apple-system,sans-serif;color:#182230;padding:16px;background:#f4f7f6}
|
||||||
|
.enx-facts *{box-sizing:border-box}.enx-facts-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
|
||||||
|
.enx-fact-panel{min-width:0;background:#fff;border:1px solid #dfe7e4;border-radius:8px;overflow:hidden;box-shadow:0 4px 18px rgba(17,44,36,.08)}
|
||||||
|
.enx-fact-head{padding:15px 18px 13px;border-bottom:3px solid #22a06b;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.enx-fact-head b{font-size:18px}.enx-fact-head span{font-size:11px;letter-spacing:.08em;color:#60736c}
|
||||||
|
.enx-fact-body{padding:5px 18px 10px}.enx-fact-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:11px 0;border-bottom:1px solid #edf1ef}
|
||||||
|
.enx-fact-row:last-child{border-bottom:0}.enx-fact-label{min-width:0;color:#52635d;font-size:13px}.enx-fact-value{text-align:right;font-size:15px;font-weight:700;white-space:nowrap}
|
||||||
|
.enx-fact-date{display:block;color:#81908b;font-size:11px;font-weight:500;margin-top:2px}.enx-fact-panel.grid .enx-fact-head{border-color:#2f80ed}
|
||||||
|
.enx-fact-panel.impact .enx-fact-head{border-color:#f4c542}.enx-impact{padding:18px}.enx-impact-value{font-size:30px;font-weight:750;line-height:1.15}
|
||||||
|
.enx-impact-label{font-size:12px;color:#60736c;margin:5px 0 20px}.enx-impact-note{font-size:11px;color:#81908b;line-height:1.45}
|
||||||
|
@media(max-width:820px){.enx-facts-grid{grid-template-columns:1fr}.enx-facts{padding:10px}}
|
||||||
|
</style>
|
||||||
|
<div class="enx-facts"><div class="enx-facts-grid">
|
||||||
|
<section class="enx-fact-panel"><div class="enx-fact-head"><b>Solar</b><span>ERZEUGUNG</span></div><div class="enx-fact-body">
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Heute</span><span class="enx-fact-value">{{PV_HEUTE}}</span></div>
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Dieses Jahr</span><span class="enx-fact-value">{{PV_JAHR}}</span></div>
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Total</span><span class="enx-fact-value">{{PV_TOTAL}}</span></div>
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Bester Tag</span><span class="enx-fact-value">{{BESTER_TAG}}<small class="enx-fact-date">{{BESTER_TAG_DATUM}}</small></span></div>
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Max. Leistung</span><span class="enx-fact-value">{{MAX_PV}}<small class="enx-fact-date">{{MAX_PV_DATUM}}</small></span></div>
|
||||||
|
</div></section>
|
||||||
|
<section class="enx-fact-panel grid"><div class="enx-fact-head"><b>Netz & Haus</b><span>SPITZENWERTE</span></div><div class="enx-fact-body">
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Max. Tagesverbrauch</span><span class="enx-fact-value">{{MAX_HAUS_TAG}}<small class="enx-fact-date">{{MAX_HAUS_TAG_DATUM}}</small></span></div>
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Max. Hauslast</span><span class="enx-fact-value">{{MAX_HAUS}}<small class="enx-fact-date">{{MAX_HAUS_DATUM}}</small></span></div>
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Max. Tagesbezug</span><span class="enx-fact-value">{{MAX_BEZUG}}<small class="enx-fact-date">{{MAX_BEZUG_DATUM}}</small></span></div>
|
||||||
|
<div class="enx-fact-row"><span class="enx-fact-label">Max. Einspeisung</span><span class="enx-fact-value">{{MAX_EXPORT}}<small class="enx-fact-date">{{MAX_EXPORT_DATUM}}</small></span></div>
|
||||||
|
</div></section>
|
||||||
|
<section class="enx-fact-panel impact"><div class="enx-fact-head"><b>Wirkung</b><span>VERGLEICHE</span></div><div class="enx-impact">
|
||||||
|
<div class="enx-impact-value">{{HEIZOEL}}</div><div class="enx-impact-label">rechnerisch kompensiertes Heizoel</div>
|
||||||
|
<div class="enx-impact-value">{{FLUGKM}}</div><div class="enx-impact-label">rechnerisch kompensierte Flugkilometer</div>
|
||||||
|
<div class="enx-impact-note">Vergleichswerte auf Basis von 10 kWh pro Liter Heizoel und 2,4 Flugkilometern pro erzeugter kWh.</div>
|
||||||
|
</div></section>
|
||||||
|
</div></div>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,33 @@ final class Nachrichtenvertrag
|
|||||||
self::pruefeZustand($daten['Zustand']);
|
self::pruefeZustand($daten['Zustand']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
|
*/
|
||||||
|
public static function enthaeltLeistungswert(array $leistungswerte, int $leistung): bool
|
||||||
|
{
|
||||||
|
foreach ($leistungswerte as $eintrag) {
|
||||||
|
if (is_int($eintrag)) {
|
||||||
|
if ($eintrag === $leistung) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($eintrag)
|
||||||
|
&& isset($eintrag['Von_W'], $eintrag['Bis_W'])
|
||||||
|
&& is_int($eintrag['Von_W'])
|
||||||
|
&& is_int($eintrag['Bis_W'])
|
||||||
|
&& $leistung >= $eintrag['Von_W']
|
||||||
|
&& $leistung <= $eintrag['Bis_W']
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $kopf
|
* @param mixed $kopf
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class StoerungsSnapshot
|
||||||
|
{
|
||||||
|
public const VERSION = '1.0';
|
||||||
|
|
||||||
|
private const SCHWEREGRADE = [
|
||||||
|
'critical' => 0,
|
||||||
|
'error' => 1,
|
||||||
|
'warning' => 2,
|
||||||
|
'info' => 3,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $meldungen
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function erstellen(
|
||||||
|
string $installationID,
|
||||||
|
int $managerID,
|
||||||
|
string $rolle,
|
||||||
|
bool $managerAktiv,
|
||||||
|
array $meldungen,
|
||||||
|
?int $zeitpunkt = null
|
||||||
|
): array {
|
||||||
|
if (preg_match(
|
||||||
|
'/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',
|
||||||
|
$installationID
|
||||||
|
) !== 1) {
|
||||||
|
throw new InvalidArgumentException('Die Installations-ID muss eine UUIDv4 sein.');
|
||||||
|
}
|
||||||
|
if ($managerID <= 0) {
|
||||||
|
throw new InvalidArgumentException('Die Manager-ID muss positiv sein.');
|
||||||
|
}
|
||||||
|
if (!in_array($rolle, ['standalone', 'main', 'submanager'], true)) {
|
||||||
|
throw new InvalidArgumentException('Die Managerrolle ist ungueltig.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$eindeutig = [];
|
||||||
|
foreach ($meldungen as $meldung) {
|
||||||
|
$normalisiert = self::normalisiereMeldung($meldung);
|
||||||
|
$schluessel = $normalisiert['sourceId'] . "\0" . $normalisiert['code'];
|
||||||
|
$eindeutig[$schluessel] = $normalisiert;
|
||||||
|
}
|
||||||
|
$meldungen = array_values($eindeutig);
|
||||||
|
usort($meldungen, static function (array $links, array $rechts): int {
|
||||||
|
$schweregrad = self::SCHWEREGRADE[$links['severity']]
|
||||||
|
<=> self::SCHWEREGRADE[$rechts['severity']];
|
||||||
|
if ($schweregrad !== 0) {
|
||||||
|
return $schweregrad;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$links['sourceName'], $links['code']]
|
||||||
|
<=> [$rechts['sourceName'], $rechts['code']];
|
||||||
|
});
|
||||||
|
// Das Portal akzeptiert hoechstens 100 Meldungen; die hoechste Prioritaet bleibt erhalten.
|
||||||
|
$meldungen = array_slice($meldungen, 0, 100);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'version' => self::VERSION,
|
||||||
|
'installationId' => strtolower($installationID),
|
||||||
|
'capturedAt' => gmdate('Y-m-d\TH:i:s\Z', $zeitpunkt ?? time()),
|
||||||
|
'manager' => [
|
||||||
|
'instanceId' => $managerID,
|
||||||
|
'role' => $rolle,
|
||||||
|
'active' => $managerAktiv,
|
||||||
|
],
|
||||||
|
'faults' => $meldungen,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $snapshot */
|
||||||
|
public static function zustandHash(array $snapshot): string
|
||||||
|
{
|
||||||
|
unset($snapshot['capturedAt']);
|
||||||
|
|
||||||
|
return hash('sha256', json_encode(
|
||||||
|
$snapshot,
|
||||||
|
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $meldung
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private static function normalisiereMeldung(array $meldung): array
|
||||||
|
{
|
||||||
|
$felder = ['sourceType', 'sourceId', 'sourceName', 'code', 'severity', 'message'];
|
||||||
|
if (array_diff($felder, array_keys($meldung)) !== []
|
||||||
|
|| array_diff(array_keys($meldung), $felder) !== []
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('Eine Stoerungsmeldung hat ungueltige Felder.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalisiert = [];
|
||||||
|
foreach ($felder as $feld) {
|
||||||
|
if (!is_string($meldung[$feld])) {
|
||||||
|
throw new InvalidArgumentException('Stoerungsfelder muessen Texte sein.');
|
||||||
|
}
|
||||||
|
$normalisiert[$feld] = trim((string) preg_replace('/\s+/', ' ', $meldung[$feld]));
|
||||||
|
}
|
||||||
|
if ($normalisiert['sourceType'] === ''
|
||||||
|
|| $normalisiert['sourceId'] === ''
|
||||||
|
|| $normalisiert['sourceName'] === ''
|
||||||
|
|| $normalisiert['code'] === ''
|
||||||
|
|| $normalisiert['message'] === ''
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('Stoerungsfelder duerfen nicht leer sein.');
|
||||||
|
}
|
||||||
|
if (!array_key_exists($normalisiert['severity'], self::SCHWEREGRADE)) {
|
||||||
|
throw new InvalidArgumentException('Der Stoerungsschweregrad ist ungueltig.');
|
||||||
|
}
|
||||||
|
if (strlen($normalisiert['sourceType']) > 40
|
||||||
|
|| strlen($normalisiert['sourceId']) > 120
|
||||||
|
|| strlen($normalisiert['sourceName']) > 160
|
||||||
|
|| strlen($normalisiert['code']) > 120
|
||||||
|
|| strlen($normalisiert['message']) > 500
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('Mindestens ein Stoerungsfeld ist zu lang.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalisiert;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\Anlagentopologie;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class AnlagentopologieTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testHybridwechselrichterVerknuepftPvUndBatterieMitGemeinsamerAcGrenze(): void
|
||||||
|
{
|
||||||
|
$topologie = Anlagentopologie::ausJson(
|
||||||
|
json_encode([[
|
||||||
|
'ID' => 'hybrid-1',
|
||||||
|
'Name' => 'Hybrid Ost',
|
||||||
|
'Typ' => 'hybrid',
|
||||||
|
'ACNennleistung_kW' => 10.0,
|
||||||
|
]], JSON_THROW_ON_ERROR),
|
||||||
|
json_encode([
|
||||||
|
[
|
||||||
|
'ID' => 'pv-ost',
|
||||||
|
'Name' => 'Ostdach',
|
||||||
|
'DCLeistung_kWp' => 6.4,
|
||||||
|
'Neigung_Grad' => 25.0,
|
||||||
|
'Azimut_Grad' => -90.0,
|
||||||
|
'WechselrichterID' => 'hybrid-1',
|
||||||
|
'MPPT' => 'A',
|
||||||
|
'Modulanzahl' => 16,
|
||||||
|
'Modulleistung_Wp' => 400.0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ID' => 'pv-west',
|
||||||
|
'Name' => 'Westdach',
|
||||||
|
'DCLeistung_kWp' => 6.4,
|
||||||
|
'Neigung_Grad' => 25.0,
|
||||||
|
'Azimut_Grad' => 90.0,
|
||||||
|
'WechselrichterID' => 'hybrid-1',
|
||||||
|
'MPPT' => 'B',
|
||||||
|
'Modulanzahl' => 16,
|
||||||
|
'Modulleistung_Wp' => 400.0,
|
||||||
|
],
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
json_encode([[
|
||||||
|
'ID' => 'bat-1',
|
||||||
|
'Name' => 'Hausspeicher',
|
||||||
|
'Nennkapazitaet_kWh' => 13.5,
|
||||||
|
'Nutzkapazitaet_kWh' => 12.8,
|
||||||
|
'MaxLadeleistung_kW' => 5.0,
|
||||||
|
'MaxEntladeleistung_kW' => 5.0,
|
||||||
|
'WechselrichterID' => 'hybrid-1',
|
||||||
|
'Kopplung' => 'hybrid',
|
||||||
|
]], JSON_THROW_ON_ERROR),
|
||||||
|
'2a518707-8d62-4d97-82ce-606c95726500'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(Anlagentopologie::VERSION, $topologie['version']);
|
||||||
|
self::assertSame(12.8, $topologie['summary']['pvDcKwp']);
|
||||||
|
self::assertSame(10.0, $topologie['summary']['pvAcKw']);
|
||||||
|
self::assertSame(12.8, $topologie['summary']['usableBatteryKwh']);
|
||||||
|
self::assertArrayNotHasKey('acExportLimitKw', $topologie['inverters'][0]);
|
||||||
|
self::assertArrayNotHasKey('acImportLimitKw', $topologie['inverters'][0]);
|
||||||
|
self::assertSame([[
|
||||||
|
'inverterId' => 'hybrid-1',
|
||||||
|
'acRatedKw' => 10.0,
|
||||||
|
'pvArrayIds' => ['pv-ost', 'pv-west'],
|
||||||
|
'batteryIds' => ['bat-1'],
|
||||||
|
]], $topologie['sharedAcLimits']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLeereTopologieBleibtFuerBestandsinstallationenGueltig(): void
|
||||||
|
{
|
||||||
|
$topologie = Anlagentopologie::ausJson(
|
||||||
|
'[]',
|
||||||
|
'[]',
|
||||||
|
'[]',
|
||||||
|
'2a518707-8d62-4d97-82ce-606c95726500'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([], $topologie['inverters']);
|
||||||
|
self::assertSame([], $topologie['pvArrays']);
|
||||||
|
self::assertSame([], $topologie['batteries']);
|
||||||
|
self::assertSame(0.0, $topologie['summary']['pvDcKwp']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPvFlaecheBenoetigtEinenVorhandenenPassendenWechselrichter(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('referenzierter Wechselrichter ist nicht vorhanden');
|
||||||
|
|
||||||
|
Anlagentopologie::ausJson(
|
||||||
|
'[]',
|
||||||
|
json_encode([[
|
||||||
|
'ID' => 'pv-1',
|
||||||
|
'DCLeistung_kWp' => 5.0,
|
||||||
|
'Neigung_Grad' => 30.0,
|
||||||
|
'Azimut_Grad' => 0.0,
|
||||||
|
'WechselrichterID' => 'wr-fehlt',
|
||||||
|
]], JSON_THROW_ON_ERROR),
|
||||||
|
'[]',
|
||||||
|
'2a518707-8d62-4d97-82ce-606c95726500'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDcBatterieBenoetigtEinenHybridwechselrichter(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('DC- und Hybridkopplung');
|
||||||
|
|
||||||
|
Anlagentopologie::ausJson(
|
||||||
|
json_encode([[
|
||||||
|
'ID' => 'bat-wr',
|
||||||
|
'Typ' => 'battery',
|
||||||
|
'ACNennleistung_kW' => 5.0,
|
||||||
|
]], JSON_THROW_ON_ERROR),
|
||||||
|
'[]',
|
||||||
|
json_encode([[
|
||||||
|
'ID' => 'bat-1',
|
||||||
|
'Nennkapazitaet_kWh' => 10.0,
|
||||||
|
'Nutzkapazitaet_kWh' => 9.0,
|
||||||
|
'MaxLadeleistung_kW' => 4.0,
|
||||||
|
'MaxEntladeleistung_kW' => 4.0,
|
||||||
|
'WechselrichterID' => 'bat-wr',
|
||||||
|
'Kopplung' => 'dc',
|
||||||
|
]], JSON_THROW_ON_ERROR),
|
||||||
|
'2a518707-8d62-4d97-82ce-606c95726500'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuldatenMuessenGemeinsamGesetztWerden(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Modulanzahl und Modulleistung');
|
||||||
|
|
||||||
|
Anlagentopologie::ausJson(
|
||||||
|
json_encode([[
|
||||||
|
'ID' => 'pv-wr',
|
||||||
|
'Typ' => 'pv',
|
||||||
|
'ACNennleistung_kW' => 5.0,
|
||||||
|
]], JSON_THROW_ON_ERROR),
|
||||||
|
json_encode([[
|
||||||
|
'ID' => 'pv-1',
|
||||||
|
'DCLeistung_kWp' => 6.0,
|
||||||
|
'Neigung_Grad' => 30.0,
|
||||||
|
'Azimut_Grad' => 0.0,
|
||||||
|
'WechselrichterID' => 'pv-wr',
|
||||||
|
'Modulanzahl' => 15,
|
||||||
|
'Modulleistung_Wp' => 0.0,
|
||||||
|
]], JSON_THROW_ON_ERROR),
|
||||||
|
'[]',
|
||||||
|
'2a518707-8d62-4d97-82ce-606c95726500'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,14 +10,14 @@ use PHPUnit\Framework\TestCase;
|
|||||||
|
|
||||||
final class BatterieReglerTest extends TestCase
|
final class BatterieReglerTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testLeistungsrasterBehaeltGrobeUndFeineSchritte(): void
|
public function testLeistungsbereichBleibtInnerhalbDynamischerGrenzen(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[['Von_W' => -999, 'Bis_W' => 1000]],
|
||||||
-1000, -750, -500, -250, -200, -150, -100, -50,
|
BatterieRegler::leistungsangebot(
|
||||||
0, 50, 100, 150, 200, 250, 500, 750, 1000,
|
true, true, false, 50.0, 20.0, 10.0, false,
|
||||||
],
|
0.0, 0.0, 1000.9, 999.9
|
||||||
BatterieRegler::leistungsraster(1000.0, 1000.0)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,12 +28,10 @@ final class BatterieReglerTest extends TestCase
|
|||||||
self::assertFalse(BatterieRegler::hystereseAktiv(22.1, 20.0, 2.0, true));
|
self::assertFalse(BatterieRegler::hystereseAktiv(22.1, 20.0, 2.0, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPvAngebotEntsprichtDemBisherigenSocVerhalten(): void
|
public function testPvAngebotVerwendetStufenloseBereicheMitBisherigerSocLogik(): void
|
||||||
{
|
{
|
||||||
$voll = BatterieRegler::leistungsraster(1000.0, 1000.0);
|
|
||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$voll,
|
[['Von_W' => -1000, 'Bis_W' => 1000]],
|
||||||
BatterieRegler::leistungsangebot(
|
BatterieRegler::leistungsangebot(
|
||||||
true, true, false, 50.0, 20.0, 10.0, false,
|
true, true, false, 50.0, 20.0, 10.0, false,
|
||||||
0.0, 0.0, 1000.0, 1000.0
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
@@ -47,7 +45,7 @@ final class BatterieReglerTest extends TestCase
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[-1000, -750, -500, -250, -200, -150, -100, -50, 0],
|
[['Von_W' => -1000, 'Bis_W' => 0]],
|
||||||
BatterieRegler::leistungsangebot(
|
BatterieRegler::leistungsangebot(
|
||||||
true, true, false, 100.0, 20.0, 10.0, false,
|
true, true, false, 100.0, 20.0, 10.0, false,
|
||||||
0.0, 0.0, 1000.0, 1000.0
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
@@ -66,6 +64,24 @@ final class BatterieReglerTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPeakEinschraenkungenWerdenAlsBereicheGemeldet(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[['Von_W' => -1000, 'Bis_W' => 0]],
|
||||||
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, true, true, 21.0, 20.0, 10.0, true,
|
||||||
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[['Von_W' => 0, 'Bis_W' => 1000]],
|
||||||
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, true, true, 10.0, 20.0, 10.0, true,
|
||||||
|
0.0, 0.0, 1000.0, 1000.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testWechselrichtersteuerungBietetNurNull(): void
|
public function testWechselrichtersteuerungBietetNurNull(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
@@ -186,6 +202,9 @@ final class BatterieReglerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->expectException(InvalidArgumentException::class);
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
BatterieRegler::leistungsraster(-1.0, 1000.0);
|
BatterieRegler::leistungsangebot(
|
||||||
|
true, true, false, 50.0, 20.0, 10.0, false,
|
||||||
|
0.0, 0.0, -1.0, 1000.0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,27 @@ final class DemoanlageBootstrapTest extends TestCase
|
|||||||
self::assertStringContainsString("'BatterieListe'", $source);
|
self::assertStringContainsString("'BatterieListe'", $source);
|
||||||
self::assertStringContainsString('ENELIX_DEMO_BUFFER_MODULE', $source);
|
self::assertStringContainsString('ENELIX_DEMO_BUFFER_MODULE', $source);
|
||||||
self::assertStringContainsString('ENELIX_DEMO_BATTERY_MODULE', $source);
|
self::assertStringContainsString('ENELIX_DEMO_BATTERY_MODULE', $source);
|
||||||
|
self::assertStringContainsString("'AnlagenWechselrichter'", $source);
|
||||||
|
self::assertStringContainsString("'AnlagenPVFlaechen'", $source);
|
||||||
|
self::assertStringContainsString("'AnlagenBatterien'", $source);
|
||||||
|
self::assertStringContainsString("'EnergyPieAnzeigen' => true", $source);
|
||||||
|
self::assertStringContainsString("'EnergiediagrammeAnzeigen' => true", $source);
|
||||||
|
self::assertStringContainsString("'FunFactsAnzeigen' => true", $source);
|
||||||
|
self::assertStringContainsString("'EnergieflussAnzeigen' => true", $source);
|
||||||
|
self::assertStringContainsString("'LeistungVariableID' => \$batteryActual", $source);
|
||||||
|
self::assertStringContainsString("'SOCVariableID' => \$batterySoc", $source);
|
||||||
|
self::assertStringContainsString("'PrognoseSendeintervall' => 60", $source);
|
||||||
|
self::assertStringContainsString("'IstleistungVariableID' => \$inverterActual", $source);
|
||||||
|
self::assertStringContainsString("'BegrenzungVariableID' => \$inverterLimit", $source);
|
||||||
|
self::assertStringContainsString("'Begrenzungsart' => 'watt'", $source);
|
||||||
|
self::assertStringContainsString("'EinspeisebegrenzungAktiv' => true", $source);
|
||||||
|
self::assertStringContainsString("'Einspeisegrenze' => 3000.0", $source);
|
||||||
|
self::assertStringContainsString("'PrognoseAktiv' => false", $source);
|
||||||
|
self::assertStringContainsString("'NetzfahrplanAktiv' => false", $source);
|
||||||
|
self::assertStringNotContainsString("'Einspeisetoleranz'", $source);
|
||||||
|
self::assertStringContainsString("'demo-hybrid-1'", $source);
|
||||||
|
self::assertStringContainsString("'demo-pv-1'", $source);
|
||||||
|
self::assertStringContainsString("'demo-battery-1'", $source);
|
||||||
self::assertStringContainsString("'EnergieflussPVkW'", $source);
|
self::assertStringContainsString("'EnergieflussPVkW'", $source);
|
||||||
self::assertStringContainsString("'~Power'", $source);
|
self::assertStringContainsString("'~Power'", $source);
|
||||||
self::assertStringContainsString("'~Electricity'", $source);
|
self::assertStringContainsString("'~Electricity'", $source);
|
||||||
@@ -181,6 +202,14 @@ final class DemoanlageBootstrapTest extends TestCase
|
|||||||
self::assertSame(0.0, enelixDemoBatteryRegisterPower(2, 0, 1500.0));
|
self::assertSame(0.0, enelixDemoBatteryRegisterPower(2, 0, 1500.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testWechselrichtergrenzeBegrenztNurDieTatsaechlichePvLeistung(): void
|
||||||
|
{
|
||||||
|
self::assertSame(3000.0, enelixDemoLimitPvPower(8000.0, 3000.0));
|
||||||
|
self::assertSame(2500.0, enelixDemoLimitPvPower(2500.0, 3000.0));
|
||||||
|
self::assertSame(0.0, enelixDemoLimitPvPower(-100.0, 3000.0));
|
||||||
|
self::assertSame(0.0, enelixDemoLimitPvPower(8000.0, -100.0));
|
||||||
|
}
|
||||||
|
|
||||||
public function testRollbackIstExplizitBestaetigungspflichtig(): void
|
public function testRollbackIstExplizitBestaetigungspflichtig(): void
|
||||||
{
|
{
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ final class EaseeModuleStrukturTest extends TestCase
|
|||||||
|
|
||||||
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
|
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
|
||||||
self::assertStringContainsString('EaseeLadestatus::ausBeobachtungen', $modul);
|
self::assertStringContainsString('EaseeLadestatus::ausBeobachtungen', $modul);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyBoolean('Ladefreigabe', true)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
self::assertStringContainsString("'FahrzeugVerbunden'", $modul);
|
self::assertStringContainsString("'FahrzeugVerbunden'", $modul);
|
||||||
self::assertStringContainsString("'Phasenzahl'", $modul);
|
self::assertStringContainsString("'Phasenzahl'", $modul);
|
||||||
self::assertStringNotContainsString('7500', $modul);
|
self::assertStringNotContainsString('7500', $modul);
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\EinspeiseRegler;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class EinspeiseReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testUeberschreitungWirdAufDieGesamtanlageBegrenzt(): void
|
||||||
|
{
|
||||||
|
$ergebnis = EinspeiseRegler::verteile(
|
||||||
|
-15000.0,
|
||||||
|
10000.0,
|
||||||
|
[
|
||||||
|
['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 10000],
|
||||||
|
['ID' => 'wr-2', 'Nennleistung_W' => 5000, 'Istleistung_W' => 5000],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(10000.0, $ergebnis['ZielGesamtleistung_W']);
|
||||||
|
self::assertSame(5000.0, $ergebnis['Abregelbedarf_W']);
|
||||||
|
self::assertSame(6666.667, $ergebnis['Grenzen_W']['wr-1']);
|
||||||
|
self::assertSame(3333.333, $ergebnis['Grenzen_W']['wr-2']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAbregelungFolgtDerTatsaechlichenErzeugung(): void
|
||||||
|
{
|
||||||
|
$ergebnis = EinspeiseRegler::verteile(
|
||||||
|
-10000.0,
|
||||||
|
5000.0,
|
||||||
|
[
|
||||||
|
['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 10000],
|
||||||
|
['ID' => 'wr-2', 'Nennleistung_W' => 10000, 'Istleistung_W' => 0],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(5000.0, $ergebnis['Grenzen_W']['wr-1']);
|
||||||
|
self::assertSame(0.0, $ergebnis['Grenzen_W']['wr-2']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFreierNetzspielraumHebtEineBestehendeBegrenzungAn(): void
|
||||||
|
{
|
||||||
|
$ergebnis = EinspeiseRegler::verteile(
|
||||||
|
-6000.0,
|
||||||
|
10000.0,
|
||||||
|
[
|
||||||
|
['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 4000],
|
||||||
|
['ID' => 'wr-2', 'Nennleistung_W' => 10000, 'Istleistung_W' => 2000],
|
||||||
|
],
|
||||||
|
['wr-1' => 5000, 'wr-2' => 5000]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(14000.0, $ergebnis['ZielGesamtleistung_W']);
|
||||||
|
self::assertSame(7000.0, $ergebnis['Grenzen_W']['wr-1']);
|
||||||
|
self::assertSame(7000.0, $ergebnis['Grenzen_W']['wr-2']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegelungFolgtAuchKleinenAbweichungenStufenlos(): void
|
||||||
|
{
|
||||||
|
$ergebnis = EinspeiseRegler::verteile(
|
||||||
|
-10050.0,
|
||||||
|
10000.0,
|
||||||
|
[['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 9000]],
|
||||||
|
['wr-1' => 8000]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(7950.0, $ergebnis['ZielGesamtleistung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStellwerteWerdenInProzentOderWattAusgegeben(): void
|
||||||
|
{
|
||||||
|
self::assertSame(62.5, EinspeiseRegler::stellwert(5000.0, 8000.0, 'percent'));
|
||||||
|
self::assertSame(5000.0, EinspeiseRegler::stellwert(5000.0, 8000.0, 'watt'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUngueltigeBegrenzungsartWirdAbgelehnt(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
EinspeiseRegler::stellwert(5000.0, 8000.0, 'volt');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\EnergieMessung;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class EnergieMessungTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testIntegriertVierLeistungenMitVorzeichenrichtung(): void
|
||||||
|
{
|
||||||
|
$werte = EnergieMessung::energieDeltas([
|
||||||
|
'PV' => 7200.0,
|
||||||
|
'Haus' => 3600.0,
|
||||||
|
'Netz' => -1800.0,
|
||||||
|
'Batterie' => 900.0,
|
||||||
|
], 300);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(0.6, $werte['PVEnergie'], 0.000001);
|
||||||
|
self::assertEqualsWithDelta(0.3, $werte['Hausenergie'], 0.000001);
|
||||||
|
self::assertSame(0.0, $werte['NetzbezugEnergie']);
|
||||||
|
self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001);
|
||||||
|
self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001);
|
||||||
|
self::assertSame(0.0, $werte['BatterieEntladenEnergie']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTrenntBezugUndBatterieentladung(): void
|
||||||
|
{
|
||||||
|
$werte = EnergieMessung::energieDeltas([
|
||||||
|
'PV' => -10.0,
|
||||||
|
'Haus' => -10.0,
|
||||||
|
'Netz' => 1000.0,
|
||||||
|
'Batterie' => -2000.0,
|
||||||
|
], 3600);
|
||||||
|
|
||||||
|
self::assertSame(0.0, $werte['PVEnergie']);
|
||||||
|
self::assertSame(0.0, $werte['Hausenergie']);
|
||||||
|
self::assertSame(1.0, $werte['NetzbezugEnergie']);
|
||||||
|
self::assertSame(0.0, $werte['EinspeisungEnergie']);
|
||||||
|
self::assertSame(0.0, $werte['BatterieLadenEnergie']);
|
||||||
|
self::assertSame(2.0, $werte['BatterieEntladenEnergie']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBerechnetHausverbrauchAusDerAnlagenbilanz(): void
|
||||||
|
{
|
||||||
|
self::assertSame(2700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0));
|
||||||
|
self::assertSame(3500.0, EnergieMessung::hausleistung(2000.0, 500.0, -1000.0));
|
||||||
|
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBerechnetHausenergieAusDenZaehlerdeltas(): void
|
||||||
|
{
|
||||||
|
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0));
|
||||||
|
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 5.0, 0.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
|
||||||
|
{
|
||||||
|
$werte = EnergieMessung::energieDeltas([
|
||||||
|
'PV' => INF,
|
||||||
|
'Haus' => NAN,
|
||||||
|
'Netz' => 1000.0,
|
||||||
|
'Batterie' => -1000.0,
|
||||||
|
], -1);
|
||||||
|
|
||||||
|
foreach ($werte as $wert) {
|
||||||
|
self::assertSame(0.0, $wert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,10 @@ final class LadestationStandAloneModulstrukturTest extends TestCase
|
|||||||
{
|
{
|
||||||
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyBoolean('Ladefreigabe', true)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
self::assertStringContainsString("EnableAction('Ladefreigabe')", $modul);
|
self::assertStringContainsString("EnableAction('Ladefreigabe')", $modul);
|
||||||
self::assertStringContainsString("EnableAction('Solarladen')", $modul);
|
self::assertStringContainsString("EnableAction('Solarladen')", $modul);
|
||||||
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ final class LibraryMetadataTest extends TestCase
|
|||||||
self::assertSame('Enelix EMS', $daten['name']);
|
self::assertSame('Enelix EMS', $daten['name']);
|
||||||
self::assertSame('Enelix', $daten['author']);
|
self::assertSame('Enelix', $daten['author']);
|
||||||
self::assertSame('8.0', $daten['compatibility']['version']);
|
self::assertSame('8.0', $daten['compatibility']['version']);
|
||||||
|
self::assertSame('0.1', $daten['version']);
|
||||||
|
self::assertSame(1, $daten['build']);
|
||||||
|
self::assertGreaterThan(0, $daten['date']);
|
||||||
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $daten['id']);
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $daten['id']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,61 @@ final class LizenzpruefungTest extends TestCase
|
|||||||
self::assertTrue(true);
|
self::assertTrue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSeparateWaermepumpenlizenzBleibtUnveraendert(): void
|
||||||
|
{
|
||||||
|
$lease = $this->lease('manager_standard');
|
||||||
|
$lease['entitlements'][] = [
|
||||||
|
'catalog_key' => 'heat_pump',
|
||||||
|
'quantity' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
$verwendung = ['heat_pump' => 1];
|
||||||
|
|
||||||
|
$pruefmengen = Lizenzpruefung::beruecksichtigeBestandslizenzFuerWaermepumpen(
|
||||||
|
$lease,
|
||||||
|
$verwendung
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame($verwendung, $pruefmengen);
|
||||||
|
Lizenzpruefung::pruefeVerbrauchermengen($lease, $pruefmengen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFreieBestandslizenzDecktEineWaermepumpeAb(): void
|
||||||
|
{
|
||||||
|
$lease = $this->lease('manager_standard');
|
||||||
|
$lease['entitlements'][] = [
|
||||||
|
'catalog_key' => 'consumer_single',
|
||||||
|
'quantity' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
$pruefmengen = Lizenzpruefung::beruecksichtigeBestandslizenzFuerWaermepumpen(
|
||||||
|
$lease,
|
||||||
|
['heat_pump' => 1]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(['heat_pump' => 0, 'consumer_single' => 1], $pruefmengen);
|
||||||
|
Lizenzpruefung::pruefeVerbrauchermengen($lease, $pruefmengen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBestandslizenzDarfNichtDoppeltVerwendetWerden(): void
|
||||||
|
{
|
||||||
|
$lease = $this->lease('manager_standard');
|
||||||
|
$lease['entitlements'][] = [
|
||||||
|
'catalog_key' => 'consumer_single',
|
||||||
|
'quantity' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
$pruefmengen = Lizenzpruefung::beruecksichtigeBestandslizenzFuerWaermepumpen(
|
||||||
|
$lease,
|
||||||
|
['consumer_single' => 1, 'heat_pump' => 1]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Lizenzkontingent fuer heat_pump ueberschritten');
|
||||||
|
Lizenzpruefung::pruefeVerbrauchermengen($lease, $pruefmengen);
|
||||||
|
}
|
||||||
|
|
||||||
public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void
|
public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ManagerEnergieModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testManagerKonfiguriertMessquellenUndArchivregeln(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
$trait = (string) file_get_contents(__DIR__ . '/../libs/ManagerEnergieTrait.php');
|
||||||
|
$formular = (string) file_get_contents(__DIR__ . '/../Manager/form.json');
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
"RegisterPropertyBoolean('EnergyPieAnzeigen', false)",
|
||||||
|
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
|
||||||
|
"RegisterPropertyBoolean('FunFactsAnzeigen', false)",
|
||||||
|
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
|
||||||
|
"RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)",
|
||||||
|
"RegisterPropertyInteger('LeistungLoeschenMonate', 12)",
|
||||||
|
"RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)",
|
||||||
|
"RegisterPropertyInteger('EnergieVerdichtenMonate', 12)",
|
||||||
|
"RegisterPropertyInteger('EnergieLoeschenMonate', 0)",
|
||||||
|
] as $erwartet) {
|
||||||
|
self::assertStringContainsString($erwartet, $modul);
|
||||||
|
}
|
||||||
|
foreach ([
|
||||||
|
'AC_SetLoggingStatus',
|
||||||
|
'AC_SetAggregationType',
|
||||||
|
'AC_SetCompaction',
|
||||||
|
'AC_ReAggregateVariable',
|
||||||
|
'IPS_CreateMedia(4)',
|
||||||
|
'IPS_SetMediaContent',
|
||||||
|
'ENERGY_PIE_MODULE_ID',
|
||||||
|
'ENERGY_FLOW_MODULE_ID',
|
||||||
|
'energieflussKnoten',
|
||||||
|
'IPS_DeleteInstance',
|
||||||
|
'IPS_DeleteMedia',
|
||||||
|
] as $erwartet) {
|
||||||
|
self::assertStringContainsString($erwartet, $modul . $trait);
|
||||||
|
}
|
||||||
|
foreach ([
|
||||||
|
'Energieaufzeichnung',
|
||||||
|
'FormEnergyPieAnzeigen',
|
||||||
|
'FormEnergiediagrammeAnzeigen',
|
||||||
|
'FormFunFactsAnzeigen',
|
||||||
|
'FormEnergieflussAnzeigen',
|
||||||
|
'MesswerteAnzeigen',
|
||||||
|
'IstleistungVariableID',
|
||||||
|
'LeistungVariableID',
|
||||||
|
'SOCVariableID',
|
||||||
|
'NetzbezugEnergieVariableID',
|
||||||
|
'NetzeinspeisungEnergieVariableID',
|
||||||
|
'ErzeugungsenergieVariableID',
|
||||||
|
'LadeenergieVariableID',
|
||||||
|
'EntladeenergieVariableID',
|
||||||
|
] as $erwartet) {
|
||||||
|
self::assertStringContainsString($erwartet, $formular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testManagerFuehrtAlleMessvariablenUndRichtungszaehler(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'PVLeistungArchiv',
|
||||||
|
'HausverbrauchLeistungArchiv',
|
||||||
|
'NetzleistungArchiv',
|
||||||
|
'BatterieleistungArchiv',
|
||||||
|
'PVEnergie',
|
||||||
|
'Hausenergie',
|
||||||
|
'NetzbezugEnergie',
|
||||||
|
'EinspeisungEnergie',
|
||||||
|
'BatterieLadenEnergie',
|
||||||
|
'BatterieEntladenEnergie',
|
||||||
|
] as $ident) {
|
||||||
|
self::assertStringContainsString("'" . $ident . "'", $modul);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString("RegisterTimer(
|
||||||
|
'MesswerteAufzeichnen'", $modul);
|
||||||
|
self::assertStringContainsString('EnergieMessung::energieDeltas', $modul);
|
||||||
|
self::assertStringContainsString('EnergieMessung::hausenergie', $modul);
|
||||||
|
self::assertStringContainsString("RegisterAttributeString('MessungZaehlerstaende'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFunFactsSindResponsivUndErklaerenUmrechnungen(): void
|
||||||
|
{
|
||||||
|
$trait = (string) file_get_contents(__DIR__ . '/../libs/ManagerEnergieTrait.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('grid-template-columns:repeat(3,minmax(0,1fr))', $trait);
|
||||||
|
self::assertStringContainsString('@media(max-width:820px)', $trait);
|
||||||
|
self::assertStringContainsString('10 kWh pro Liter Heizoel', $trait);
|
||||||
|
self::assertStringContainsString('2,4 Flugkilometern', $trait);
|
||||||
|
self::assertStringContainsString('AC_GetAggregatedValues', $trait);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,18 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
'VerbraucherTimeout',
|
'VerbraucherTimeout',
|
||||||
'MesswertMaxAlter',
|
'MesswertMaxAlter',
|
||||||
'Umschaltdifferenz',
|
'Umschaltdifferenz',
|
||||||
|
'EinspeisebegrenzungAktiv',
|
||||||
|
'Einspeisegrenze',
|
||||||
|
'PrognoseAktiv',
|
||||||
|
'PrognoseLizenzInformation',
|
||||||
|
'NetzfahrplanAktiv',
|
||||||
|
'NetzbezugEnergieVariableID',
|
||||||
|
'NetzeinspeisungEnergieVariableID',
|
||||||
|
'IstleistungVariableID',
|
||||||
|
'ErzeugungsenergieVariableID',
|
||||||
|
'Erzeugungsenergiefaktor',
|
||||||
|
'BegrenzungVariableID',
|
||||||
|
'Begrenzungsart',
|
||||||
'DiagnosevariablenAnzeigen',
|
'DiagnosevariablenAnzeigen',
|
||||||
'LoggingEin',
|
'LoggingEin',
|
||||||
'Lizenzcode',
|
'Lizenzcode',
|
||||||
@@ -56,11 +68,29 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
'LizenzInstallationID',
|
'LizenzInstallationID',
|
||||||
'LizenzInformation',
|
'LizenzInformation',
|
||||||
'VerbraucherAktualisieren',
|
'VerbraucherAktualisieren',
|
||||||
|
'AnlagenWechselrichter',
|
||||||
|
'AnlagenPVFlaechen',
|
||||||
|
'AnlagenBatterien',
|
||||||
|
'Istleistungsfaktor',
|
||||||
|
'LeistungVariableID',
|
||||||
|
'Leistungsfaktor',
|
||||||
|
'SOCVariableID',
|
||||||
|
'LadeenergieVariableID',
|
||||||
|
'EntladeenergieVariableID',
|
||||||
|
'Energiefaktor',
|
||||||
|
'EnergyPieAnzeigen',
|
||||||
|
'EnergiediagrammeAnzeigen',
|
||||||
|
'FunFactsAnzeigen',
|
||||||
|
'EnergieflussAnzeigen',
|
||||||
|
'PrognoseSendeintervall',
|
||||||
|
'StoerueberwachungAktiv',
|
||||||
|
'StoerungsSendeintervall',
|
||||||
] as $property) {
|
] as $property) {
|
||||||
self::assertStringContainsString($property, $json);
|
self::assertStringContainsString($property, $json);
|
||||||
}
|
}
|
||||||
foreach (['Regelintervall', 'Vorgabeintervall', 'UmschaltschwelleUeberschuss',
|
foreach (['Regelintervall', 'Vorgabeintervall', 'UmschaltschwelleUeberschuss',
|
||||||
'UmschaltschwelleLastspitze', 'Optionale obere Anschluesse', 'LizenzAnschluss'] as $entfernt
|
'UmschaltschwelleLastspitze', 'Optionale obere Anschluesse', 'LizenzAnschluss',
|
||||||
|
'Einspeisetoleranz', 'ACEinspeisegrenze_kW', 'ACBezugsgrenze_kW'] as $entfernt
|
||||||
) {
|
) {
|
||||||
self::assertStringNotContainsString($entfernt, $json);
|
self::assertStringNotContainsString($entfernt, $json);
|
||||||
}
|
}
|
||||||
@@ -139,12 +169,13 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
'Konstant und Monatlich benoetigen eine Peak-Shaving-Lizenz.',
|
'Konstant und Monatlich benoetigen eine Peak-Shaving-Lizenz.',
|
||||||
$inhalt
|
$inhalt
|
||||||
);
|
);
|
||||||
foreach (['battery', 'buffer_storage', 'ev_charger', 'consumer_single', 'boiler_multi'] as $katalog) {
|
foreach (['battery', 'buffer_storage', 'ev_charger', 'consumer_single', 'heat_pump', 'boiler_multi'] as $katalog) {
|
||||||
self::assertStringContainsString("'Katalog' => '" . $katalog . "'", $inhalt);
|
self::assertStringContainsString("'Katalog' => '" . $katalog . "'", $inhalt);
|
||||||
}
|
}
|
||||||
foreach ([
|
foreach ([
|
||||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
||||||
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||||
|
'{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}',
|
||||||
'{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}',
|
||||||
@@ -171,6 +202,120 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
self::assertStringNotContainsString("'publicKey' =>", $inhalt);
|
self::assertStringNotContainsString("'publicKey' =>", $inhalt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testManagerSynchronisiertDieTopologieMitSeparatemGeraetezugang(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'https://license.enelix.ch/api/v1/installations/%s/prognosis/topology',
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterAttributeString('PrognoseInstallationsToken', '')",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('JSON_PRESERVE_ZERO_FRACTION', $inhalt);
|
||||||
|
self::assertStringContainsString("'issueDeviceToken' =>", $inhalt);
|
||||||
|
self::assertStringContainsString("unset(\$neueLease['deviceToken'])", $inhalt);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv'))",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"\$this->ReadPropertyString('Lizenzcode'),\n \$geraetezugangFehlt",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('synchronisiereAnlagentopologie()', $inhalt);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry',
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString("'PrognoseSenden',", $inhalt);
|
||||||
|
self::assertStringContainsString("'capturedAt' => gmdate(", $inhalt);
|
||||||
|
foreach (["'PV'", "'Hausverbrauch'", "'Netzleistung'", "'SOC'"] as $messwert) {
|
||||||
|
self::assertStringContainsString($messwert, $inhalt);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString("'Authorization: Bearer ' . \$token", $inhalt);
|
||||||
|
self::assertStringNotContainsString(
|
||||||
|
"protokolliere('Prognosesynchronisation', \$token)",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
|
||||||
|
$leasePruefung = strpos($inhalt, 'Lizenzpruefung::pruefeLease(');
|
||||||
|
$tokenSpeicherung = strpos(
|
||||||
|
$inhalt,
|
||||||
|
"WriteAttributeString('PrognoseInstallationsToken', \$geraeteToken)"
|
||||||
|
);
|
||||||
|
self::assertNotFalse($leasePruefung);
|
||||||
|
self::assertNotFalse($tokenSpeicherung);
|
||||||
|
self::assertTrue(
|
||||||
|
$leasePruefung < $tokenSpeicherung,
|
||||||
|
'Der Gerätezugang darf erst nach validierter Lizenzantwort gespeichert werden.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testManagerUebertraegtStoerungsvollsnapshotsMitRetry(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
self::assertFileExists(__DIR__ . '/../libs/StoerungsSnapshot.php');
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'https://license.enelix.ch/api/v1/installations/%s/faults',
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
foreach ([
|
||||||
|
"RegisterPropertyBoolean('StoerueberwachungAktiv', false)",
|
||||||
|
"RegisterPropertyInteger('StoerungsSendeintervall', 300)",
|
||||||
|
"RegisterAttributeString('StoerungsSnapshot', '{}')",
|
||||||
|
"RegisterAttributeString('StoerungsGesendeterHash', '')",
|
||||||
|
"RegisterAttributeInteger('StoerungsNaechsterVersuch', 0)",
|
||||||
|
"'StoerungenSenden',",
|
||||||
|
'StoerungsSnapshot::erstellen(',
|
||||||
|
'StoerungsSnapshot::zustandHash(',
|
||||||
|
"CURLOPT_CUSTOMREQUEST => 'PUT'",
|
||||||
|
] as $erwartet) {
|
||||||
|
self::assertStringContainsString($erwartet, $inhalt);
|
||||||
|
}
|
||||||
|
self::assertStringNotContainsString(
|
||||||
|
'"name":"StoermeldeAnschluss"',
|
||||||
|
str_replace(' ', '', (string) file_get_contents(__DIR__ . '/../Manager/form.json'))
|
||||||
|
);
|
||||||
|
self::assertGreaterThanOrEqual(
|
||||||
|
3,
|
||||||
|
substr_count(
|
||||||
|
$inhalt,
|
||||||
|
"|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv'))"
|
||||||
|
),
|
||||||
|
'Der Gerätezugang muss beim Anwenden, Erneuern und Anfordern berücksichtigt werden.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testManagerRegeltDieAnlagenweiteEinspeisegrenzeUndDenNetzfahrplan(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
self::assertFileExists(__DIR__ . '/../libs/EinspeiseRegler.php');
|
||||||
|
self::assertStringContainsString('EinspeiseRegler::verteile(', $inhalt);
|
||||||
|
self::assertStringContainsString('EinspeiseRegler::stellwert(', $inhalt);
|
||||||
|
self::assertStringContainsString("RegisterAttributeString('LetztePvGrenzen'", $inhalt);
|
||||||
|
self::assertStringContainsString("RegisterAttributeString('Netzfahrplan'", $inhalt);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'https://license.enelix.ch/api/v1/installations/%s/prognosis/schedule',
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('Lizenzpruefung::NETZFAHRPLAN', $inhalt);
|
||||||
|
self::assertStringContainsString('Lizenzpruefung::PROGNOSE_PV', $inhalt);
|
||||||
|
self::assertStringContainsString('Lizenzpruefung::PROGNOSE_VERBRAUCH', $inhalt);
|
||||||
|
self::assertStringContainsString('prognoseLizenzStatustext(', $inhalt);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"foreach (['PeakShavingTitel', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze']",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('aktuelleNetzfahrplanVorgabe()', $inhalt);
|
||||||
|
self::assertStringContainsString('gibEinspeisebegrenzungFrei()', $inhalt);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyFloat('Einspeisetoleranz'", $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
public function testManagerVerwendetDieNeuenZeitdefaults(): void
|
public function testManagerVerwendetDieNeuenZeitdefaults(): void
|
||||||
{
|
{
|
||||||
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
@@ -193,6 +338,8 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
{
|
{
|
||||||
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
self::assertStringContainsString('implements ManagerSchnittstelle', $inhalt);
|
self::assertStringContainsString('implements ManagerSchnittstelle', $inhalt);
|
||||||
|
self::assertStringContainsString("require_once __DIR__ . '/../libs/Anlagentopologie.php'", $inhalt);
|
||||||
|
self::assertStringContainsString('public function AnlagentopologieExportieren(): string', $inhalt);
|
||||||
self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt);
|
self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt);
|
||||||
self::assertStringContainsString("'ManagerdatenEmpfangen'", $inhalt);
|
self::assertStringContainsString("'ManagerdatenEmpfangen'", $inhalt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,17 @@ final class NachrichtenvertragTest extends TestCase
|
|||||||
self::assertTrue(true);
|
self::assertTrue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLeistungswertWirdInEinzelwertenUndBereichenGefunden(): void
|
||||||
|
{
|
||||||
|
$angebot = [0, ['Von_W' => 1000, 'Bis_W' => 2000], 3000];
|
||||||
|
|
||||||
|
self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 0));
|
||||||
|
self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 1537));
|
||||||
|
self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 2000));
|
||||||
|
self::assertFalse(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 999));
|
||||||
|
self::assertFalse(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 2500));
|
||||||
|
}
|
||||||
|
|
||||||
public function testNegativePrioritaetWirdAbgelehnt(): void
|
public function testNegativePrioritaetWirdAbgelehnt(): void
|
||||||
{
|
{
|
||||||
$daten = $this->verbraucherdaten();
|
$daten = $this->verbraucherdaten();
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\StoerungsSnapshot;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class StoerungsSnapshotTest extends TestCase
|
||||||
|
{
|
||||||
|
private const INSTALLATION_ID = '123e4567-e89b-42d3-a456-426614174000';
|
||||||
|
|
||||||
|
public function testSnapshotNormalisiertDedupliziertUndSortiertMeldungen(): void
|
||||||
|
{
|
||||||
|
$snapshot = StoerungsSnapshot::erstellen(
|
||||||
|
self::INSTALLATION_ID,
|
||||||
|
42,
|
||||||
|
'main',
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'sourceType' => 'consumer',
|
||||||
|
'sourceId' => 'ips:8',
|
||||||
|
'sourceName' => ' Boiler ',
|
||||||
|
'code' => 'Temp.High',
|
||||||
|
'severity' => 'warning',
|
||||||
|
'message' => " Temperatur hoch ",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'sourceType' => 'manager',
|
||||||
|
'sourceId' => 'ips:42',
|
||||||
|
'sourceName' => 'Manager',
|
||||||
|
'code' => 'Grid.Missing',
|
||||||
|
'severity' => 'critical',
|
||||||
|
'message' => 'Netzmessung fehlt',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'sourceType' => 'consumer',
|
||||||
|
'sourceId' => 'ips:8',
|
||||||
|
'sourceName' => 'Boiler',
|
||||||
|
'code' => 'Temp.High',
|
||||||
|
'severity' => 'warning',
|
||||||
|
'message' => 'Temperatur sehr hoch',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
1_700_000_000
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame('1.0', $snapshot['version']);
|
||||||
|
self::assertSame('2023-11-14T22:13:20Z', $snapshot['capturedAt']);
|
||||||
|
self::assertSame(
|
||||||
|
['Grid.Missing', 'Temp.High'],
|
||||||
|
array_column($snapshot['faults'], 'code')
|
||||||
|
);
|
||||||
|
self::assertSame('Temperatur sehr hoch', $snapshot['faults'][1]['message']);
|
||||||
|
self::assertSame(
|
||||||
|
['instanceId' => 42, 'role' => 'main', 'active' => true],
|
||||||
|
$snapshot['manager']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZustandHashIgnoriertDenErfassungszeitpunkt(): void
|
||||||
|
{
|
||||||
|
$a = StoerungsSnapshot::erstellen(
|
||||||
|
self::INSTALLATION_ID,
|
||||||
|
42,
|
||||||
|
'standalone',
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
1_700_000_000
|
||||||
|
);
|
||||||
|
$b = StoerungsSnapshot::erstellen(
|
||||||
|
self::INSTALLATION_ID,
|
||||||
|
42,
|
||||||
|
'standalone',
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
1_700_000_300
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(StoerungsSnapshot::zustandHash($a), StoerungsSnapshot::zustandHash($b));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSnapshotBegrenztSichAufHundertPriorisierteMeldungen(): void
|
||||||
|
{
|
||||||
|
$meldungen = [];
|
||||||
|
for ($index = 1; $index <= 101; ++$index) {
|
||||||
|
$meldungen[] = [
|
||||||
|
'sourceType' => 'consumer',
|
||||||
|
'sourceId' => 'ips:' . $index,
|
||||||
|
'sourceName' => sprintf('Quelle %03d', $index),
|
||||||
|
'code' => 'Fault.' . $index,
|
||||||
|
'severity' => $index === 101 ? 'critical' : 'warning',
|
||||||
|
'message' => 'Testmeldung ' . $index,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = StoerungsSnapshot::erstellen(
|
||||||
|
self::INSTALLATION_ID,
|
||||||
|
42,
|
||||||
|
'standalone',
|
||||||
|
true,
|
||||||
|
$meldungen
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertCount(100, $snapshot['faults']);
|
||||||
|
self::assertSame('Fault.101', $snapshot['faults'][0]['code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUngueltigerSchweregradWirdAbgelehnt(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
StoerungsSnapshot::erstellen(
|
||||||
|
self::INSTALLATION_ID,
|
||||||
|
42,
|
||||||
|
'standalone',
|
||||||
|
true,
|
||||||
|
[[
|
||||||
|
'sourceType' => 'consumer',
|
||||||
|
'sourceId' => 'ips:8',
|
||||||
|
'sourceName' => 'Boiler',
|
||||||
|
'code' => 'Temp.High',
|
||||||
|
'severity' => 'alarm',
|
||||||
|
'message' => 'Temperatur hoch',
|
||||||
|
]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ return [
|
|||||||
'file' => __DIR__ . '/modules/Manager.php',
|
'file' => __DIR__ . '/modules/Manager.php',
|
||||||
'paths' => [
|
'paths' => [
|
||||||
'Manager/*',
|
'Manager/*',
|
||||||
|
'libs/EinspeiseRegler.php',
|
||||||
'libs/Lizenzpruefung.php',
|
'libs/Lizenzpruefung.php',
|
||||||
'libs/ManagerRegler.php',
|
'libs/ManagerRegler.php',
|
||||||
'libs/ManagerSchnittstelle.php',
|
'libs/ManagerSchnittstelle.php',
|
||||||
|
|||||||
@@ -119,10 +119,10 @@ PHP
|
|||||||
): void {
|
): void {
|
||||||
IPS_RequestAction($batterieID, 'Aktiv', true);
|
IPS_RequestAction($batterieID, 'Aktiv', true);
|
||||||
$managerdatenSenden('PV', null);
|
$managerdatenSenden('PV', null);
|
||||||
$managerdatenSenden('PV', 500);
|
$managerdatenSenden('PV', 537);
|
||||||
|
|
||||||
$test->waitUntil(
|
$test->waitUntil(
|
||||||
static fn (): bool => (float) GetValue($leistungID) === 500.0
|
static fn (): bool => (float) GetValue($leistungID) === 537.0
|
||||||
&& (int) GetValue($modusID) === 11
|
&& (int) GetValue($modusID) === 11
|
||||||
&& (int) GetValue($managementID) === 2
|
&& (int) GetValue($managementID) === 2
|
||||||
);
|
);
|
||||||
@@ -136,9 +136,9 @@ PHP
|
|||||||
$modusID,
|
$modusID,
|
||||||
$leistungID
|
$leistungID
|
||||||
): void {
|
): void {
|
||||||
$managerdatenSenden('PV', -500);
|
$managerdatenSenden('PV', -843);
|
||||||
$test->waitUntil(
|
$test->waitUntil(
|
||||||
static fn (): bool => (float) GetValue($leistungID) === 500.0
|
static fn (): bool => (float) GetValue($leistungID) === 843.0
|
||||||
&& (int) GetValue($modusID) === 12
|
&& (int) GetValue($modusID) === 12
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ declare(strict_types=1);
|
|||||||
use Belevo\EnelixEMS\SymconTest\TestContext;
|
use Belevo\EnelixEMS\SymconTest\TestContext;
|
||||||
|
|
||||||
return static function (TestContext $test): void {
|
return static function (TestContext $test): void {
|
||||||
$netzleistungId = $test->createVariable(2, 'Netzleistung', 0.0);
|
$netzleistungId = $test->createVariable(2, 'Netzleistung', -800.0);
|
||||||
|
$pvLeistungId = $test->createVariable(2, 'PV-Leistung', 4200.0);
|
||||||
|
$batterieLeistungId = $test->createVariable(2, 'Batterieleistung', 700.0);
|
||||||
|
$batterieSocId = $test->createVariable(2, 'Batterie-Ladezustand', 62.0);
|
||||||
$einKontaktId = $test->createActionVariable('Kontakt Verbraucher 1-Stufig');
|
$einKontaktId = $test->createActionVariable('Kontakt Verbraucher 1-Stufig');
|
||||||
$einStufigId = $test->createInstance(
|
$einStufigId = $test->createInstance(
|
||||||
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||||
@@ -72,12 +75,48 @@ return static function (TestContext $test): void {
|
|||||||
['InstanzID' => $warmwasserId, 'Aktiv' => true],
|
['InstanzID' => $warmwasserId, 'Aktiv' => true],
|
||||||
['InstanzID' => $pufferspeicherId, 'Aktiv' => true],
|
['InstanzID' => $pufferspeicherId, 'Aktiv' => true],
|
||||||
];
|
];
|
||||||
|
$wechselrichterKonfiguration = [[
|
||||||
|
'ID' => 'hybrid-1',
|
||||||
|
'Name' => 'Hybridwechselrichter',
|
||||||
|
'Typ' => 'hybrid',
|
||||||
|
'ACNennleistung_kW' => 10.0,
|
||||||
|
'IstleistungVariableID' => $pvLeistungId,
|
||||||
|
'Istleistungsfaktor' => 1.0,
|
||||||
|
]];
|
||||||
|
$pvKonfiguration = [[
|
||||||
|
'ID' => 'pv-1',
|
||||||
|
'Name' => 'Suedflaeche',
|
||||||
|
'DCLeistung_kWp' => 12.0,
|
||||||
|
'Neigung_Grad' => 30.0,
|
||||||
|
'Azimut_Grad' => 0.0,
|
||||||
|
'WechselrichterID' => 'hybrid-1',
|
||||||
|
'MPPT' => 'A',
|
||||||
|
'Modulanzahl' => 30,
|
||||||
|
'Modulleistung_Wp' => 400.0,
|
||||||
|
]];
|
||||||
|
$batterieKonfiguration = [[
|
||||||
|
'ID' => 'bat-1',
|
||||||
|
'Name' => 'Hausspeicher',
|
||||||
|
'Nennkapazitaet_kWh' => 13.5,
|
||||||
|
'Nutzkapazitaet_kWh' => 12.8,
|
||||||
|
'MaxLadeleistung_kW' => 5.0,
|
||||||
|
'MaxEntladeleistung_kW' => 5.0,
|
||||||
|
'WechselrichterID' => 'hybrid-1',
|
||||||
|
'Kopplung' => 'hybrid',
|
||||||
|
'LeistungVariableID' => $batterieLeistungId,
|
||||||
|
'Leistungsfaktor' => 1.0,
|
||||||
|
'SOCVariableID' => $batterieSocId,
|
||||||
|
]];
|
||||||
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
|
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
|
||||||
$managerId = $test->createInstance(
|
$managerId = $test->createInstance(
|
||||||
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||||
'Manager',
|
'Manager',
|
||||||
[
|
[
|
||||||
'NetzleistungVariableID' => $netzleistungId,
|
'NetzleistungVariableID' => $netzleistungId,
|
||||||
|
'EnergyPieAnzeigen' => true,
|
||||||
|
'EnergiediagrammeAnzeigen' => true,
|
||||||
|
'FunFactsAnzeigen' => true,
|
||||||
|
'EnergieflussAnzeigen' => true,
|
||||||
'Lizenzcode' => $lizenzcode,
|
'Lizenzcode' => $lizenzcode,
|
||||||
'AutomatischeSuche' => false,
|
'AutomatischeSuche' => false,
|
||||||
'VerbraucherZuordnung' => json_encode($zuordnung, JSON_THROW_ON_ERROR),
|
'VerbraucherZuordnung' => json_encode($zuordnung, JSON_THROW_ON_ERROR),
|
||||||
@@ -87,6 +126,12 @@ return static function (TestContext $test): void {
|
|||||||
'Lastspitzengrenze' => 0.0,
|
'Lastspitzengrenze' => 0.0,
|
||||||
'SollwertSolarladen' => 0.0,
|
'SollwertSolarladen' => 0.0,
|
||||||
'Umschaltdifferenz' => 0.0,
|
'Umschaltdifferenz' => 0.0,
|
||||||
|
'AnlagenWechselrichter' => json_encode(
|
||||||
|
$wechselrichterKonfiguration,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
),
|
||||||
|
'AnlagenPVFlaechen' => json_encode($pvKonfiguration, JSON_THROW_ON_ERROR),
|
||||||
|
'AnlagenBatterien' => json_encode($batterieKonfiguration, JSON_THROW_ON_ERROR),
|
||||||
'DiagnosevariablenAnzeigen' => true,
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -163,7 +208,96 @@ return static function (TestContext $test): void {
|
|||||||
$vorher = $lizenzInstallationID();
|
$vorher = $lizenzInstallationID();
|
||||||
IPS_ApplyChanges($managerId);
|
IPS_ApplyChanges($managerId);
|
||||||
$test->assertSame($vorher, $lizenzInstallationID());
|
$test->assertSame($vorher, $lizenzInstallationID());
|
||||||
|
|
||||||
|
$topologie = json_decode(
|
||||||
|
ENELIX_AnlagentopologieExportieren($managerId),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$test->assertSame($vorher, $topologie['installationId'] ?? null);
|
||||||
|
$test->assertSame('1.0', $topologie['version'] ?? null);
|
||||||
|
$test->assertSame(12.0, $topologie['summary']['pvDcKwp'] ?? null);
|
||||||
|
$test->assertSame(10.0, $topologie['summary']['pvAcKw'] ?? null);
|
||||||
|
$test->assertSame(12.8, $topologie['summary']['usableBatteryKwh'] ?? null);
|
||||||
|
$test->assertSame('hybrid-1', $topologie['sharedAcLimits'][0]['inverterId'] ?? null);
|
||||||
|
$test->assertSame(['pv-1'], $topologie['sharedAcLimits'][0]['pvArrayIds'] ?? null);
|
||||||
|
$test->assertSame(['bat-1'], $topologie['sharedAcLimits'][0]['batteryIds'] ?? null);
|
||||||
});
|
});
|
||||||
|
$test->runCase('Energiewerte, Archivregeln und Unterobjekte werden erzeugt', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($managerId): void {
|
||||||
|
IPS_RequestAction($managerId, 'MesswerteAufzeichnen', true);
|
||||||
|
$test->assertSame(
|
||||||
|
4200.0,
|
||||||
|
GetValue($test->objectByIdent('PVLeistungArchiv', $managerId))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
2700.0,
|
||||||
|
GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
-800.0,
|
||||||
|
GetValue($test->objectByIdent('NetzleistungArchiv', $managerId))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
700.0,
|
||||||
|
GetValue($test->objectByIdent('BatterieleistungArchiv', $managerId))
|
||||||
|
);
|
||||||
|
|
||||||
|
$archive = IPS_GetInstanceListByModuleID(
|
||||||
|
'{43192F0B-135B-4CE7-A0A7-1475603F3060}'
|
||||||
|
);
|
||||||
|
$test->assertTrue($archive !== []);
|
||||||
|
$archivID = (int) $archive[0];
|
||||||
|
$leistungID = $test->objectByIdent('PVLeistungArchiv', $managerId);
|
||||||
|
$energieID = $test->objectByIdent('PVEnergie', $managerId);
|
||||||
|
$test->assertSame(true, AC_GetLoggingStatus($archivID, $leistungID));
|
||||||
|
$test->assertSame(0, AC_GetAggregationType($archivID, $leistungID));
|
||||||
|
$test->assertSame(1, AC_GetAggregationType($archivID, $energieID));
|
||||||
|
|
||||||
|
foreach (['Leistungsdiagramm', 'Energiediagramm'] as $ident) {
|
||||||
|
$medienID = $test->objectByIdent($ident, $managerId);
|
||||||
|
$test->assertSame(4, (int) IPS_GetMedia($medienID)['MediaType']);
|
||||||
|
}
|
||||||
|
$html = (string) GetValue($test->objectByIdent('EnergieFunFacts', $managerId));
|
||||||
|
$test->assertTrue(str_contains($html, 'Solar'));
|
||||||
|
$test->assertTrue(str_contains($html, 'Netz & Haus'));
|
||||||
|
$energieflussID = $test->objectByIdent('Energiefluss', $managerId);
|
||||||
|
$test->assertSame(
|
||||||
|
'{BA90FDB3-C203-8F26-B4D6-F3DB99905724}',
|
||||||
|
IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID']
|
||||||
|
);
|
||||||
|
|
||||||
|
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false);
|
||||||
|
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
||||||
|
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', true);
|
||||||
|
$test->assertSame(false, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'EnergyPieAnzeigen',
|
||||||
|
'EnergiediagrammeAnzeigen',
|
||||||
|
'FunFactsAnzeigen',
|
||||||
|
'EnergieflussAnzeigen',
|
||||||
|
] as $property) {
|
||||||
|
IPS_SetProperty($managerId, $property, false);
|
||||||
|
}
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
foreach ([
|
||||||
|
'EnergyPie',
|
||||||
|
'Leistungsdiagramm',
|
||||||
|
'Energiediagramm',
|
||||||
|
'EnergieFunFacts',
|
||||||
|
] as $ident) {
|
||||||
|
$test->assertSame(
|
||||||
|
false,
|
||||||
|
@IPS_GetObjectIDByIdent($ident, $managerId),
|
||||||
|
$ident . ' wurde beim Ausschalten nicht entfernt.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
||||||
|
});
|
||||||
|
|
||||||
$zustandswert = static function (array $daten, int $instanzId, string $kennung) {
|
$zustandswert = static function (array $daten, int $instanzId, string $kennung) {
|
||||||
foreach ($daten['Verbraucher'] ?? [] as $verbraucher) {
|
foreach ($daten['Verbraucher'] ?? [] as $verbraucher) {
|
||||||
if (($verbraucher['InstanzID'] ?? 0) !== $instanzId) {
|
if (($verbraucher['InstanzID'] ?? 0) !== $instanzId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user