5 Commits
Author SHA1 Message Date
dh 5437f3f058 fix(verbraucher): clear discarded power setpoints
Tests / test (push) Failing after 51s
2026-10-06 13:45:54 +00:00
dh ebec355c96 feat(manager): account for SDL power and SOC separately
Tests / test (push) Failing after 1m12s
Add opt-in SDL measurement sources and independent visualization toggles. Exclude balancing power from household telemetry and new household energy. Preserve custom views and cover measurement, migration and Symcon 8.0 behavior.
2026-10-06 13:35:17 +00:00
dh 58f91e4319 feat(manager): distribute equal-priority power stepwise
Tests / test (push) Successful in 44s
2026-10-06 11:55:26 +00:00
dh ec7182b833 fix(charger): stabilize Go-E setpoint control
Tests / test (push) Successful in 47s
2026-10-06 11:33:59 +00:00
dh 37addf4242 feat(manager): Energievisualisierung und Synchronisierung erweitern
Tests / test (push) Successful in 50s
2026-10-06 08:00:30 +00:00
35 changed files with 2818 additions and 204 deletions
+7 -6
View File
@@ -153,6 +153,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->registriereMesswertmeldungen(); $this->registriereMesswertmeldungen();
$this->WriteAttributeBoolean('RegisterInitialisiert', false); $this->WriteAttributeBoolean('RegisterInitialisiert', false);
@@ -219,7 +220,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
$this->aktualisiere(true); $this->aktualisiere(true);
@@ -235,7 +236,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
case 'VorgabeTimeout': case 'VorgabeTimeout':
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -249,7 +250,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
return; return;
case 'SichererZustand': case 'SichererZustand':
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->schreibeRegister(0, true); $this->schreibeRegister(0, true);
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -313,7 +314,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
(int) $this->leseZustand('Sollleistung') (int) $this->leseZustand('Sollleistung')
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(false); $this->aktualisiere(false);
} }
$this->planeMeldung(); $this->planeMeldung();
@@ -419,7 +420,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
(int) $this->leseZustand('Sollleistung') (int) $this->leseZustand('Sollleistung')
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->pruefeVorgabeTimeout($jetzt); $this->pruefeVorgabeTimeout($jetzt);
@@ -596,7 +597,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
} }
+22
View File
@@ -2,6 +2,28 @@
Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert. Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
## Unreleased
- Verbraucher: Beim Verwerfen einer Manager-Vorgabe wird auch die gespeicherte
und sichtbare Sollleistung auf 0 W gesetzt. Gilt fuer Batterie, beide
Ladestationen, Pufferspeicher, Verbraucher 1-Stufig, Waermepumpe und
Warmwassererwaermer. Bereits ungueltige Altwerte werden beim Anwenden der
Instanzkonfiguration bereinigt. Gueltige Vorgaben, lokale Schutzprogramme,
Mindestlaufzeiten und der Nachrichtenvertrag bleiben erhalten.
- Manager: SDL-/Regelenergie als separate Istleistungsquelle mit optionalem SOC,
Leistungsfaktor und getrennten Anzeigeoptionen fuer Energiefluss und Diagramme.
SDL wird unabhaengig von der Anzeige aus Hauslast und neuer Hausenergie
herausgerechnet; die Prognosetelemetrie verwendet die korrigierte Hauslast.
Bestehende Visualisierungen werden ausschliesslich um eigene SDL-Eintraege
ergaenzt. Standardmaessig deaktiviert, keine automatische Quellenmigration,
keine rueckwirkende Datenkorrektur und keine Aenderung externer SDL-Steuerungen.
- Manager: Positives Budget wird bei gleicher Prioritaet nach der kleinsten
naechsten erreichbaren Sollleistung verteilt. Nur bei gleicher Leistungsstufe
entscheiden exakter Energiebezug und anschliessend die Instanz-ID. Einzelstufen
und ganzzahlige Bereiche teilen sich das Budget; reservierte Lasten und die
negative Defizitregelung bleiben unveraendert. Keine Konfigurationsmigration.
## 0.1 Build 1 - Erste Beta - 2026-09-29 ## 0.1 Build 1 - Erste Beta - 2026-09-29
### Enthalten ### Enthalten
+6 -5
View File
@@ -144,6 +144,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->initialisiereLokaleEinstellungen(); $this->initialisiereLokaleEinstellungen();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->WriteAttributeBoolean('GatewayVerbunden', false); $this->WriteAttributeBoolean('GatewayVerbunden', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1); $this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false); $this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
@@ -179,7 +180,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->starteRegeluebergang(); $this->starteRegeluebergang();
} else { } else {
$this->beendeRegeluebergang(); $this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -192,7 +193,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->starteRegeluebergang(); $this->starteRegeluebergang();
} else { } else {
$this->beendeRegeluebergang(); $this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -215,7 +216,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
case 'VorgabeTimeout': case 'VorgabeTimeout':
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -322,7 +323,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
true true
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(false); $this->aktualisiere(false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -559,7 +560,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if (in_array($sollleistung, $angebot, true)) { if (in_array($sollleistung, $angebot, true)) {
return $sollleistung; return $sollleistung;
} }
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
if ( if (
+17 -6
View File
@@ -100,6 +100,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterAttributeInteger('PhasenerkennungSeit', 0); $this->RegisterAttributeInteger('PhasenerkennungSeit', 0);
$this->RegisterAttributeInteger('PhasenerkennungVersion', 0); $this->RegisterAttributeInteger('PhasenerkennungVersion', 0);
$this->RegisterAttributeBoolean('LadeleistungSeitFreigabeErkannt', false); $this->RegisterAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->RegisterAttributeInteger('GeraeteMaximalstrom', 32);
$this->RegisterAttributeInteger('ErkannterMaximalstrom', 32); $this->RegisterAttributeInteger('ErkannterMaximalstrom', 32);
$this->RegisterAttributeInteger('FahrzeugstromKandidat', 0); $this->RegisterAttributeInteger('FahrzeugstromKandidat', 0);
$this->RegisterAttributeInteger('FahrzeugstromKandidatSeit', 0); $this->RegisterAttributeInteger('FahrzeugstromKandidatSeit', 0);
@@ -138,6 +139,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->initialisiereLokaleEinstellungen(); $this->initialisiereLokaleEinstellungen();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
// Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen. // Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen.
$this->setzeZustand('FahrzeugGeladen', false); $this->setzeZustand('FahrzeugGeladen', false);
@@ -189,7 +191,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->starteRegeluebergang(); $this->starteRegeluebergang();
} else { } else {
$this->beendeRegeluebergang(); $this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -202,7 +204,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->starteRegeluebergang(); $this->starteRegeluebergang();
} else { } else {
$this->beendeRegeluebergang(); $this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -223,7 +225,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
case 'VorgabeTimeout': case 'VorgabeTimeout':
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -236,7 +238,6 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if ((bool) $wert) { if ((bool) $wert) {
$this->SetTimerInterval('RueckmeldungVerzoegert', 0); $this->SetTimerInterval('RueckmeldungVerzoegert', 0);
} }
$this->aktualisiere(false);
$this->sendeVerbraucherdaten(); $this->sendeVerbraucherdaten();
return; return;
@@ -275,7 +276,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true) && !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(false, false); $this->aktualisiere(false, false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -315,6 +316,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl'); $phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
$maximalstrom = min( $maximalstrom = min(
$this->ReadPropertyInteger('MaximalerLadestrom'), $this->ReadPropertyInteger('MaximalerLadestrom'),
max(6, $this->ReadAttributeInteger('GeraeteMaximalstrom')),
max(6, $this->ReadAttributeInteger('ErkannterMaximalstrom')) max(6, $this->ReadAttributeInteger('ErkannterMaximalstrom'))
); );
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $maximalstrom); $this->setzeSichtbareVariable('FahrzeugMaximalstrom', $maximalstrom);
@@ -423,6 +425,15 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->ReadPropertyInteger('Geraetetyp'), $this->ReadPropertyInteger('Geraetetyp'),
$antwort $antwort
); );
if (isset($rohstatus['Sollstrom_A'])) {
$geraeteSollstrom = (int) $rohstatus['Sollstrom_A'];
if ($geraeteSollstrom === 0 || ($geraeteSollstrom >= 6 && $geraeteSollstrom <= 32)) {
$this->WriteAttributeInteger('LetzterGesetzterStrom', $geraeteSollstrom);
}
}
if (isset($rohstatus['Maximalstrom_A'])) {
$this->WriteAttributeInteger('GeraeteMaximalstrom', (int) $rohstatus['Maximalstrom_A']);
}
$warVerbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden'); $warVerbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
$verbunden = $rohstatus['Rohstatus'] !== 1; $verbunden = $rohstatus['Rohstatus'] !== 1;
@@ -550,7 +561,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if (in_array($sollleistung, $angebot, true)) { if (in_array($sollleistung, $angebot, true)) {
return $sollleistung; return $sollleistung;
} }
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$ersatz = 0; $ersatz = 0;
foreach ($angebot as $leistungswert) { foreach ($angebot as $leistungswert) {
if ($leistungswert > $sollleistung) { if ($leistungswert > $sollleistung) {
+96 -9
View File
@@ -260,10 +260,14 @@
"type": "Label", "type": "Label",
"caption": "Energiezaehler werden in kWh erwartet; ueber den Faktor koennen beispielsweise Wh (0,001) umgerechnet werden." "caption": "Energiezaehler werden in kWh erwartet; ueber den Faktor koennen beispielsweise Wh (0,001) umgerechnet werden."
}, },
{
"type": "Label",
"caption": "Die Wechselrichter-Tabelle beschreibt die AC-Wandler der Anlage. PV-Flaechen und Batteriespeicher verweisen darauf, damit gemeinsame Leistungsgrenzen und die elektrische Kopplung eindeutig sind. Bei einem Hybridgeraet verwenden PV und Batterie dieselbe Wechselrichter-ID; bei einem AC-gekoppelten Speicher wird dessen separater Batteriewechselrichter erfasst."
},
{ {
"type": "List", "type": "List",
"name": "AnlagenWechselrichter", "name": "AnlagenWechselrichter",
"caption": "Wechselrichter", "caption": "Wechselrichter / AC-Wandler",
"rowCount": 4, "rowCount": 4,
"add": true, "add": true,
"delete": true, "delete": true,
@@ -459,6 +463,10 @@
} }
] ]
}, },
{
"type": "Label",
"caption": "Der Batterieeintrag beschreibt den Speicher selbst: Kapazitaet, Leistung, Ladezustand und Energiezaehler. Die Wechselrichter-ID ist nur die Verknuepfung zum zugehoerigen AC-Wandler und keine doppelte Batteriekonfiguration."
},
{ {
"type": "List", "type": "List",
"name": "AnlagenBatterien", "name": "AnlagenBatterien",
@@ -722,26 +730,61 @@
{ {
"type": "CheckBox", "type": "CheckBox",
"name": "EnergyPieAnzeigen", "name": "EnergyPieAnzeigen",
"caption": "Energy Pie unter dem Manager anzeigen", "caption": "Energy Pie unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormEnergyPieAnzeigen', $EnergyPieAnzeigen);"
}, },
{ {
"type": "CheckBox", "type": "CheckBox",
"name": "EnergiediagrammeAnzeigen", "name": "EnergiediagrammeAnzeigen",
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen", "caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormEnergiediagrammeAnzeigen', $EnergiediagrammeAnzeigen);" },
{
"type": "RowLayout",
"items": [
{
"type": "Select",
"name": "DiagrammPVModus",
"caption": "PV in Diagrammen",
"options": [
{"caption": "Summe", "value": 0},
{"caption": "Einzelne Anlagen", "value": 1},
{"caption": "Summe und einzelne Anlagen", "value": 2}
]
},
{
"type": "Select",
"name": "DiagrammBatterieModus",
"caption": "Batterien in Diagrammen",
"options": [
{"caption": "Summe", "value": 0},
{"caption": "Einzelne Speicher", "value": 1},
{"caption": "Summe und einzelne Speicher", "value": 2}
]
}
]
},
{
"type": "CheckBox",
"name": "LadestationenSeparatAnzeigen",
"caption": "Ladestationen separat in Energiefluss und Diagrammen anzeigen"
},
{
"type": "CheckBox",
"name": "VerbraucherSeparatAnzeigen",
"caption": "Uebrige Verbraucher separat in Energiefluss und Diagrammen anzeigen"
},
{
"type": "Label",
"caption": "Bestehende Energy Pies, Diagramme und Energiefluesse bleiben bei Apply unveraendert. Fuer eine Neuerzeugung den jeweiligen Schalter ausschalten, Apply ausfuehren, wieder einschalten und erneut Apply ausfuehren."
}, },
{ {
"type": "CheckBox", "type": "CheckBox",
"name": "FunFactsAnzeigen", "name": "FunFactsAnzeigen",
"caption": "Energy Facts unter dem Manager anzeigen", "caption": "Energy Facts unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormFunFactsAnzeigen', $FunFactsAnzeigen);"
}, },
{ {
"type": "CheckBox", "type": "CheckBox",
"name": "EnergieflussAnzeigen", "name": "EnergieflussAnzeigen",
"caption": "Energiefluss unter dem Manager anzeigen", "caption": "Energiefluss unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);"
}, },
{ {
"type": "Select", "type": "Select",
@@ -792,6 +835,50 @@
"type": "ExpansionPanel", "type": "ExpansionPanel",
"caption": "SDL / VGT", "caption": "SDL / VGT",
"items": [ "items": [
{
"type": "CheckBox",
"name": "SDLAktiv",
"caption": "SDL aktiv",
"onChange": "IPS_RequestAction($id, 'FormSDLAktiv', $SDLAktiv);"
},
{
"type": "ExpansionPanel",
"name": "SDLKonfiguration",
"caption": "SDL / Regelenergie Messung",
"visible": false,
"items": [
{
"type": "SelectVariable",
"name": "SDLLeistungVariableID",
"caption": "SDL Istleistung"
},
{
"type": "NumberSpinner",
"name": "SDLLeistungsfaktor",
"caption": "SDL Leistungsfaktor nach Watt",
"digits": 3
},
{
"type": "SelectVariable",
"name": "SDLSOCVariableID",
"caption": "SDL SOC in Prozent (optional)"
},
{
"type": "CheckBox",
"name": "SDLEnergieflussAnzeigen",
"caption": "SDL mit SOC im Energiefluss anzeigen"
},
{
"type": "CheckBox",
"name": "SDLDiagrammeAnzeigen",
"caption": "SDL mit SOC in den Diagrammen anzeigen"
},
{
"type": "Label",
"caption": "Nach Skalierung: positiv = Laden / Bezug, negativ = Entladen / Abgabe. SDL muss separat gemessen sein und darf nicht bereits in der normalen Batterieleistung enthalten sein. Die Hauslast fuer Prognosen wird unabhaengig von den Anzeigeoptionen korrigiert."
}
]
},
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"name": "SDLAnschluss", "name": "SDLAnschluss",
+158 -25
View File
@@ -11,6 +11,7 @@ require_once __DIR__ . '/../libs/Anlagentopologie.php';
require_once __DIR__ . '/../libs/StoerungsSnapshot.php'; require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
require_once __DIR__ . '/../libs/EnergieMessung.php'; require_once __DIR__ . '/../libs/EnergieMessung.php';
require_once __DIR__ . '/../libs/ManagerEnergieTrait.php'; require_once __DIR__ . '/../libs/ManagerEnergieTrait.php';
require_once __DIR__ . '/../libs/ManagerSDLTrait.php';
use Belevo\EnelixEMS\Anlagentopologie; use Belevo\EnelixEMS\Anlagentopologie;
use Belevo\EnelixEMS\EinspeiseRegler; use Belevo\EnelixEMS\EinspeiseRegler;
@@ -25,6 +26,7 @@ use Belevo\EnelixEMS\StoerungsSnapshot;
class Manager extends IPSModule implements ManagerSchnittstelle class Manager extends IPSModule implements ManagerSchnittstelle
{ {
use ManagerEnergieTrait; use ManagerEnergieTrait;
use \Belevo\EnelixEMS\ManagerSDLTrait;
private const STATUS_AKTIV = 102; private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201; private const STATUS_MESSWERT_UNGUELTIG = 201;
@@ -46,6 +48,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv', 'HausverbrauchLeistungArchiv',
'NetzleistungArchiv', 'NetzleistungArchiv',
'BatterieleistungArchiv', 'BatterieleistungArchiv',
'SDLLeistungArchiv',
'SDLSOCArchiv',
]; ];
private const ENERGIE_VARIABLEN = [ private const ENERGIE_VARIABLEN = [
'PVEnergie', 'PVEnergie',
@@ -54,6 +58,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'EinspeisungEnergie', 'EinspeisungEnergie',
'BatterieLadenEnergie', 'BatterieLadenEnergie',
'BatterieEntladenEnergie', 'BatterieEntladenEnergie',
'SDLLadenEnergie',
'SDLEntladenEnergie',
]; ];
private const VERBRAUCHER_LIZENZEN = [ private const VERBRAUCHER_LIZENZEN = [
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [ '{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
@@ -146,6 +152,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('PrognoseSendeintervall', 60); $this->RegisterPropertyInteger('PrognoseSendeintervall', 60);
$this->RegisterPropertyString('PrognoseAnschluss', '{}'); $this->RegisterPropertyString('PrognoseAnschluss', '{}');
$this->RegisterPropertyString('SDLAnschluss', '{}'); $this->RegisterPropertyString('SDLAnschluss', '{}');
$this->RegisterPropertyBoolean('SDLAktiv', false);
$this->RegisterPropertyInteger('SDLLeistungVariableID', 0);
$this->RegisterPropertyFloat('SDLLeistungsfaktor', 1.0);
$this->RegisterPropertyInteger('SDLSOCVariableID', 0);
$this->RegisterPropertyBoolean('SDLEnergieflussAnzeigen', false);
$this->RegisterPropertyBoolean('SDLDiagrammeAnzeigen', false);
$this->RegisterPropertyString('Lizenzcode', ''); $this->RegisterPropertyString('Lizenzcode', '');
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen. // Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
$this->RegisterPropertyString('LizenzAnschluss', '{}'); $this->RegisterPropertyString('LizenzAnschluss', '{}');
@@ -171,6 +183,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('EnergieLoeschenMonate', 0); $this->RegisterPropertyInteger('EnergieLoeschenMonate', 0);
$this->RegisterPropertyBoolean('EnergyPieAnzeigen', false); $this->RegisterPropertyBoolean('EnergyPieAnzeigen', false);
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false); $this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false);
$this->RegisterPropertyInteger('DiagrammPVModus', 0);
$this->RegisterPropertyInteger('DiagrammBatterieModus', 0);
$this->RegisterPropertyBoolean('LadestationenSeparatAnzeigen', true);
$this->RegisterPropertyBoolean('VerbraucherSeparatAnzeigen', true);
$this->RegisterPropertyBoolean('FunFactsAnzeigen', false); $this->RegisterPropertyBoolean('FunFactsAnzeigen', false);
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false); $this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
$this->RegisterPropertyInteger('EnergieflussLeistungseinheit', 0); $this->RegisterPropertyInteger('EnergieflussLeistungseinheit', 0);
@@ -203,6 +219,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0); $this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0);
$this->RegisterAttributeString('MessungEnergiePuffer', '{}'); $this->RegisterAttributeString('MessungEnergiePuffer', '{}');
$this->RegisterAttributeString('MessungZaehlerstaende', '{}'); $this->RegisterAttributeString('MessungZaehlerstaende', '{}');
$this->RegisterAttributeString('EnergiediagrammKonfiguration', '{}');
$this->RegisterAttributeString('EnergieflussKonfiguration', '{}');
$this->RegisterAttributeBoolean('EnergieflussNeuErzeugen', false);
$this->RegisterAttributeString('SDLMesskonfiguration', '');
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren. // Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
$this->RegisterTimer('Regelzyklus', 0, ''); $this->RegisterTimer('Regelzyklus', 0, '');
@@ -212,6 +232,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);" "IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
); );
$this->RegisterTimer(
'RegelungVerzoegert',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'RegelnVerzoegert', false);"
);
$this->RegisterTimer( $this->RegisterTimer(
'PrognoseSenden', 'PrognoseSenden',
0, 0,
@@ -238,13 +263,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
$this->konfiguriereEnergiearchiv(); $this->synchronisiereSDLMesskonfiguration();
$this->synchronisiereEnergievisualisierungen(); $this->synchronisiereEnergievisualisierungen();
$this->konfiguriereEnergiearchiv();
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0); $this->SetTimerInterval('VorgabenErneuern', 0);
$this->SetTimerInterval('KeepAlive', 0); $this->SetTimerInterval('KeepAlive', 0);
$this->SetTimerInterval('RegelungVerzoegert', 0);
$this->SetTimerInterval('PrognoseSenden', 0); $this->SetTimerInterval('PrognoseSenden', 0);
$this->SetTimerInterval('StoerungenSenden', 0); $this->SetTimerInterval('StoerungenSenden', 0);
$this->SetTimerInterval('MesswerteAufzeichnen', 0); $this->SetTimerInterval('MesswerteAufzeichnen', 0);
@@ -256,6 +283,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0); $this->SetTimerInterval('VorgabenErneuern', 0);
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000); $this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
$this->SetTimerInterval('RegelungVerzoegert', 0);
$this->SetTimerInterval('PrognoseSenden', 0); $this->SetTimerInterval('PrognoseSenden', 0);
$this->SetTimerInterval( $this->SetTimerInterval(
'StoerungenSenden', 'StoerungenSenden',
@@ -416,6 +444,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->monatsgrenzenFuerFormular() $this->monatsgrenzenFuerFormular()
); );
$this->setzeFormularParameter(
$formular['elements'],
'SDLKonfiguration',
'visible',
$this->ReadPropertyBoolean('SDLAktiv')
);
return json_encode($formular, JSON_THROW_ON_ERROR); return json_encode($formular, JSON_THROW_ON_ERROR);
} }
@@ -449,6 +484,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->regeln((bool) $wert); $this->regeln((bool) $wert);
return; return;
case 'RegelnVerzoegert':
$this->SetTimerInterval('RegelungVerzoegert', 0);
$this->regeln(false);
return;
case 'PrognoseSenden': case 'PrognoseSenden':
$this->sendePrognoseTelemetrie(); $this->sendePrognoseTelemetrie();
return; return;
@@ -482,6 +522,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
return; return;
case 'FormSDLAktiv':
$this->UpdateFormField('SDLKonfiguration', 'visible', (bool) $wert);
return;
case 'FormEnergyPieAnzeigen': case 'FormEnergyPieAnzeigen':
$this->synchronisiereEnergyPie((bool) $wert); $this->synchronisiereEnergyPie((bool) $wert);
return; return;
@@ -616,7 +660,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
]; ];
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR)); $this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
$this->aktualisiereEnergieflusswerte(); $this->aktualisiereEnergieflusswerte();
$this->regeln(false); $this->aktualisiereDiagrammMesswerte();
// Den synchronen Rueckruf in die noch meldende Verbraucherinstanz vermeiden.
$this->SetTimerInterval('RegelungVerzoegert', 100);
} }
/** @param array<string, mixed> $daten */ /** @param array<string, mixed> $daten */
@@ -1008,13 +1055,20 @@ class Manager extends IPSModule implements ManagerSchnittstelle
foreach ($ids as $instanzID) { foreach ($ids as $instanzID) {
$eintrag = $cache[(string) $instanzID] ?? null; $eintrag = $cache[(string) $instanzID] ?? null;
$grund = ''; $grund = $this->verbraucherMeldegrund($eintrag, $betriebsart, $timeout);
if (!is_array($eintrag) || !isset($eintrag['EmpfangenAm'], $eintrag['Daten'])) { $synchronisierungsfehler = '';
$grund = 'Noch keine Meldung'; if (in_array($grund, ['Noch keine Meldung', 'Meldung ist veraltet'], true)) {
} elseif (time() - (int) $eintrag['EmpfangenAm'] > $timeout) { try {
$grund = 'Meldung ist veraltet'; $this->SetBuffer('VerbraucherSynchronisierungAktiv', '1');
} elseif (($eintrag['Daten']['Betriebsart'] ?? null) !== $betriebsart) { IPS_RequestAction($instanzID, 'Melden', true);
$grund = 'Leistungsangebot gehoert nicht zur Betriebsart ' . $betriebsart; } catch (Throwable $fehler) {
$synchronisierungsfehler = $fehler->getMessage();
} finally {
$this->SetBuffer('VerbraucherSynchronisierungAktiv', '0');
}
$cache = $this->leseJsonAttribut('VerbraucherCache');
$eintrag = $cache[(string) $instanzID] ?? null;
$grund = $this->verbraucherMeldegrund($eintrag, $betriebsart, $timeout);
} }
if ($grund !== '') { if ($grund !== '') {
@@ -1025,8 +1079,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
try { try {
$this->sendeManagerdaten($instanzID, $betriebsart, null); $this->sendeManagerdaten($instanzID, $betriebsart, null);
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$stoerungen[] = $detail['Name'] . ': Betriebsart konnte nicht uebermittelt werden: ' $synchronisierungsfehler = $fehler->getMessage();
. $fehler->getMessage(); }
if ($synchronisierungsfehler !== '') {
$stoerungen[] = $detail['Name'] . ': Vollmeldung konnte nicht angefordert werden: '
. $synchronisierungsfehler;
} }
continue; continue;
} }
@@ -1039,6 +1096,27 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return $aktuell; return $aktuell;
} }
/** @param array<string, mixed>|null $eintrag */
private function verbraucherMeldegrund(
?array $eintrag,
string $betriebsart,
int $timeout
): string {
if (!isset($eintrag['EmpfangenAm'], $eintrag['Daten'])
|| !is_array($eintrag['Daten'])
) {
return 'Noch keine Meldung';
}
if (time() - (int) $eintrag['EmpfangenAm'] > $timeout) {
return 'Meldung ist veraltet';
}
if (($eintrag['Daten']['Betriebsart'] ?? null) !== $betriebsart) {
return 'Leistungsangebot gehoert nicht zur Betriebsart ' . $betriebsart;
}
return '';
}
private function setzeNichtGeregelteVerbraucher(string $grund): void private function setzeNichtGeregelteVerbraucher(string $grund): void
{ {
try { try {
@@ -1623,6 +1701,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv' => 'Hausverbrauch', 'HausverbrauchLeistungArchiv' => 'Hausverbrauch',
'NetzleistungArchiv' => 'Netzleistung Aufzeichnung', 'NetzleistungArchiv' => 'Netzleistung Aufzeichnung',
'BatterieleistungArchiv' => 'Batterieleistung', 'BatterieleistungArchiv' => 'Batterieleistung',
'SDLLeistungArchiv' => 'SDL Istleistung',
]; ];
$energievariablen = [ $energievariablen = [
'PVEnergie' => 'PV-Energie', 'PVEnergie' => 'PV-Energie',
@@ -1631,6 +1710,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'EinspeisungEnergie' => 'Einspeisung Energie', 'EinspeisungEnergie' => 'Einspeisung Energie',
'BatterieLadenEnergie' => 'Batterie geladen', 'BatterieLadenEnergie' => 'Batterie geladen',
'BatterieEntladenEnergie' => 'Batterie entladen', 'BatterieEntladenEnergie' => 'Batterie entladen',
'SDLLadenEnergie' => 'SDL geladen',
'SDLEntladenEnergie' => 'SDL entladen',
]; ];
$position = 200; $position = 200;
foreach ($leistungsvariablen as $ident => $name) { foreach ($leistungsvariablen as $ident => $name) {
@@ -1644,6 +1725,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$position += 10; $position += 10;
} }
$this->RegisterVariableFloat('SDLSOCArchiv', 'SDL Ladezustand', '~Intensity.100', 390);
IPS_SetHidden($this->GetIDForIdent('SDLSOCArchiv'), !$this->ReadPropertyBoolean('MesswerteAnzeigen'));
if ($this->ReadPropertyBoolean('SDLAktiv')) {
$this->RegisterVariableString('SDLStatus', 'SDL-Status', '', 120);
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) { if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'NetzleistungGueltig', 'NetzleistungGueltig',
@@ -1702,6 +1789,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
foreach (self::DIAGNOSE_VARIABLEN as $ident) { foreach (self::DIAGNOSE_VARIABLEN as $ident) {
if ($ident === 'SDLStatus' && $this->ReadPropertyBoolean('SDLAktiv')) {
continue;
}
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
$this->UnregisterVariable($ident); $this->UnregisterVariable($ident);
@@ -1977,7 +2067,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
]; ];
} }
/** @return array{PV: float, Haus: float, Netz: float, Batterie: float} */ /** @return array{PV: float, Haus: float, Netz: float, Batterie: float, SDL: float} */
private function leseMessleistungen(): array private function leseMessleistungen(): array
{ {
$pv = 0.0; $pv = 0.0;
@@ -1990,12 +2080,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
$netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID')) $netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID'))
* $this->ReadPropertyFloat('Netzleistungsfaktor'); * $this->ReadPropertyFloat('Netzleistungsfaktor');
$sdl = $this->aktuelleSDLLeistung();
return [ return [
'PV' => $pv, 'PV' => $pv,
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie), 'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie, $sdl),
'Netz' => $netz, 'Netz' => $netz,
'Batterie' => $batterie, 'Batterie' => $batterie,
'SDL' => $sdl,
]; ];
} }
@@ -2020,7 +2112,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
/** @return array<string, float>|null */ /** @return array<string, float>|null */
private function leseEnergieZaehlerDeltas(): ?array private function leseEnergieZaehlerDeltas(
float $sdlLaden = 0.0,
float $sdlEntladen = 0.0,
bool $hausVollstaendig = true
): ?array
{ {
$quellen = $this->energieZaehlerquellen(); $quellen = $this->energieZaehlerquellen();
if ($quellen === null) { if ($quellen === null) {
@@ -2049,6 +2145,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$delta = $stand - (float) $alt[$schluessel]; $delta = $stand - (float) $alt[$schluessel];
if ($delta >= 0.0) { if ($delta >= 0.0) {
$deltas[$ident] += $delta; $deltas[$ident] += $delta;
} elseif ($this->ReadPropertyBoolean('SDLAktiv')) {
$hausVollstaendig = false;
} }
} }
} }
@@ -2060,13 +2158,17 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return array_fill_keys(self::ENERGIE_VARIABLEN, 0.0); return array_fill_keys(self::ENERGIE_VARIABLEN, 0.0);
} }
$deltas['Hausenergie'] = EnergieMessung::hausenergie( $deltas['SDLLadenEnergie'] = $sdlLaden;
$deltas['SDLEntladenEnergie'] = $sdlEntladen;
$deltas['Hausenergie'] = $hausVollstaendig ? EnergieMessung::hausenergie(
$deltas['PVEnergie'], $deltas['PVEnergie'],
$deltas['NetzbezugEnergie'], $deltas['NetzbezugEnergie'],
$deltas['EinspeisungEnergie'], $deltas['EinspeisungEnergie'],
$deltas['BatterieLadenEnergie'], $deltas['BatterieLadenEnergie'],
$deltas['BatterieEntladenEnergie'] $deltas['BatterieEntladenEnergie'],
); $sdlLaden,
$sdlEntladen
) : 0.0;
return $deltas; return $deltas;
} }
@@ -2077,13 +2179,31 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return; return;
} }
$jetzt = time(); $jetzt = time();
try {
$leistungen = $this->leseMessleistungen(); $leistungen = $this->leseMessleistungen();
} catch (Throwable $fehler) {
// Keine falsche Hauslast aufzeichnen oder ueber die Messluecke integrieren.
$this->WriteAttributeInteger('MessungLetzterZeitstempel', 0);
$this->WriteAttributeString('MessungZaehlerstaende', '{}');
$this->setzeDiagnosewert('SDLStatus', 'Fehler: ' . $fehler->getMessage());
$this->protokolliere('Energiemessung', $fehler->getMessage());
return;
}
$letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel'); $letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel');
$puffer = $this->leseJsonAttribut('MessungEnergiePuffer'); $puffer = $this->leseJsonAttribut('MessungEnergiePuffer');
$energieDeltas = $this->leseEnergieZaehlerDeltas(); $dauer = $letzterZeitstempel > 0 ? max(0, $jetzt - $letzterZeitstempel) : 0;
$sdlIntervallGueltig = $letzterZeitstempel > 0 && $dauer <= 300;
$integriert = EnergieMessung::energieDeltas(
$leistungen,
$this->ReadPropertyBoolean('SDLAktiv') && !$sdlIntervallGueltig ? 0 : min(300, $dauer)
);
$energieDeltas = $this->leseEnergieZaehlerDeltas(
$integriert['SDLLadenEnergie'],
$integriert['SDLEntladenEnergie'],
!$this->ReadPropertyBoolean('SDLAktiv') || $sdlIntervallGueltig
);
if ($energieDeltas === null && $letzterZeitstempel > 0) { if ($energieDeltas === null && $letzterZeitstempel > 0) {
$sekunden = min(300, max(0, $jetzt - $letzterZeitstempel)); $energieDeltas = $integriert;
$energieDeltas = EnergieMessung::energieDeltas($leistungen, $sekunden);
} }
foreach ($energieDeltas ?? [] as $ident => $delta) { foreach ($energieDeltas ?? [] as $ident => $delta) {
$puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta; $puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta;
@@ -2102,10 +2222,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv' => 'Haus', 'HausverbrauchLeistungArchiv' => 'Haus',
'NetzleistungArchiv' => 'Netz', 'NetzleistungArchiv' => 'Netz',
'BatterieleistungArchiv' => 'Batterie', 'BatterieleistungArchiv' => 'Batterie',
'SDLLeistungArchiv' => 'SDL',
] as $ident => $quelle) { ] as $ident => $quelle) {
$this->SetValue($ident, $leistungen[$quelle]); $this->SetValue($ident, $leistungen[$quelle]);
} }
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt); $this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt);
$soc = $this->aktuelleSDLSOC();
if ($soc !== null) {
$this->SetValue('SDLSOCArchiv', $soc);
}
} }
$energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60; $energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60;
@@ -2120,6 +2245,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt); $this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt);
} }
$this->aktualisiereDiagrammMesswerte(true);
$funFactsID = @IPS_GetObjectIDByIdent('EnergieFunFacts', $this->InstanceID); $funFactsID = @IPS_GetObjectIDByIdent('EnergieFunFacts', $this->InstanceID);
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) { if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
$this->aktualisiereFunFacts($funFactsID); $this->aktualisiereFunFacts($funFactsID);
@@ -2618,11 +2744,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'Prognosestatus', 'Prognosestatus',
$this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet' $this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet'
); );
foreach (['SDLAnschluss' => 'SDLStatus'] as $property => $variable) { $this->aktualisiereSDLStatus();
$konfiguration = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
$aktiv = is_array($konfiguration) && ($konfiguration['Aktiv'] ?? false) === true;
$this->setzeDiagnosewert($variable, $aktiv ? 'Fehler' : 'NichtVerwendet');
}
$this->setzeDiagnosewert( $this->setzeDiagnosewert(
'Stoerueberwachungsstatus', 'Stoerueberwachungsstatus',
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet' $this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet'
@@ -2631,6 +2753,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function pruefeKonfiguration(): void private function pruefeKonfiguration(): void
{ {
$this->pruefeSDLKonfiguration();
foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) { foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) { if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
@@ -2647,6 +2770,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new InvalidArgumentException($property . ' darf nicht negativ sein.'); throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
} }
} }
foreach (['DiagrammPVModus', 'DiagrammBatterieModus'] as $property) {
if (!in_array($this->ReadPropertyInteger($property), [0, 1, 2], true)) {
throw new InvalidArgumentException($property . ' muss 0, 1 oder 2 sein.');
}
}
if (!in_array($this->ReadPropertyInteger('EnergieflussLeistungseinheit'), [0, 1], true)) {
throw new InvalidArgumentException(
'EnergieflussLeistungseinheit muss 0 oder 1 sein.'
);
}
$energieLoeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate'); $energieLoeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate');
$energieVerdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate'); $energieVerdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate');
if ($energieLoeschen > 0 && $energieLoeschen <= $energieVerdichten) { if ($energieLoeschen > 0 && $energieLoeschen <= $energieVerdichten) {
+5 -4
View File
@@ -112,6 +112,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
@@ -150,7 +151,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->regelzyklus(true); $this->regelzyklus(true);
return; return;
@@ -208,7 +209,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true) && !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->regelzyklus(false); $this->regelzyklus(false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -290,7 +291,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$ziel = 0; $ziel = 0;
@@ -301,7 +302,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
} elseif ((bool) $this->leseZustand('SollwertGueltig')) { } elseif ((bool) $this->leseZustand('SollwertGueltig')) {
$ziel = (int) $this->leseZustand('Sollleistung'); $ziel = (int) $this->leseZustand('Sollleistung');
if (!in_array($ziel, $angebot, true)) { if (!in_array($ziel, $angebot, true)) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$ziel = 0; $ziel = 0;
} }
} }
+6 -5
View File
@@ -118,6 +118,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->registriereMeldungen(); $this->registriereMeldungen();
try { try {
@@ -158,7 +159,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
$this->aktualisiere(true); $this->aktualisiere(true);
@@ -214,7 +215,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true) && !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->planeMeldung(); $this->planeMeldung();
return; return;
@@ -254,7 +255,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
true true
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$aktiv = (bool) $this->GetValue('Aktiv'); $aktiv = (bool) $this->GetValue('Aktiv');
@@ -474,7 +475,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
} }
@@ -528,7 +529,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt)); $this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt));
$this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt); $this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt);
$this->setzeZustand('Tageslaufzeit', 0); $this->setzeZustand('Tageslaufzeit', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->setzeZustand('Rueckmeldefehler', false); $this->setzeZustand('Rueckmeldefehler', false);
$this->WriteAttributeBoolean('Initialisiert', true); $this->WriteAttributeBoolean('Initialisiert', true);
} }
+5 -4
View File
@@ -132,6 +132,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->registriereMeldungen(); $this->registriereMeldungen();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -172,7 +173,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
$this->aktualisiere(true); $this->aktualisiere(true);
@@ -230,7 +231,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true) && !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->planeMeldung(); $this->planeMeldung();
return; return;
@@ -286,7 +287,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
$this->WriteAttributeInteger('Laufbeginn', $laeuft ? $jetzt : 0); $this->WriteAttributeInteger('Laufbeginn', $laeuft ? $jetzt : 0);
$this->schalte(WaermepumpenRegler::STEUERZUSTAND_NORMAL, $jetzt); $this->schalte(WaermepumpenRegler::STEUERZUSTAND_NORMAL, $jetzt);
$steuerzustand = WaermepumpenRegler::STEUERZUSTAND_NORMAL; $steuerzustand = WaermepumpenRegler::STEUERZUSTAND_NORMAL;
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->aktualisiereSperrerholung($laeuft, $jetzt); $this->aktualisiereSperrerholung($laeuft, $jetzt);
@@ -828,7 +829,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
$ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit') $ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit')
+ $this->ReadPropertyInteger('VorgabeTimeout'); + $this->ReadPropertyInteger('VorgabeTimeout');
if ($this->ReadAttributeInteger('LetzteVorgabeZeit') <= 0 || $jetzt >= $ablauf) { if ($this->ReadAttributeInteger('LetzteVorgabeZeit') <= 0 || $jetzt >= $ablauf) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
} }
+4 -3
View File
@@ -122,6 +122,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
$this->initialisiereTemperaturen(); $this->initialisiereTemperaturen();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -219,7 +220,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true) && !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->regelzyklus(false); $this->regelzyklus(false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -264,7 +265,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$ziel = 0; $ziel = 0;
@@ -276,7 +277,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
} elseif ((bool) $this->leseZustand('SollwertGueltig')) { } elseif ((bool) $this->leseZustand('SollwertGueltig')) {
$ziel = (int) $this->leseZustand('Sollleistung'); $ziel = (int) $this->leseZustand('Sollleistung');
if (!in_array($ziel, $angebot, true)) { if (!in_array($ziel, $angebot, true)) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$ziel = 0; $ziel = 0;
} }
} }
+23
View File
@@ -41,6 +41,29 @@ berechnet sein Leistungsangebot neu und meldet es zurueck. Eine vorhandene
Sollleistung wird dabei nur verworfen, wenn sie im neuen Angebot nicht mehr Sollleistung wird dabei nur verworfen, wenn sie im neuen Angebot nicht mehr
zulaessig ist. zulaessig ist.
### Verworfene Sollwerte und Anzeige
Beim Verwerfen einer gueltigen Vorgabe setzen alle Verbrauchermodule sowohl
`SollwertGueltig=false` als auch den gespeicherten und gegebenenfalls sichtbaren
Wert `Sollleistung=0`. An den Manager geht weiterhin `Sollleistung_W=null`;
die Anzeige 0 W ist keine neue gueltige Manager-Vorgabe und kein Nachweis einer
physisch ausgeschalteten Last.
Lokale Schutzprogramme, Mindestlaufzeiten und bestehende Lade-Uebergaenge
bleiben unveraendert. Berechnet ein Modul danach eine lokale Sollleistung,
darf es diese weiterhin anzeigen. Bei Ladestationen bleibt waehrend eines
Regeluebergangs insbesondere der letzte Geraetebefehl erhalten, auch wenn die
alte Manager-Vorgabe bereits verworfen und ihre Anzeige auf 0 gesetzt wurde.
Wiederholte Verwerfungen einer bereits ungueltigen Manager-Vorgabe lassen eine
inzwischen neu berechnete lokale Schutzleistung unveraendert.
Nach einem Modulupdate bereinigt `ApplyChanges` bereits gespeicherte ungueltige
Altwerte. Gueltige Vorgaben einschliesslich 0 W und negativer Batterieleistung
bleiben erhalten. Keine neuen Properties, Variablen-IDs oder Vertragsversion;
es ist keine manuelle Konfigurationsmigration erforderlich. Das Update ist
kontrolliert anzuwenden, da die regulaere Instanzinitialisierung wie bisher
Geraeteaktionen ausloesen kann.
## Verbraucher an Manager ## Verbraucher an Manager
```json ```json
+89 -9
View File
@@ -73,6 +73,12 @@ Die drei Regelungsvariablen `Aktiv`, `Betriebsart` und `Netzleistung` sind immer
| `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. | | `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. |
| `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. | | `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. |
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. | | `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
| `SDLAktiv` | Boolean | `false`; separat gemessene SDL-/Regelenergie in der Anlagenbilanz beruecksichtigen. Keine SDL-Steuerung. |
| `SDLLeistungVariableID` | Integer | `0`; bei aktiver SDL erforderliche numerische Istleistungsquelle. |
| `SDLLeistungsfaktor` | Float | `1`; Umrechnung nach W, positiv Laden/Bezug, negativ Entladen/Abgabe. Fuer kW `1000`, bei umgekehrtem Vorzeichen negativ. |
| `SDLSOCVariableID` | Integer | `0`; optionaler separater SDL-Ladezustand in Prozent, 0 bis 100. |
| `SDLEnergieflussAnzeigen` | Boolean | `false`; SDL mit optionalem SOC als eigenen Speicher im aktivierten Energiefluss anzeigen. |
| `SDLDiagrammeAnzeigen` | Boolean | `false`; SDL-Leistung, Lade-/Entladeenergie und optionalen SOC in aktivierten Diagrammen anzeigen. |
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. | | `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. |
@@ -134,6 +140,59 @@ entfernt beziehungsweise die native Energy Distribution ausgeblendet:
- responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten, - responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten,
- native Energy Distribution fuer PV, Netz, Haus und Batterie. - native Energy Distribution fuer PV, Netz, Haus und Batterie.
## SDL / Regelenergie
`SDL aktiv` blendet die Quellen- und Anzeigeauswahl ein. Die SDL-Istleistung
beschreibt einen **separat gemessenen AC-Zweig am selben Netzanschlusspunkt**.
Sie darf weder in der normalen Batterieleistung noch in einer PV-Leistung
enthalten sein. Eine identische Variablen-ID fuer SDL und Netz/PV/Batterie wird
abgewiesen. Ueberschneidungen in extern gebildeten Summen muss die
Anlagenkonfiguration ausschliessen. Ein gemeinsamer Speicher mit nur einem
Gesamtleistungsmesswert kann damit nicht in EMS- und SDL-Anteile zerlegt werden.
Bei aktiver SDL lautet die Hauslast `max(0, PV + Netz - Batterie - SDL)`.
Die Korrektur geschieht vor der Begrenzung auf null und unabhaengig davon,
ob SDL angezeigt wird. Diese Hauslast wird an den Prognosedienst gesendet;
Netzleistung und der kapazitaetsgewichtete SOC der normalen Batterien bleiben
unveraendert. SDL-SOC ersetzt keinen Batterie-SOC und wird nicht als
planbare EMS-Speicherkapazitaet exportiert. Ungueltige SDL-Leistungswerte
unterbrechen die Messaufzeichnung und den jeweiligen Telemetrieversand,
statt als null in die Hauslast einzugehen. Der SDL-Status ist bei aktiver
SDL auch ohne eingeschaltete Diagnosevariablen sichtbar.
`SDLLeistungArchiv` zeichnet Watt auf, `SDLSOCArchiv` gueltige Prozentwerte.
`SDLLadenEnergie` und `SDLEntladenEnergie` integrieren die getrennten Richtungen
in kWh. Die Abtastung erfolgt wie bei den anderen Leistungen minuetlich, nicht
als abrechnungsgenaue SDL-Energiemessung. Auch bei vorhandenen physikalischen
Energiezaehlern wird die Hausenergie um die im selben Intervall integrierte
SDL-Ladung vermindert und um SDL-Entladung erhoeht. Bei Messluecken ueber
fuenf Minuten, Quellenwechseln oder Zaehlerresets wird keine unvollstaendige
Hausenergie nachberechnet; Historie und bereits erfasste Zaehler bleiben erhalten.
Die Anzeigeoptionen wirken beim Anwenden direkt auf die vorhandenen
Manager-Visualisierungen. Nur die eigenen SDL-Knoten und -Datensaetze werden
ergaenzt oder entfernt. Andere Knoten, Farben, Datensaetze und sonstige
Anpassungen bleiben bestehen. Im Energiefluss verwendet SDL dieselbe W/kW-
Einheit wie die anderen Manager-Knoten; ein ungueltiger SOC wird dort nicht
als aktueller Zusatzwert angezeigt. Ohne separaten SDL-Knoten enthaelt der
Manager-Sammelknoten Batteriespeicher die normale Batterie plus SDL, damit
die dargestellte Leistungsbilanz vollstaendig bleibt. Im Leistungsdiagramm
erscheint die signierte SDL-Istleistung, im Energiediagramm erscheinen Laden
und Entladen getrennt. SOC verwendet in beiden Diagrammen eine eigene rechte
Prozentachse. Die allgemeinen Schalter fuer Energiefluss, Diagramme und
Energieaufzeichnung bleiben erforderlich.
Migration: SDL bleibt nach dem Modulupdate standardmaessig aus. Bestehende
Installationen behalten damit ihre bisherige Bilanz. Erst Quellen und
Vorzeichen pruefen, SDL aktivieren und anwenden. Historische Hauslasten oder
bereits trainierte Prognosen werden nicht rueckwirkend korrigiert; neue
Prognoseeingaben sind ab Aktivierung bereinigt. Ausschalten entfernt nur
SDL-Anzeigen, nicht die aufgezeichnete Historie. Der alte `SDLAnschluss`
bleibt lesbar; externe SDL-Regelung und Stellbefehle werden nicht veraendert.
Native Darstellungsvertraege: [Energy Distribution](https://www.symcon.de/en/service/documentation/module-reference/energy/energy-distribution/)
und [Diagramme](https://www.symcon.de/en/service/documentation/basics/media/charts/).
## Anlagenweite Einspeisebegrenzung ## Anlagenweite Einspeisebegrenzung
Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der
@@ -310,15 +369,36 @@ Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
wird mit genau `11000 W` zugeteilt. wird mit genau `11000 W` zugeteilt.
Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
sortiert. Bei gleicher Priorität bevorzugt der Manager wie Enelix 1 den sortiert. Zuerst reserviert der Manager die richtungsneutralen erlaubten Werte
Verbraucher mit der bisher kleineren bezogenen Energie; verglichen wird in sowie die gemessene Leistung fester oder gesperrter Verbraucher.
2-kWh-Gruppen. Erst wenn auch diese Gruppe gleich ist, entscheidet stabil die
Instanz-ID. Zuerst reserviert der Manager für jeden Verbraucher den kleinsten Positives Restbudget wird innerhalb jeder Priorität schrittweise verteilt:
erlaubten Leistungswert. Nur das danach Zuerst kommt die kleinste nächste erreichbare **absolute Sollleistung**, nicht
verbleibende Budget wird in Prioritätsreihenfolge aufgefüllt. Einzelwerte und die kleinste Erhöhung. Bei gleicher nächster Sollleistung entscheidet die exakt
ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch gezählte bezogene Energie in Wh, danach stabil die Instanz-ID. Die bisherigen
unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im 2-kWh-Gruppen werden für diese Vergabe nicht mehr verwendet. Vom Budget wird nur
`Verbraucherstatus` dokumentiert. die Differenz zum bereits zugeteilten Sollwert abgezogen. Ein nicht finanzierbarer
Schritt wird übersprungen; andere passende Schritte und danach tiefere
Prioritäten können das verbleibende Budget nutzen.
Beispiel bei gleicher Priorität: A bietet `[0,100,200,400,800]` bei 10 kWh,
B bietet `[0,100,110,500,780,1500]` bei 12 kWh. Die Zuteilungsfolge bei
ausreichendem Budget ist `A100, B100, B110, A200, A400, B500, B780, A800, B1500`.
Die zugehörigen Gesamtbudgets sind 100, 200, 210, 310, 510, 900, 1180, 1580 und
2300 W. Energie entscheidet nur bei gleichen nächsten Leistungsstufen, nicht
mehr über die vollständige Versorgung eines Verbrauchers vor allen anderen.
Ganzzahlige Bereiche folgen derselben Reihenfolge wie einzelne Wattstufen,
werden aber über gemeinsame Leistungsniveaus effizient verarbeitet. Lücken
werden nie durch unzulässige Werte geschlossen. Bei negativem Restbudget bleibt
die bestehende Defizitregelung einschliesslich ihrer Energiegruppen unverändert.
Eine verbleibende Abweichung wird im `Verbraucherstatus` dokumentiert.
Für das Update sind keine neuen Properties oder eine Migration nötig. Bestehende
Energiezähler, Sperrzeiten und Freigaben bleiben erhalten. Nach dem Modulupdate
gilt die neue Vergabe beim nächsten Regellauf. Vor einem Update einer laufenden
Anlage Einstellungen sichern und die neuen Sollwerte kontrolliert prüfen;
die Git-Veröffentlichung allein aktualisiert keine laufende Installation.
Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
+14 -5
View File
@@ -8,11 +8,11 @@ final class EnergieMessung
{ {
private const WATTSEKUNDEN_PRO_KWH = 3600000.0; private const WATTSEKUNDEN_PRO_KWH = 3600000.0;
public static function hausleistung(float $pv, float $netz, float $batterie): float public static function hausleistung(float $pv, float $netz, float $batterie, float $sdl = 0.0): float
{ {
return max( return max(
0.0, 0.0,
self::endlich($pv) + self::endlich($netz) - self::endlich($batterie) self::endlich($pv) + self::endlich($netz) - self::endlich($batterie) - self::endlich($sdl)
); );
} }
@@ -21,7 +21,9 @@ final class EnergieMessung
float $netzbezug, float $netzbezug,
float $netzeinspeisung, float $netzeinspeisung,
float $batterieLaden, float $batterieLaden,
float $batterieEntladen float $batterieEntladen,
float $sdlLaden = 0.0,
float $sdlEntladen = 0.0
): float { ): float {
return max( return max(
0.0, 0.0,
@@ -30,18 +32,22 @@ final class EnergieMessung
+ self::endlich($batterieEntladen) + self::endlich($batterieEntladen)
- self::endlich($netzeinspeisung) - self::endlich($netzeinspeisung)
- self::endlich($batterieLaden) - self::endlich($batterieLaden)
- self::endlich($sdlLaden)
+ self::endlich($sdlEntladen)
); );
} }
/** /**
* @param array{PV: float, Haus: float, Netz: float, Batterie: float} $leistungen * @param array{PV: float, Haus: float, Netz: float, Batterie: float, SDL?: float} $leistungen
* @return array{ * @return array{
* PVEnergie: float, * PVEnergie: float,
* Hausenergie: float, * Hausenergie: float,
* NetzbezugEnergie: float, * NetzbezugEnergie: float,
* EinspeisungEnergie: float, * EinspeisungEnergie: float,
* BatterieLadenEnergie: float, * BatterieLadenEnergie: float,
* BatterieEntladenEnergie: float * BatterieEntladenEnergie: float,
* SDLLadenEnergie: float,
* SDLEntladenEnergie: float
* } * }
*/ */
public static function energieDeltas(array $leistungen, int $sekunden): array public static function energieDeltas(array $leistungen, int $sekunden): array
@@ -51,6 +57,7 @@ final class EnergieMessung
$haus = self::endlich($leistungen['Haus']); $haus = self::endlich($leistungen['Haus']);
$netz = self::endlich($leistungen['Netz']); $netz = self::endlich($leistungen['Netz']);
$batterie = self::endlich($leistungen['Batterie']); $batterie = self::endlich($leistungen['Batterie']);
$sdl = self::endlich($leistungen['SDL'] ?? 0.0);
return [ return [
'PVEnergie' => max(0.0, $pv) * $faktor, 'PVEnergie' => max(0.0, $pv) * $faktor,
@@ -59,6 +66,8 @@ final class EnergieMessung
'EinspeisungEnergie' => max(0.0, -$netz) * $faktor, 'EinspeisungEnergie' => max(0.0, -$netz) * $faktor,
'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor, 'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor,
'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor, 'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor,
'SDLLadenEnergie' => max(0.0, $sdl) * $faktor,
'SDLEntladenEnergie' => max(0.0, -$sdl) * $faktor,
]; ];
} }
+15 -1
View File
@@ -91,7 +91,7 @@ final class LadestationAdapter
} }
/** /**
* @return array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} * @return array{Rohstatus: int, Istleistung_W: float, Sollstrom_A?: int, Maximalstrom_A?: int, Iststrom_A?: float, Phasenzahl?: int}
*/ */
public static function dekodiereStatus(int $geraetetyp, string $antwort): array public static function dekodiereStatus(int $geraetetyp, string $antwort): array
{ {
@@ -115,6 +115,20 @@ final class LadestationAdapter
'Rohstatus' => (int) $daten['car'], 'Rohstatus' => (int) $daten['car'],
'Istleistung_W' => (float) $daten['nrg'][11] * $leistungsfaktor, 'Istleistung_W' => (float) $daten['nrg'][11] * $leistungsfaktor,
]; ];
if (isset($daten['amp']) && is_numeric($daten['amp'])) {
$sollstrom = (int) $daten['amp'];
$freigabeEntzogen = isset($daten['alw']) && $daten['alw'] === false;
$erzwungenAus = $geraetetyp === self::GO_E_GEMINI
&& isset($daten['frc'])
&& (int) $daten['frc'] === 1;
$status['Sollstrom_A'] = $freigabeEntzogen || $erzwungenAus ? 0 : $sollstrom;
}
if (isset($daten['ama']) && is_numeric($daten['ama'])) {
$maximalstrom = (int) $daten['ama'];
if ($maximalstrom >= 6 && $maximalstrom <= 32) {
$status['Maximalstrom_A'] = $maximalstrom;
}
}
if (isset($daten['nrg'][4], $daten['nrg'][5], $daten['nrg'][6]) if (isset($daten['nrg'][4], $daten['nrg'][5], $daten['nrg'][6])
&& is_numeric($daten['nrg'][4]) && is_numeric($daten['nrg'][4])
&& is_numeric($daten['nrg'][5]) && is_numeric($daten['nrg'][5])
+634 -50
View File
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace Belevo\EnelixEMS; namespace Belevo\EnelixEMS;
use RuntimeException;
use Throwable;
trait ManagerEnergieTrait trait ManagerEnergieTrait
{ {
private function konfiguriereEnergiearchiv(): void private function konfiguriereEnergiearchiv(): void
@@ -23,9 +26,13 @@ trait ManagerEnergieTrait
); );
foreach (self::LEISTUNGS_VARIABLEN as $ident) { foreach (self::LEISTUNGS_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident); $variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv); $loggen = $aktiv && (!str_starts_with($ident, 'SDL') || $this->ReadPropertyBoolean('SDLAktiv'))
&& ($ident !== 'SDLSOCArchiv' || $this->ReadPropertyInteger('SDLSOCVariableID') > 0);
if (!$this->setzeArchivLogging($archivID, $variableID, $loggen)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, 0); AC_SetAggregationType($archivID, $variableID, 0);
AC_SetGraphStatus($archivID, $variableID, $aktiv); AC_SetGraphStatus($archivID, $variableID, $loggen);
$regeln = [-1 => $leistungstyp]; $regeln = [-1 => $leistungstyp];
$loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate'); $loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate');
if ($loeschen > 0) { if ($loeschen > 0) {
@@ -38,7 +45,9 @@ trait ManagerEnergieTrait
foreach (self::ENERGIE_VARIABLEN as $ident) { foreach (self::ENERGIE_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident); $variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv); if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, 1); AC_SetAggregationType($archivID, $variableID, 1);
AC_SetGraphStatus($archivID, $variableID, $aktiv); AC_SetGraphStatus($archivID, $variableID, $aktiv);
$regeln = [-1 => $energietyp]; $regeln = [-1 => $energietyp];
@@ -55,12 +64,57 @@ trait ManagerEnergieTrait
} }
} }
foreach (IPS_GetChildrenIDs($this->InstanceID) as $variableID) {
$objekt = IPS_GetObject($variableID);
$ident = (string) ($objekt['ObjectIdent'] ?? '');
if ((int) ($objekt['ObjectType'] ?? -1) !== 2
|| !str_starts_with($ident, 'Diagramm')
) {
continue;
}
$istEnergie = str_contains($ident, 'Energie')
|| str_contains($ident, 'BatterieLaden_')
|| str_contains($ident, 'BatterieEntladen_');
if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, $istEnergie ? 1 : 0);
AC_SetGraphStatus($archivID, $variableID, $aktiv);
$regeln = [-1 => $istEnergie ? $energietyp : $leistungstyp];
$verdichten = $istEnergie
? $this->ReadPropertyInteger('EnergieVerdichtenMonate')
: 0;
if ($verdichten > 0) {
$regeln[$verdichten] = 3;
}
$loeschen = $istEnergie
? $this->ReadPropertyInteger('EnergieLoeschenMonate')
: $this->ReadPropertyInteger('LeistungLoeschenMonate');
if ($loeschen > 0) {
$regeln[$loeschen] = 7;
}
if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) {
$geaendert[] = $variableID;
}
}
IPS_ApplyChanges($archivID); IPS_ApplyChanges($archivID);
foreach (array_unique($geaendert) as $variableID) { foreach (array_unique($geaendert) as $variableID) {
AC_ReAggregateVariable($archivID, $variableID); AC_ReAggregateVariable($archivID, $variableID);
} }
} }
private function setzeArchivLogging(int $archivID, int $variableID, bool $aktiv): bool
{
// Archiv-Metadaten sind nur bei aktivierter Aufzeichnung aenderbar.
if (!$aktiv && AC_GetLoggingStatus($archivID, $variableID)) {
AC_SetGraphStatus($archivID, $variableID, false);
}
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
return $aktiv;
}
/** @param array<int, int> $regeln */ /** @param array<int, int> $regeln */
private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool
{ {
@@ -129,6 +183,7 @@ trait ManagerEnergieTrait
} }
$this->entferneDiagrammUnterobjekt('Leistungsdiagramm'); $this->entferneDiagrammUnterobjekt('Leistungsdiagramm');
$this->entferneDiagrammUnterobjekt('Energiediagramm'); $this->entferneDiagrammUnterobjekt('Energiediagramm');
$this->WriteAttributeString('EnergiediagrammKonfiguration', '{}');
} }
private function synchronisiereFunFacts(bool $anzeigen): void private function synchronisiereFunFacts(bool $anzeigen): void
@@ -154,9 +209,7 @@ trait ManagerEnergieTrait
return; return;
} }
$instanzID = $this->findeUnterobjekt('Energiefluss'); $instanzID = $this->findeUnterobjekt('Energiefluss');
if ($instanzID === 0) { if ($instanzID !== 0) {
return;
}
$objekt = IPS_GetObject($instanzID); $objekt = IPS_GetObject($instanzID);
$modulID = $objekt['ObjectType'] === 1 $modulID = $objekt['ObjectType'] === 1
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] ? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
@@ -166,6 +219,9 @@ trait ManagerEnergieTrait
} }
IPS_SetHidden($instanzID, true); IPS_SetHidden($instanzID, true);
} }
$this->WriteAttributeString('EnergieflussKonfiguration', '{}');
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', true);
}
private function entferneInstanzUnterobjekt(string $ident, string $modulID): void private function entferneInstanzUnterobjekt(string $ident, string $modulID): void
{ {
@@ -216,6 +272,9 @@ trait ManagerEnergieTrait
if ($modulID !== self::ENERGY_PIE_MODULE_ID) { if ($modulID !== self::ENERGY_PIE_MODULE_ID) {
throw new RuntimeException('Unterobjekt EnergyPie hat einen unpassenden Typ.'); throw new RuntimeException('Unterobjekt EnergyPie hat einen unpassenden Typ.');
} }
IPS_SetHidden($instanzID, false);
return 'Bestehende Energy-Pie-Konfiguration bleibt unveraendert.';
} }
IPS_SetName($instanzID, 'Energy Pie'); IPS_SetName($instanzID, 'Energy Pie');
IPS_SetPosition($instanzID, 500); IPS_SetPosition($instanzID, 500);
@@ -239,6 +298,7 @@ trait ManagerEnergieTrait
if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) { if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) {
throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.'); throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.');
} }
$neuErzeugen = $this->ReadAttributeBoolean('EnergieflussNeuErzeugen');
$instanzID = $this->findeUnterobjekt('Energiefluss'); $instanzID = $this->findeUnterobjekt('Energiefluss');
if ($instanzID === 0) { if ($instanzID === 0) {
$instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID); $instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID);
@@ -252,7 +312,16 @@ trait ManagerEnergieTrait
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) { if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.'); throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
} }
if (!$neuErzeugen) {
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
$this->synchronisiereSDLEnergiefluss($instanzID);
IPS_SetHidden($instanzID, false);
$this->aktualisiereEnergieflusswerte();
return 'Bestehende Energiefluss-Konfiguration bleibt unveraendert.';
} }
}
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
IPS_SetName($instanzID, 'Energiefluss'); IPS_SetName($instanzID, 'Energiefluss');
IPS_SetPosition($instanzID, 540); IPS_SetPosition($instanzID, 540);
IPS_SetHidden($instanzID, false); IPS_SetHidden($instanzID, false);
@@ -289,7 +358,6 @@ trait ManagerEnergieTrait
$this->energieflussKnoten($netzID, 'Stromnetz', 'grid'), $this->energieflussKnoten($netzID, 'Stromnetz', 'grid'),
$this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'), $this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'),
]; ];
$hatBatterieverbraucher = false;
$position = 640; $position = 640;
foreach ($verbraucher as $eintrag) { foreach ($verbraucher as $eintrag) {
$istID = $this->energieflussLeistungsvariable( $istID = $this->energieflussLeistungsvariable(
@@ -311,17 +379,13 @@ trait ManagerEnergieTrait
true, true,
$sollID $sollID
); );
$hatBatterieverbraucher = $hatBatterieverbraucher
|| $eintrag['Typ'] === 'storage';
$position += 10; $position += 10;
} }
if (!$hatBatterieverbraucher) {
$knoten[] = $this->energieflussKnoten( $knoten[] = $this->energieflussKnoten(
$batterieID, $batterieID,
'Batteriespeicher', 'Batteriespeicher',
'storage' 'storage'
); );
}
IPS_SetProperty($instanzID, 'Calculation', true); IPS_SetProperty($instanzID, 'Calculation', true);
IPS_SetProperty( IPS_SetProperty(
@@ -332,6 +396,8 @@ trait ManagerEnergieTrait
IPS_SetProperty($instanzID, 'EnableLimits', false); IPS_SetProperty($instanzID, 'EnableLimits', false);
IPS_SetProperty($instanzID, 'Restrictions', '[]'); IPS_SetProperty($instanzID, 'Restrictions', '[]');
IPS_ApplyChanges($instanzID); IPS_ApplyChanges($instanzID);
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', false);
$this->synchronisiereSDLEnergiefluss($instanzID, true);
$this->aktualisiereEnergieflusswerte($verbraucher); $this->aktualisiereEnergieflusswerte($verbraucher);
return 'Der Energiefluss wurde erstellt oder aktualisiert.'; return 'Der Energiefluss wurde erstellt oder aktualisiert.';
@@ -360,7 +426,10 @@ trait ManagerEnergieTrait
private function energieflussLeistungsprofil(): string private function energieflussLeistungsprofil(): string
{ {
if ($this->ReadPropertyInteger('EnergieflussLeistungseinheit') !== 1) { $konfiguration = $this->visualisierungsKonfiguration(
'EnergieflussKonfiguration'
);
if ($konfiguration['EnergieflussLeistungseinheit'] !== 1) {
return '~Power'; return '~Power';
} }
@@ -388,18 +457,55 @@ trait ManagerEnergieTrait
return $variableID; return $variableID;
} }
/** @return array<string, bool|int> */
private function visualisierungsKonfiguration(string $attribut): array
{
$konfiguration = $this->leseJsonAttribut($attribut);
$gueltig = isset(
$konfiguration['DiagrammPVModus'],
$konfiguration['DiagrammBatterieModus'],
$konfiguration['LadestationenSeparatAnzeigen'],
$konfiguration['VerbraucherSeparatAnzeigen'],
$konfiguration['EnergieflussLeistungseinheit']
);
if ($gueltig) {
return $konfiguration;
}
$konfiguration = [
'DiagrammPVModus' => $this->ReadPropertyInteger('DiagrammPVModus'),
'DiagrammBatterieModus' => $this->ReadPropertyInteger('DiagrammBatterieModus'),
'LadestationenSeparatAnzeigen' =>
$this->ReadPropertyBoolean('LadestationenSeparatAnzeigen'),
'VerbraucherSeparatAnzeigen' =>
$this->ReadPropertyBoolean('VerbraucherSeparatAnzeigen'),
'EnergieflussLeistungseinheit' =>
$this->ReadPropertyInteger('EnergieflussLeistungseinheit'),
];
$this->WriteAttributeString(
$attribut,
json_encode($konfiguration, JSON_THROW_ON_ERROR)
);
return $konfiguration;
}
/** /**
* @return list<array{ * @return list<array{
* InstanzID: int, * InstanzID: int,
* Name: string, * Name: string,
* Typ: string, * Typ: string,
* Istleistung_W: float, * Istleistung_W: float,
* Sollleistung_W: float * Sollleistung_W: float,
* Energie_Wh: float
* }> * }>
*/ */
private function energieflussVerbraucher(): array private function energieflussVerbraucher(
{ string $konfigurationsAttribut = 'EnergieflussKonfiguration'
): array {
$konfiguration = $this->visualisierungsKonfiguration($konfigurationsAttribut);
$cache = $this->leseJsonAttribut('VerbraucherCache'); $cache = $this->leseJsonAttribut('VerbraucherCache');
$energien = $this->leseJsonAttribut('VerbraucherEnergie');
$ergebnis = []; $ergebnis = [];
foreach ($this->konfigurierteAktiveVerbraucherIDs() as $instanzID) { foreach ($this->konfigurierteAktiveVerbraucherIDs() as $instanzID) {
if (!IPS_InstanceExists($instanzID)) { if (!IPS_InstanceExists($instanzID)) {
@@ -416,17 +522,30 @@ trait ManagerEnergieTrait
(string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
); );
$katalog = self::VERBRAUCHER_LIZENZEN[$modulID]['Katalog'] ?? ''; $katalog = self::VERBRAUCHER_LIZENZEN[$modulID]['Katalog'] ?? '';
if ($katalog === 'battery') {
continue;
}
if ($katalog === 'ev_charger'
&& !$konfiguration['LadestationenSeparatAnzeigen']
) {
continue;
}
if ($katalog !== 'ev_charger'
&& !$konfiguration['VerbraucherSeparatAnzeigen']
) {
continue;
}
$verbraucherEnergie = $energien[(string) $instanzID]['Energie_Wh'] ?? 0.0;
$ergebnis[] = [ $ergebnis[] = [
'InstanzID' => $instanzID, 'InstanzID' => $instanzID,
'Name' => IPS_GetName($instanzID), 'Name' => IPS_GetName($instanzID),
'Typ' => match ($katalog) { 'Typ' => $katalog === 'ev_charger' ? 'wallbox' : 'consumer',
'battery' => 'storage',
'ev_charger' => 'wallbox',
default => 'consumer',
},
'Istleistung_W' => $istleistung, 'Istleistung_W' => $istleistung,
'Sollleistung_W' => $sollleistung, 'Sollleistung_W' => $sollleistung,
'Energie_Wh' => is_numeric($verbraucherEnergie)
? (float) $verbraucherEnergie
: 0.0,
]; ];
} }
@@ -443,7 +562,6 @@ trait ManagerEnergieTrait
} }
$verbraucher = $verbraucher ?? $this->energieflussVerbraucher(); $verbraucher = $verbraucher ?? $this->energieflussVerbraucher();
$verbraucherleistung = 0.0; $verbraucherleistung = 0.0;
$hatBatterieverbraucher = false;
foreach ($verbraucher as $eintrag) { foreach ($verbraucher as $eintrag) {
$this->setzeEnergieflussLeistungswert( $this->setzeEnergieflussLeistungswert(
'EnergieflussIst_' . $eintrag['InstanzID'], 'EnergieflussIst_' . $eintrag['InstanzID'],
@@ -453,12 +571,8 @@ trait ManagerEnergieTrait
'EnergieflussSoll_' . $eintrag['InstanzID'], 'EnergieflussSoll_' . $eintrag['InstanzID'],
(float) $eintrag['Sollleistung_W'] (float) $eintrag['Sollleistung_W']
); );
if ($eintrag['Typ'] === 'storage') {
$hatBatterieverbraucher = true;
} else {
$verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']); $verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']);
} }
}
try { try {
$leistungen = $this->leseMessleistungen(); $leistungen = $this->leseMessleistungen();
@@ -472,11 +586,18 @@ trait ManagerEnergieTrait
'EnergieflussHaus', 'EnergieflussHaus',
max(0.0, $leistungen['Haus'] - $verbraucherleistung) max(0.0, $leistungen['Haus'] - $verbraucherleistung)
); );
if (!$hatBatterieverbraucher) {
$this->setzeEnergieflussLeistungswert( $this->setzeEnergieflussLeistungswert(
'EnergieflussBatterie', 'EnergieflussBatterie',
$leistungen['Batterie'] $leistungen['Batterie'] + ($this->sdlAnzeigen('SDLEnergieflussAnzeigen') ? 0.0 : $leistungen['SDL'])
); );
$this->setzeEnergieflussLeistungswert('EnergieflussSDL', $leistungen['SDL']);
$soc = $this->aktuelleSDLSOC();
if ($soc !== null) {
$this->SetValue('SDLSOCArchiv', $soc);
}
$flussID = $this->findeUnterobjekt('Energiefluss');
if ($flussID > 0) {
$this->synchronisiereSDLEnergiefluss($flussID);
} }
} }
@@ -486,33 +607,489 @@ trait ManagerEnergieTrait
if (!is_int($variableID) || $variableID <= 0 || !IPS_VariableExists($variableID)) { if (!is_int($variableID) || $variableID <= 0 || !IPS_VariableExists($variableID)) {
return; return;
} }
$faktor = $this->ReadPropertyInteger('EnergieflussLeistungseinheit') === 1 $konfiguration = $this->visualisierungsKonfiguration(
? 0.001 'EnergieflussKonfiguration'
: 1.0; );
$faktor = $konfiguration['EnergieflussLeistungseinheit'] === 1 ? 0.001 : 1.0;
SetValue($variableID, $watt * $faktor); SetValue($variableID, $watt * $faktor);
} }
private function erstelleEnergiediagramme(): string private function erstelleEnergiediagramme(): string
{ {
$leistung = [ $konfiguration = $this->visualisierungsKonfiguration(
$this->chartDatensatz('PVLeistungArchiv', 'PV', '#F4C542', '~Power'), 'EnergiediagrammKonfiguration'
$this->chartDatensatz('HausverbrauchLeistungArchiv', 'Haus', '#22A06B', '~Power'), );
$this->chartDatensatz('NetzleistungArchiv', 'Netz', '#E95D5D', '~Power'), $pvAnlagen = $this->diagrammPVAnlagen();
$this->chartDatensatz('BatterieleistungArchiv', 'Batterie', '#8B5CF6', '~Power'), $batterien = $this->diagrammBatterien();
]; $verbraucher = $this->energieflussVerbraucher(
$energie = [ 'EnergiediagrammKonfiguration'
$this->chartDatensatz('PVEnergie', 'PV', '#F4C542', '~Electricity'), );
$this->chartDatensatz('Hausenergie', 'Haus', '#22A06B', '~Electricity'), $this->registriereDiagrammVariablen($pvAnlagen, $batterien, $verbraucher);
$this->chartDatensatz('NetzbezugEnergie', 'Netzbezug', '#E95D5D', '~Electricity'), $this->aktualisiereDiagrammMesswerte(true);
$this->chartDatensatz('EinspeisungEnergie', 'Einspeisung', '#2F80ED', '~Electricity'),
$this->chartDatensatz('BatterieLadenEnergie', 'Batterie laden', '#8B5CF6', '~Electricity'), $leistung = [];
$this->chartDatensatz('BatterieEntladenEnergie', 'Batterie entladen', '#F59E0B', '~Electricity'), $energie = [];
]; if ($konfiguration['DiagrammPVModus'] !== 1) {
$this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510); $leistung[] = $this->chartDatensatz(
$this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520); 'PVLeistungArchiv',
'PV gesamt',
'#F4C542',
'~Power'
);
$energie[] = $this->chartDatensatz(
'PVEnergie',
'PV gesamt',
'#F4C542',
'~Electricity',
'bar'
);
}
if ($konfiguration['DiagrammPVModus'] !== 0) {
foreach ($pvAnlagen as $index => $anlage) {
$farbe = $this->diagrammFarbe($index);
if ($anlage['LeistungVariableID'] > 0) {
$leistung[] = $this->chartDatensatz(
$anlage['LeistungIdent'],
$anlage['Name'],
$farbe,
'~Power'
);
}
if ($anlage['EnergieVariableID'] > 0) {
$energie[] = $this->chartDatensatz(
$anlage['EnergieIdent'],
$anlage['Name'],
$farbe,
'~Electricity',
'bar'
);
}
}
}
$hausTitel = $verbraucher === [] ? 'Haus' : 'Haus ohne separat dargestellte Verbraucher';
$leistung[] = $this->chartDatensatz(
'DiagrammHausLeistung',
$hausTitel,
'#22A06B',
'~Power'
);
$leistung[] = $this->chartDatensatz(
'NetzleistungArchiv',
'Netz',
'#E95D5D',
'~Power'
);
$energie[] = $this->chartDatensatz(
'DiagrammHausEnergie',
$hausTitel,
'#22A06B',
'~Electricity',
'bar'
);
$energie[] = $this->chartDatensatz(
'NetzbezugEnergie',
'Netzbezug',
'#E95D5D',
'~Electricity',
'bar'
);
$energie[] = $this->chartDatensatz(
'EinspeisungEnergie',
'Netzeinspeisung',
'#2F80ED',
'~Electricity',
'bar'
);
if ($konfiguration['DiagrammBatterieModus'] !== 1) {
$leistung[] = $this->chartDatensatz(
'BatterieleistungArchiv',
'Batterie gesamt',
'#8B5CF6',
'~Power'
);
$energie[] = $this->chartDatensatz(
'BatterieLadenEnergie',
'Batterie laden gesamt',
'#8B5CF6',
'~Electricity',
'bar'
);
$energie[] = $this->chartDatensatz(
'BatterieEntladenEnergie',
'Batterie entladen gesamt',
'#F59E0B',
'~Electricity',
'bar'
);
}
if ($konfiguration['DiagrammBatterieModus'] !== 0) {
foreach ($batterien as $index => $batterie) {
$farbe = $this->diagrammFarbe($index + count($pvAnlagen));
if ($batterie['LeistungVariableID'] > 0) {
$leistung[] = $this->chartDatensatz(
$batterie['LeistungIdent'],
$batterie['Name'],
$farbe,
'~Power'
);
}
if ($batterie['LadeenergieVariableID'] > 0) {
$energie[] = $this->chartDatensatz(
$batterie['LadeenergieIdent'],
$batterie['Name'] . ' laden',
$farbe,
'~Electricity',
'bar'
);
}
if ($batterie['EntladeenergieVariableID'] > 0) {
$energie[] = $this->chartDatensatz(
$batterie['EntladeenergieIdent'],
$batterie['Name'] . ' entladen',
$farbe,
'~Electricity',
'bar'
);
}
}
}
foreach ($verbraucher as $index => $eintrag) {
$farbe = $this->diagrammFarbe(
$index + count($pvAnlagen) + count($batterien)
);
$leistung[] = $this->chartDatensatz(
'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'],
$eintrag['Name'],
$farbe,
'~Power'
);
$energie[] = $this->chartDatensatz(
'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'],
$eintrag['Name'],
$farbe,
'~Electricity',
'bar'
);
}
$leistungID = $this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
$energieID = $this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
$this->synchronisiereSDLDiagramm($leistungID, false);
$this->synchronisiereSDLDiagramm($energieID, true);
return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.'; return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.';
} }
/** @return list<array<string, mixed>> */
private function diagrammPVAnlagen(): array
{
$verwendet = [];
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenPVFlaechen') as $flaeche) {
$wechselrichterID = trim((string) ($flaeche['WechselrichterID'] ?? ''));
if ($wechselrichterID !== '') {
$verwendet[$wechselrichterID] = true;
}
}
$ergebnis = [];
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenWechselrichter') as $eintrag) {
$id = trim((string) ($eintrag['ID'] ?? ''));
if ($id === '' || ($verwendet !== [] && !isset($verwendet[$id]))) {
continue;
}
if ($verwendet === []
&& !in_array((string) ($eintrag['Typ'] ?? ''), ['pv', 'hybrid'], true)
) {
continue;
}
$suffix = substr(hash('sha256', 'pv:' . $id), 0, 12);
$leistungID = (int) ($eintrag['IstleistungVariableID'] ?? 0);
$energieID = (int) ($eintrag['ErzeugungsenergieVariableID'] ?? 0);
$ergebnis[] = [
'Name' => trim((string) ($eintrag['Name'] ?? '')) ?: $id,
'LeistungVariableID' => $this->istNumerischeVariable($leistungID)
? $leistungID
: 0,
'Leistungsfaktor' => (float) ($eintrag['Istleistungsfaktor'] ?? 1.0),
'EnergieVariableID' => $this->istNumerischeVariable($energieID)
? $energieID
: 0,
'Energiefaktor' => (float) ($eintrag['Erzeugungsenergiefaktor'] ?? 1.0),
'LeistungIdent' => 'DiagrammPVLeistung_' . $suffix,
'EnergieIdent' => 'DiagrammPVEnergie_' . $suffix,
];
}
return $ergebnis;
}
/** @return list<array<string, mixed>> */
private function diagrammBatterien(): array
{
$ergebnis = [];
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenBatterien') as $eintrag) {
$id = trim((string) ($eintrag['ID'] ?? ''));
if ($id === '') {
continue;
}
$suffix = substr(hash('sha256', 'batterie:' . $id), 0, 12);
$leistungID = (int) ($eintrag['LeistungVariableID'] ?? 0);
$ladenID = (int) ($eintrag['LadeenergieVariableID'] ?? 0);
$entladenID = (int) ($eintrag['EntladeenergieVariableID'] ?? 0);
$ergebnis[] = [
'Name' => trim((string) ($eintrag['Name'] ?? '')) ?: $id,
'LeistungVariableID' => $this->istNumerischeVariable($leistungID)
? $leistungID
: 0,
'Leistungsfaktor' => (float) ($eintrag['Leistungsfaktor'] ?? 1.0),
'LadeenergieVariableID' => $this->istNumerischeVariable($ladenID)
? $ladenID
: 0,
'EntladeenergieVariableID' => $this->istNumerischeVariable($entladenID)
? $entladenID
: 0,
'Energiefaktor' => (float) ($eintrag['Energiefaktor'] ?? 1.0),
'LeistungIdent' => 'DiagrammBatterieLeistung_' . $suffix,
'LadeenergieIdent' => 'DiagrammBatterieLaden_' . $suffix,
'EntladeenergieIdent' => 'DiagrammBatterieEntladen_' . $suffix,
];
}
return $ergebnis;
}
/**
* @param list<array<string, mixed>> $pvAnlagen
* @param list<array<string, mixed>> $batterien
* @param list<array<string, mixed>> $verbraucher
*/
private function registriereDiagrammVariablen(
array $pvAnlagen,
array $batterien,
array $verbraucher
): void {
$position = 800;
$this->diagrammMessvariable(
'DiagrammHausLeistung',
'Diagramm Hausleistung',
'~Power',
$position
);
$this->diagrammMessvariable(
'DiagrammHausEnergie',
'Diagramm Hausenergie',
'~Electricity',
$position + 1
);
$position += 10;
foreach ($pvAnlagen as $anlage) {
if ($anlage['LeistungVariableID'] > 0) {
$this->diagrammMessvariable(
$anlage['LeistungIdent'],
$anlage['Name'] . ' Leistung',
'~Power',
$position
);
}
if ($anlage['EnergieVariableID'] > 0) {
$this->diagrammMessvariable(
$anlage['EnergieIdent'],
$anlage['Name'] . ' Energie',
'~Electricity',
$position + 1
);
}
$position += 10;
}
foreach ($batterien as $batterie) {
if ($batterie['LeistungVariableID'] > 0) {
$this->diagrammMessvariable(
$batterie['LeistungIdent'],
$batterie['Name'] . ' Leistung',
'~Power',
$position
);
}
if ($batterie['LadeenergieVariableID'] > 0) {
$this->diagrammMessvariable(
$batterie['LadeenergieIdent'],
$batterie['Name'] . ' Ladeenergie',
'~Electricity',
$position + 1
);
}
if ($batterie['EntladeenergieVariableID'] > 0) {
$this->diagrammMessvariable(
$batterie['EntladeenergieIdent'],
$batterie['Name'] . ' Entladeenergie',
'~Electricity',
$position + 2
);
}
$position += 10;
}
foreach ($verbraucher as $eintrag) {
$this->diagrammMessvariable(
'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'],
$eintrag['Name'] . ' Leistung',
'~Power',
$position
);
$this->diagrammMessvariable(
'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'],
$eintrag['Name'] . ' Energie',
'~Electricity',
$position + 1
);
$position += 10;
}
}
private function diagrammMessvariable(
string $ident,
string $name,
string $profil,
int $position
): int {
$this->RegisterVariableFloat($ident, $name, $profil, $position);
$variableID = $this->GetIDForIdent($ident);
IPS_SetHidden($variableID, true);
return $variableID;
}
private function aktualisiereDiagrammMesswerte(bool $energieAktualisieren = false): void
{
if ($this->findeUnterobjekt('Leistungsdiagramm') === 0
&& $this->findeUnterobjekt('Energiediagramm') === 0
&& $this->findeUnterobjekt('DiagrammHausLeistung') === 0
) {
return;
}
try {
$leistungen = $this->leseMessleistungen();
$pvAnlagen = $this->diagrammPVAnlagen();
$batterien = $this->diagrammBatterien();
$verbraucher = $this->energieflussVerbraucher(
'EnergiediagrammKonfiguration'
);
} catch (Throwable $fehler) {
$this->protokolliere('Energiediagramme', $fehler->getMessage());
return;
}
$separateLeistung = 0.0;
$separateEnergieKWh = 0.0;
foreach ($verbraucher as $eintrag) {
$leistung = max(0.0, (float) $eintrag['Istleistung_W']);
$separateLeistung += $leistung;
$separateEnergieKWh += max(0.0, (float) $eintrag['Energie_Wh']) / 1000.0;
$this->setzeDiagrammMesswert(
'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'],
$leistung
);
if ($energieAktualisieren) {
$this->setzeDiagrammMesswert(
'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'],
max(0.0, (float) $eintrag['Energie_Wh']) / 1000.0
);
}
}
$this->setzeDiagrammMesswert(
'DiagrammHausLeistung',
max(0.0, $leistungen['Haus'] - $separateLeistung)
);
if ($energieAktualisieren) {
$this->setzeDiagrammMesswert(
'DiagrammHausEnergie',
max(
0.0,
(float) $this->GetValue('Hausenergie') - $separateEnergieKWh
)
);
}
foreach ($pvAnlagen as $anlage) {
if ($anlage['LeistungVariableID'] > 0) {
$this->setzeDiagrammMesswert(
$anlage['LeistungIdent'],
max(
0.0,
(float) GetValue($anlage['LeistungVariableID'])
* $anlage['Leistungsfaktor']
)
);
}
if ($energieAktualisieren && $anlage['EnergieVariableID'] > 0) {
$this->setzeDiagrammMesswert(
$anlage['EnergieIdent'],
max(
0.0,
(float) GetValue($anlage['EnergieVariableID'])
* $anlage['Energiefaktor']
)
);
}
}
foreach ($batterien as $batterie) {
if ($batterie['LeistungVariableID'] > 0) {
$this->setzeDiagrammMesswert(
$batterie['LeistungIdent'],
(float) GetValue($batterie['LeistungVariableID'])
* $batterie['Leistungsfaktor']
);
}
if ($energieAktualisieren && $batterie['LadeenergieVariableID'] > 0) {
$this->setzeDiagrammMesswert(
$batterie['LadeenergieIdent'],
max(
0.0,
(float) GetValue($batterie['LadeenergieVariableID'])
* $batterie['Energiefaktor']
)
);
}
if ($energieAktualisieren && $batterie['EntladeenergieVariableID'] > 0) {
$this->setzeDiagrammMesswert(
$batterie['EntladeenergieIdent'],
max(
0.0,
(float) GetValue($batterie['EntladeenergieVariableID'])
* $batterie['Energiefaktor']
)
);
}
}
}
private function setzeDiagrammMesswert(string $ident, float $wert): void
{
$variableID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if (is_int($variableID) && $variableID > 0 && IPS_VariableExists($variableID)) {
SetValue($variableID, $wert);
}
}
private function diagrammFarbe(int $index): string
{
$farben = [
'#A85D3C',
'#007C91',
'#6B5B95',
'#2E7D32',
'#C43E1C',
'#3D5A80',
'#8D6E63',
'#00897B',
];
return $farben[$index % count($farben)];
}
/** @param list<array<string, mixed>> $datensaetze */ /** @param list<array<string, mixed>> $datensaetze */
private function erstelleDiagramm( private function erstelleDiagramm(
string $ident, string $ident,
@@ -521,7 +1098,8 @@ trait ManagerEnergieTrait
int $position int $position
): int { ): int {
$medienID = $this->findeUnterobjekt($ident); $medienID = $this->findeUnterobjekt($ident);
if ($medienID === 0) { $neu = $medienID === 0;
if ($neu) {
$medienID = IPS_CreateMedia(4); $medienID = IPS_CreateMedia(4);
IPS_SetParent($medienID, $this->InstanceID); IPS_SetParent($medienID, $this->InstanceID);
IPS_SetIdent($medienID, $ident); IPS_SetIdent($medienID, $ident);
@@ -530,6 +1108,11 @@ trait ManagerEnergieTrait
if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) { if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) {
throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.'); throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.');
} }
if (!$neu) {
IPS_SetHidden($medienID, false);
return $medienID;
}
IPS_SetName($medienID, $name); IPS_SetName($medienID, $name);
IPS_SetPosition($medienID, $position); IPS_SetPosition($medienID, $position);
IPS_SetHidden($medienID, false); IPS_SetHidden($medienID, false);
@@ -546,16 +1129,17 @@ trait ManagerEnergieTrait
string $ident, string $ident,
string $titel, string $titel,
string $farbe, string $farbe,
string $profil string $profil,
string $typ = 'line'
): array { ): array {
return [ return [
'variableID' => $this->GetIDForIdent($ident), 'variableID' => $this->GetIDForIdent($ident),
'fillColor' => 'clear', 'fillColor' => $typ === 'bar' ? $farbe : 'clear',
'strokeColor' => $farbe, 'strokeColor' => $farbe,
'timeOffset' => 0, 'timeOffset' => 0,
'visible' => true, 'visible' => true,
'title' => $titel, 'title' => $titel,
'type' => 'line', 'type' => $typ,
'side' => 'left', 'side' => 'left',
'profile' => $profil, 'profile' => $profil,
]; ];
+95 -6
View File
@@ -208,6 +208,7 @@ final class ManagerRegler
'InstanzID' => $eintrag['InstanzID'], 'InstanzID' => $eintrag['InstanzID'],
'Prioritaet' => $daten[$prioritaetsfeld], 'Prioritaet' => $daten[$prioritaetsfeld],
'Energiegruppe' => (int) round((float) $energie / 2000.0), 'Energiegruppe' => (int) round((float) $energie / 2000.0),
'Energie_Wh' => (float) $energie,
'Leistungswerte_W' => $daten['Leistungswerte_W'], 'Leistungswerte_W' => $daten['Leistungswerte_W'],
'Festwert' => $festwert, 'Festwert' => $festwert,
'Festgehalten' => $festgehalten, 'Festgehalten' => $festgehalten,
@@ -254,6 +255,7 @@ final class ManagerRegler
} }
$rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme; $rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme;
$gruppen = [];
foreach ($steuerbar as $eintrag) { foreach ($steuerbar as $eintrag) {
if ($eintrag['Festwert'] || $eintrag['Festgehalten']) { if ($eintrag['Festwert'] || $eintrag['Festgehalten']) {
continue; continue;
@@ -262,13 +264,8 @@ final class ManagerRegler
$instanzID = $eintrag['InstanzID']; $instanzID = $eintrag['InstanzID'];
$ausgangswert = $sollwerte[$instanzID]; $ausgangswert = $sollwerte[$instanzID];
if ($positiveRichtung) { if ($positiveRichtung) {
if ($rest <= 0) { $gruppen[$eintrag['Prioritaet']][] = $eintrag;
continue; continue;
}
$neuerSollwert = self::groessterLeistungswertBis(
$eintrag['Leistungswerte_W'],
$ausgangswert + $rest
);
} else { } else {
if ($rest >= 0) { if ($rest >= 0) {
continue; continue;
@@ -281,6 +278,9 @@ final class ManagerRegler
$rest -= $neuerSollwert - $ausgangswert; $rest -= $neuerSollwert - $ausgangswert;
$sollwerte[$instanzID] = $neuerSollwert; $sollwerte[$instanzID] = $neuerSollwert;
} }
foreach ($gruppen as $gruppe) {
self::verteilePositiveGruppe($gruppe, $sollwerte, $rest);
}
$zugeteilt = $nichtSteuerbar; $zugeteilt = $nichtSteuerbar;
foreach ($steuerbar as $eintrag) { foreach ($steuerbar as $eintrag) {
@@ -298,6 +298,95 @@ final class ManagerRegler
]; ];
} }
/**
* @param list<array<string, mixed>> $gruppe Verbraucher derselben Prioritaet
* @param array<int, int> $sollwerte
*/
private static function verteilePositiveGruppe(array $gruppe, array &$sollwerte, int &$rest): void
{
while ($rest > 0) {
$kandidaten = self::naechsteLeistungsschritte($gruppe, $sollwerte, $rest);
if ($kandidaten === []) {
return;
}
// Ganze Leistungsniveaus gemeinsam vergeben, ohne Bereiche Watt fuer Watt zu durchlaufen.
$unten = $kandidaten[0]['NaechsteLeistung_W'] - 1;
$oben = $unten;
foreach ($kandidaten as $eintrag) {
$oben = max($oben, $sollwerte[$eintrag['InstanzID']] + $rest);
}
while ($unten < $oben) {
$mitte = $unten + intdiv($oben - $unten + 1, 2);
$bedarf = 0;
foreach ($kandidaten as $eintrag) {
$bisher = $sollwerte[$eintrag['InstanzID']];
$bedarf += max(0, self::groessterLeistungswertBis(
$eintrag['Leistungswerte_W'],
$mitte
) - $bisher);
if ($bedarf > $rest) {
break;
}
}
if ($bedarf <= $rest) {
$unten = $mitte;
} else {
$oben = $mitte - 1;
}
}
foreach ($kandidaten as $eintrag) {
$id = $eintrag['InstanzID'];
$neu = max($sollwerte[$id], self::groessterLeistungswertBis(
$eintrag['Leistungswerte_W'],
$unten
));
$rest -= $neu - $sollwerte[$id];
$sollwerte[$id] = $neu;
}
// Reicht es nicht fuer das ganze naechste Niveau, entscheidet Leistung, Energie, ID.
$kandidaten = self::naechsteLeistungsschritte($gruppe, $sollwerte, $rest);
if ($kandidaten === []) {
return;
}
$erster = $kandidaten[0];
$id = $erster['InstanzID'];
$rest -= $erster['NaechsteLeistung_W'] - $sollwerte[$id];
$sollwerte[$id] = $erster['NaechsteLeistung_W'];
}
}
/**
* @param list<array<string, mixed>> $gruppe
* @param array<int, int> $sollwerte
* @return list<array<string, mixed>>
*/
private static function naechsteLeistungsschritte(array $gruppe, array $sollwerte, int $rest): array
{
$kandidaten = [];
foreach ($gruppe as $eintrag) {
$bisher = $sollwerte[$eintrag['InstanzID']];
foreach ($eintrag['Leistungswerte_W'] as $angebot) {
$naechste = is_int($angebot) ? $angebot : max($angebot['Von_W'], $bisher + 1);
if ($naechste <= $bisher || (!is_int($angebot) && $naechste > $angebot['Bis_W'])) {
continue;
}
if ($naechste - $bisher <= $rest) {
$eintrag['NaechsteLeistung_W'] = $naechste;
$kandidaten[] = $eintrag;
}
break;
}
}
usort($kandidaten, static function (array $links, array $rechts): int {
return [$links['NaechsteLeistung_W'], $links['Energie_Wh'], $links['InstanzID']]
<=> [$rechts['NaechsteLeistung_W'], $rechts['Energie_Wh'], $rechts['InstanzID']];
});
return $kandidaten;
}
/** /**
* @param array<string, mixed> $daten * @param array<string, mixed> $daten
*/ */
+211
View File
@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
trait ManagerSDLTrait
{
private function pruefeSDLKonfiguration(): void
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return;
}
$leistungID = $this->ReadPropertyInteger('SDLLeistungVariableID');
if (!$this->istNumerischeVariable($leistungID)) {
throw new InvalidArgumentException('SDL benoetigt eine numerische Istleistungsvariable.');
}
$this->anlagenMessfaktor($this->ReadPropertyFloat('SDLLeistungsfaktor'), 'SDL');
$socID = $this->ReadPropertyInteger('SDLSOCVariableID');
if ($socID !== 0 && (!$this->istNumerischeVariable($socID) || $socID === $leistungID)) {
throw new InvalidArgumentException('SDL-SOC muss eine separate numerische Variable sein.');
}
$andereQuellen = array_merge($this->pvMessquellen(), $this->batterieMessquellen());
$andereQuellen[] = ['VariableID' => $this->ReadPropertyInteger('NetzleistungVariableID')];
foreach ($andereQuellen as $quelle) {
if ($leistungID === $quelle['VariableID']) {
throw new InvalidArgumentException(
'SDL-Leistung darf nicht zugleich als PV-, Netz- oder Batterieleistung bilanziert werden.'
);
}
}
}
private function synchronisiereSDLMesskonfiguration(): void
{
$neu = json_encode([
$this->ReadPropertyBoolean('SDLAktiv'),
$this->ReadPropertyInteger('SDLLeistungVariableID'),
$this->ReadPropertyFloat('SDLLeistungsfaktor'),
], JSON_THROW_ON_ERROR);
$alt = $this->ReadAttributeString('SDLMesskonfiguration');
if ($neu !== $alt && ($alt !== '' || $this->ReadPropertyBoolean('SDLAktiv'))) {
// Bereits erfasste Energien bleiben erhalten; keine Rueckrechnung mit neuen Quellen.
$this->WriteAttributeInteger('MessungLetzterZeitstempel', 0);
$this->WriteAttributeString('MessungZaehlerstaende', '{}');
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', 0);
}
$this->WriteAttributeString('SDLMesskonfiguration', $neu);
}
private function aktuelleSDLLeistung(): float
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return 0.0;
}
$variableID = $this->ReadPropertyInteger('SDLLeistungVariableID');
if (!$this->istNumerischeVariable($variableID)) {
throw new RuntimeException('Die SDL-Istleistung ist nicht verfuegbar.');
}
$wert = GetValue($variableID);
if ((!is_int($wert) && !is_float($wert)) || !is_finite((float) $wert)) {
throw new RuntimeException('Die SDL-Istleistung ist keine endliche Zahl.');
}
$leistung = (float) $wert * $this->ReadPropertyFloat('SDLLeistungsfaktor');
if (!is_finite($leistung)) {
throw new RuntimeException('Die skalierte SDL-Istleistung ist nicht endlich.');
}
if ($this->ReadPropertyInteger('SDLSOCVariableID') === 0 || $this->aktuelleSDLSOC() !== null) {
$this->setzeDiagnosewert('SDLStatus', 'Aktiv');
}
return $leistung;
}
private function aktuelleSDLSOC(): ?float
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return null;
}
$variableID = $this->ReadPropertyInteger('SDLSOCVariableID');
if ($variableID === 0) {
return null;
}
if ($this->istNumerischeVariable($variableID)) {
$wert = (float) GetValue($variableID);
if (is_finite($wert) && $wert >= 0.0 && $wert <= 100.0) {
return $wert;
}
}
$this->setzeDiagnosewert('SDLStatus', 'Fehler: SDL-SOC muss zwischen 0 und 100 Prozent liegen.');
return null;
}
private function sdlAnzeigen(string $property): bool
{
return $this->ReadPropertyBoolean('SDLAktiv') && $this->ReadPropertyBoolean($property);
}
private function aktualisiereSDLStatus(): void
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
$this->setzeDiagnosewert('SDLStatus', 'NichtVerwendet');
return;
}
try {
$this->aktuelleSDLLeistung();
$this->aktuelleSDLSOC();
} catch (Throwable $fehler) {
$this->setzeDiagnosewert('SDLStatus', 'Fehler: ' . $fehler->getMessage());
}
}
private function synchronisiereSDLEnergiefluss(int $instanzID, bool $profilAktualisieren = false): void
{
$anzeigen = $this->sdlAnzeigen('SDLEnergieflussAnzeigen');
$variableID = $this->findeUnterobjekt('EnergieflussSDL');
if (!$anzeigen && $variableID === 0) {
return;
}
if ($anzeigen && ($variableID === 0 || $profilAktualisieren)) {
$variableID = $this->energieflussLeistungsvariable(
'EnergieflussSDL', 'Energiefluss SDL', $this->energieflussLeistungsprofil(), 635
);
}
$alt = json_decode(IPS_GetProperty($instanzID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($alt) || !array_is_list($alt)) {
throw new RuntimeException('Die bestehende Energiefluss-Konfiguration ist keine Liste.');
}
$neu = [];
$gefunden = false;
$socID = $this->aktuelleSDLSOC() !== null
? $this->GetIDForIdent('SDLSOCArchiv') : 1;
foreach ($alt as $knoten) {
if ((int) ($knoten['VariableID'] ?? 0) !== $variableID) {
$neu[] = $knoten;
} elseif ($anzeigen && !$gefunden) {
$knoten['InfoVariableID'] = $socID;
$neu[] = $knoten;
$gefunden = true;
}
}
if ($anzeigen && !$gefunden) {
$neu[] = $this->energieflussKnoten($variableID, 'SDL / Regelenergie', 'storage', false, $socID);
}
if ($neu !== $alt) {
IPS_SetProperty($instanzID, 'Config', json_encode($neu, JSON_THROW_ON_ERROR));
IPS_ApplyChanges($instanzID);
}
}
private function synchronisiereSDLDiagramm(int $medienID, bool $energie): void
{
$idents = $energie
? ['SDLLadenEnergie', 'SDLEntladenEnergie', 'SDLSOCArchiv']
: ['SDLLeistungArchiv', 'SDLSOCArchiv'];
$verwaltet = array_map(fn (string $ident): int => $this->GetIDForIdent($ident), $idents);
$gewuenscht = [];
if ($this->sdlAnzeigen('SDLDiagrammeAnzeigen')) {
if ($energie) {
$gewuenscht[] = $this->chartDatensatz(
'SDLLadenEnergie', 'SDL laden', '#007C91', '~Electricity', 'bar'
);
$gewuenscht[] = $this->chartDatensatz(
'SDLEntladenEnergie', 'SDL entladen', '#C43E1C', '~Electricity', 'bar'
);
} else {
$gewuenscht[] = $this->chartDatensatz(
'SDLLeistungArchiv', 'SDL Istleistung', '#007C91', '~Power'
);
}
if ($this->ReadPropertyInteger('SDLSOCVariableID') > 0) {
$soc = $this->chartDatensatz('SDLSOCArchiv', 'SDL SOC', '#C43E1C', '~Intensity.100');
$soc['side'] = 'right';
$gewuenscht[] = $soc;
}
}
$inhalt = json_decode((string) base64_decode(IPS_GetMediaContent($medienID), true), true);
if (!is_array($inhalt) || !isset($inhalt['datasets']) || !is_array($inhalt['datasets'])) {
if (!$this->sdlAnzeigen('SDLDiagrammeAnzeigen')) {
return;
}
throw new RuntimeException('Das bestehende Diagramm hat keine gueltigen Datensaetze.');
}
$alt = $inhalt['datasets'];
$neu = [];
foreach ($alt as $datensatz) {
$id = (int) ($datensatz['variableID'] ?? 0);
if (!in_array($id, $verwaltet, true)) {
$neu[] = $datensatz;
continue;
}
foreach ($gewuenscht as $index => $ziel) {
if ($id === $ziel['variableID']) {
$neu[] = $datensatz;
unset($gewuenscht[$index]);
break;
}
}
}
$neu = array_merge($neu, array_values($gewuenscht));
if ($neu !== $alt) {
$inhalt['datasets'] = $neu;
IPS_SetMediaContent($medienID, base64_encode(json_encode($inhalt, JSON_THROW_ON_ERROR)));
}
}
}
+19 -1
View File
@@ -5,13 +5,31 @@ declare(strict_types=1);
namespace Belevo\EnelixEMS; namespace Belevo\EnelixEMS;
/** /**
* Registriert die gemeinsamen Properties und Variablen aller EMS-Verbraucher. * Gemeinsame Registrierung und Sollwert-Bereinigung aller EMS-Verbraucher.
* *
* Das Trait ist noch keine vollstaendige Steuerungsimplementierung. Die * Das Trait ist noch keine vollstaendige Steuerungsimplementierung. Die
* konkreten Module ergaenzen Geraeteanbindung, Leistungsangebot und Zustand. * konkreten Module ergaenzen Geraeteanbindung, Leistungsangebot und Zustand.
*/ */
trait VerbraucherBasisTrait trait VerbraucherBasisTrait
{ {
protected function verwerfeSollwert(): void
{
// Lokale Ersatzleistungen nicht bei jedem weiteren Timeout kurz auf 0 setzen.
if (!(bool) $this->leseZustand('SollwertGueltig')) {
return;
}
$this->setzeZustand('SollwertGueltig', false);
$this->setzeZustand('Sollleistung', 0);
}
protected function bereinigeUngueltigenSollwert(): void
{
// Auch vor dem Update gespeicherte, bereits ungueltige Vorgaben bereinigen.
if (!(bool) $this->leseZustand('SollwertGueltig')) {
$this->setzeZustand('Sollleistung', 0);
}
}
protected function registriereVerbraucherBasis(): void protected function registriereVerbraucherBasis(): void
{ {
$this->RegisterPropertyInteger('PrioritaetPV', 0); $this->RegisterPropertyInteger('PrioritaetPV', 0);
+80 -1
View File
@@ -24,6 +24,8 @@ final class EnergieMessungTest extends TestCase
self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001); self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001);
self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001); self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001);
self::assertSame(0.0, $werte['BatterieEntladenEnergie']); self::assertSame(0.0, $werte['BatterieEntladenEnergie']);
self::assertSame(0.0, $werte['SDLLadenEnergie']);
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
} }
public function testTrenntBezugUndBatterieentladung(): void public function testTrenntBezugUndBatterieentladung(): void
@@ -50,23 +52,100 @@ final class EnergieMessungTest extends TestCase
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0)); self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
} }
public function testBeruecksichtigtSDLVorDerBegrenzungDerHausleistung(): void
{
self::assertSame(1700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 1000.0));
self::assertSame(3700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, -1000.0));
self::assertSame(1000.0, EnergieMessung::hausleistung(0.0, -1000.0, 0.0, -2000.0));
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, 0.0, 0.0, 1000.0));
self::assertSame(
EnergieMessung::hausleistung(4200.0, -800.0, 700.0),
EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 0.0)
);
}
public function testBerechnetHausenergieAusDenZaehlerdeltas(): void public function testBerechnetHausenergieAusDenZaehlerdeltas(): void
{ {
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0)); 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)); self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 5.0, 0.0, 0.0));
} }
public function testBereinigtHausenergieUmSDLLadungUndEntladung(): void
{
self::assertSame(5.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 0.0));
self::assertSame(11.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 3.0));
self::assertSame(7.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 2.0));
self::assertSame(1.0, EnergieMessung::hausenergie(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0));
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0));
self::assertSame(
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0),
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 0.0)
);
}
public function testIntegriertSDLGetrenntVonBatterieUndHaus(): void
{
$leistungen = [
'PV' => 7200.0,
'Haus' => 3600.0,
'Netz' => -1800.0,
'Batterie' => 900.0,
'SDL' => 1800.0,
];
$laden = EnergieMessung::energieDeltas($leistungen, 300);
self::assertEqualsWithDelta(0.15, $laden['SDLLadenEnergie'], 0.000001);
self::assertSame(0.0, $laden['SDLEntladenEnergie']);
self::assertEqualsWithDelta(0.075, $laden['BatterieLadenEnergie'], 0.000001);
self::assertEqualsWithDelta(0.3, $laden['Hausenergie'], 0.000001);
$leistungen['SDL'] = -1800.0;
$entladen = EnergieMessung::energieDeltas($leistungen, 300);
self::assertSame(0.0, $entladen['SDLLadenEnergie']);
self::assertEqualsWithDelta(0.15, $entladen['SDLEntladenEnergie'], 0.000001);
self::assertSame($laden['Hausenergie'], $entladen['Hausenergie']);
self::assertSame($laden['BatterieLadenEnergie'], $entladen['BatterieLadenEnergie']);
$leistungen['SDL'] = 0.0;
$mitNull = EnergieMessung::energieDeltas($leistungen, 300);
unset($leistungen['SDL']);
self::assertSame($mitNull, EnergieMessung::energieDeltas($leistungen, 300));
}
public function testIgnoriertNichtEndlicheSDLWerte(): void
{
foreach ([INF, -INF, NAN] as $ungueltig) {
self::assertSame(2700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, $ungueltig));
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, $ungueltig, $ungueltig));
$werte = EnergieMessung::energieDeltas([
'PV' => 0.0,
'Haus' => 1000.0,
'Netz' => 1000.0,
'Batterie' => 0.0,
'SDL' => $ungueltig,
], 3600);
self::assertSame(0.0, $werte['SDLLadenEnergie']);
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
self::assertSame(1.0, $werte['Hausenergie']);
}
}
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
{ {
foreach ([-1, 0] as $dauer) {
foreach ([-1000.0, 1000.0] as $sdl) {
$werte = EnergieMessung::energieDeltas([ $werte = EnergieMessung::energieDeltas([
'PV' => INF, 'PV' => INF,
'Haus' => NAN, 'Haus' => NAN,
'Netz' => 1000.0, 'Netz' => 1000.0,
'Batterie' => -1000.0, 'Batterie' => -1000.0,
], -1); 'SDL' => $sdl,
], $dauer);
foreach ($werte as $wert) { foreach ($werte as $wert) {
self::assertSame(0.0, $wert); self::assertSame(0.0, $wert);
} }
} }
} }
}
}
+27 -2
View File
@@ -13,7 +13,7 @@ final class LadestationAdapterTest extends TestCase
/** /**
* @dataProvider statusFaelle * @dataProvider statusFaelle
* @param array<string, string> $konfiguration * @param array<string, string> $konfiguration
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $erwartet * @param array{Rohstatus: int, Istleistung_W: float, Sollstrom_A?: int, Maximalstrom_A?: int, Iststrom_A?: float, Phasenzahl?: int} $erwartet
*/ */
public function testStatusaufrufUndDekodierungFuerAlleGeraete( public function testStatusaufrufUndDekodierungFuerAlleGeraete(
int $geraetetyp, int $geraetetyp,
@@ -52,6 +52,8 @@ final class LadestationAdapterTest extends TestCase
[ [
'Rohstatus' => 2, 'Rohstatus' => 2,
'Istleistung_W' => 11000.0, 'Istleistung_W' => 11000.0,
'Sollstrom_A' => 16,
'Maximalstrom_A' => 16,
'Iststrom_A' => 16.0, 'Iststrom_A' => 16.0,
'Phasenzahl' => 3, 'Phasenzahl' => 3,
], ],
@@ -64,6 +66,8 @@ final class LadestationAdapterTest extends TestCase
[ [
'Rohstatus' => 2, 'Rohstatus' => 2,
'Istleistung_W' => 2300.0, 'Istleistung_W' => 2300.0,
'Sollstrom_A' => 16,
'Maximalstrom_A' => 16,
'Iststrom_A' => 9.8, 'Iststrom_A' => 9.8,
'Phasenzahl' => 1, 'Phasenzahl' => 1,
], ],
@@ -81,6 +85,20 @@ final class LadestationAdapterTest extends TestCase
]; ];
} }
public function testGoEAusFreigabeWirdAlsNullSollstromDekodiert(): void
{
$daten = json_decode($this->goEAntwort(2, 0, [0, 0, 0]), true, 512, JSON_THROW_ON_ERROR);
$daten['alw'] = false;
$daten['frc'] = 1;
$status = LadestationAdapter::dekodiereStatus(
LadestationAdapter::GO_E_GEMINI,
json_encode($daten, JSON_THROW_ON_ERROR)
);
self::assertSame(0, $status['Sollstrom_A']);
}
/** /**
* @dataProvider steuerFaelle * @dataProvider steuerFaelle
* @param array<string, string> $konfiguration * @param array<string, string> $konfiguration
@@ -180,6 +198,13 @@ final class LadestationAdapterTest extends TestCase
} }
$nrg[11] = $leistung; $nrg[11] = $leistung;
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR); return json_encode([
'car' => $status,
'nrg' => $nrg,
'amp' => 16,
'ama' => 16,
'alw' => true,
'frc' => 2,
], JSON_THROW_ON_ERROR);
} }
} }
@@ -104,4 +104,32 @@ final class LadestationStandAloneModulstrukturTest extends TestCase
self::assertStringContainsString('PHASENERKENNUNGSSTROM = 6', $modul); self::assertStringContainsString('PHASENERKENNUNGSSTROM = 6', $modul);
self::assertStringContainsString('UnregisterVariable($ident)', $modul); self::assertStringContainsString('UnregisterVariable($ident)', $modul);
} }
public function testMeldezyklusSendetDenBereitsGelesenenZustandOhneZusaetzlichenHttpAbruf(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
self::assertSame(1, preg_match(
"/case 'Melden':(?<block>.*?)case 'ManagerdatenEmpfangen':/s",
$modul,
$treffer
));
self::assertStringContainsString('$this->sendeVerbraucherdaten();', $treffer['block']);
self::assertStringNotContainsString('$this->aktualisiere(', $treffer['block']);
}
public function testVomGeraetBestaetigterSollstromWirdFuerWiederholungVerwendet(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
self::assertStringContainsString("\$rohstatus['Sollstrom_A']", $modul);
self::assertStringContainsString(
"WriteAttributeInteger('LetzterGesetzterStrom', \$geraeteSollstrom)",
$modul
);
self::assertStringContainsString("RegisterAttributeInteger('GeraeteMaximalstrom', 32)", $modul);
self::assertStringContainsString(
"WriteAttributeInteger('GeraeteMaximalstrom', (int) \$rohstatus['Maximalstrom_A'])",
$modul
);
}
} }
+16 -4
View File
@@ -17,6 +17,11 @@ final class ManagerEnergieModulstrukturTest extends TestCase
foreach ([ foreach ([
"RegisterPropertyBoolean('EnergyPieAnzeigen', false)", "RegisterPropertyBoolean('EnergyPieAnzeigen', false)",
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)", "RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
"RegisterPropertyInteger('DiagrammPVModus', 0)",
"RegisterPropertyInteger('DiagrammBatterieModus', 0)",
"RegisterPropertyBoolean('LadestationenSeparatAnzeigen', true)",
"RegisterPropertyBoolean('VerbraucherSeparatAnzeigen', true)",
"RegisterAttributeBoolean('EnergieflussNeuErzeugen', false)",
"RegisterPropertyBoolean('FunFactsAnzeigen', false)", "RegisterPropertyBoolean('FunFactsAnzeigen', false)",
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)", "RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
"RegisterPropertyInteger('EnergieflussLeistungseinheit', 0)", "RegisterPropertyInteger('EnergieflussLeistungseinheit', 0)",
@@ -38,6 +43,10 @@ final class ManagerEnergieModulstrukturTest extends TestCase
'ENERGY_PIE_MODULE_ID', 'ENERGY_PIE_MODULE_ID',
'ENERGY_FLOW_MODULE_ID', 'ENERGY_FLOW_MODULE_ID',
'energieflussKnoten', 'energieflussKnoten',
"'type' => \$typ",
"'bar'",
'EnergiediagrammKonfiguration',
'EnergieflussKonfiguration',
'IPS_DeleteInstance', 'IPS_DeleteInstance',
'IPS_DeleteMedia', 'IPS_DeleteMedia',
] as $erwartet) { ] as $erwartet) {
@@ -45,11 +54,14 @@ final class ManagerEnergieModulstrukturTest extends TestCase
} }
foreach ([ foreach ([
'Energieaufzeichnung', 'Energieaufzeichnung',
'FormEnergyPieAnzeigen',
'FormEnergiediagrammeAnzeigen',
'FormFunFactsAnzeigen',
'FormEnergieflussAnzeigen',
'EnergieflussLeistungseinheit', 'EnergieflussLeistungseinheit',
'DiagrammPVModus',
'DiagrammBatterieModus',
'LadestationenSeparatAnzeigen',
'VerbraucherSeparatAnzeigen',
'Bestehende Energy Pies, Diagramme und Energiefluesse bleiben bei Apply unveraendert',
'Wechselrichter / AC-Wandler',
'keine doppelte Batteriekonfiguration',
'MesswerteAnzeigen', 'MesswerteAnzeigen',
'IstleistungVariableID', 'IstleistungVariableID',
'LeistungVariableID', 'LeistungVariableID',
+27
View File
@@ -51,6 +51,12 @@ final class ManagerModulstrukturTest extends TestCase
'EinspeisebegrenzungAktiv', 'EinspeisebegrenzungAktiv',
'Einspeisegrenze', 'Einspeisegrenze',
'PrognoseAktiv', 'PrognoseAktiv',
'SDLAktiv',
'SDLLeistungVariableID',
'SDLLeistungsfaktor',
'SDLSOCVariableID',
'SDLEnergieflussAnzeigen',
'SDLDiagrammeAnzeigen',
'PrognoseLizenzInformation', 'PrognoseLizenzInformation',
'NetzfahrplanAktiv', 'NetzfahrplanAktiv',
'NetzbezugEnergieVariableID', 'NetzbezugEnergieVariableID',
@@ -380,4 +386,25 @@ final class ManagerModulstrukturTest extends TestCase
self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt); self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt);
self::assertStringContainsString("'ManagerdatenEmpfangen'", $inhalt); self::assertStringContainsString("'ManagerdatenEmpfangen'", $inhalt);
} }
public function testVerbrauchermeldungPlantRegelungOhneSynchronenRueckruf(): void
{
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
self::assertStringContainsString(
"RegisterTimer(\n 'RegelungVerzoegert'",
$inhalt
);
self::assertStringContainsString("case 'RegelnVerzoegert':", $inhalt);
self::assertSame(1, preg_match(
'/public function VerbraucherdatenEmpfangen\(array \$daten\): void(?<block>.*?)'
. 'private function aktualisiereVerbraucherEnergie/s',
$inhalt,
$treffer
));
self::assertStringContainsString(
"SetTimerInterval('RegelungVerzoegert', 100)",
$treffer['block']
);
self::assertStringNotContainsString('$this->regeln(false);', $treffer['block']);
}
} }
+170 -3
View File
@@ -166,7 +166,7 @@ final class ManagerReglerTest extends TestCase
self::assertSame(500, $ergebnis['Restabweichung_W']); self::assertSame(500, $ergebnis['Restabweichung_W']);
} }
public function testGleichePrioritaetBevorzugtWieEnelix1DenVerbraucherMitWenigerEnergie(): void public function testGleicheNaechsteLeistungBevorzugtWenigerEnergie(): void
{ {
$ergebnis = ManagerRegler::verteile( $ergebnis = ManagerRegler::verteile(
0.0, 0.0,
@@ -183,7 +183,7 @@ final class ManagerReglerTest extends TestCase
self::assertSame(0, $ergebnis['Restabweichung_W']); self::assertSame(0, $ergebnis['Restabweichung_W']);
} }
public function testEnergievergleichVerwendetWieEnelix1ZweiKilowattstundenGruppen(): void public function testEnergiegleichstandVergleichtExakteWhStattZweiKilowattstundenGruppen(): void
{ {
$ergebnis = ManagerRegler::verteile( $ergebnis = ManagerRegler::verteile(
0.0, 0.0,
@@ -196,7 +196,174 @@ final class ManagerReglerTest extends TestCase
[10 => 999.0, 20 => 0.0] [10 => 999.0, 20 => 0.0]
); );
self::assertSame([10 => 3000, 20 => 0], $ergebnis['Sollwerte']); self::assertSame([10 => 0, 20 => 3000], $ergebnis['Sollwerte']);
}
/** @dataProvider beispielBudgets */
public function testBestaetigteStufenfolge(int $budget, int $a, int $b): void
{
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $feld) {
$ergebnis = ManagerRegler::verteile(0.0, (float) $budget, [
$this->verbraucher(20, 2, [0, 100, 110, 500, 780, 1500], 0),
$this->verbraucher(10, 2, [0, 100, 200, 400, 800], 0),
], $feld, [10 => 10000.0, 20 => 12000.0]);
self::assertSame([10 => $a, 20 => $b], $ergebnis['Sollwerte']);
self::assertSame($budget - $a - $b, $ergebnis['Restabweichung_W']);
}
}
public function beispielBudgets(): array
{
return [
[0, 0, 0], [99, 0, 0], [100, 100, 0], [199, 100, 0],
[200, 100, 100], [209, 100, 100], [210, 100, 110],
[309, 100, 110], [310, 200, 110], [509, 200, 110],
[510, 400, 110], [899, 400, 110], [900, 400, 500],
[1179, 400, 500], [1180, 400, 780], [1579, 400, 780],
[1580, 800, 780], [2299, 800, 780], [2300, 800, 1500],
[3000, 800, 1500],
];
}
public function testLeistungsniveauGehtVorEnergieUndSchrittdifferenz(): void
{
$ergebnis = ManagerRegler::verteile(0.0, 220.0, [
$this->verbraucher(10, 0, [0, 100, 200], 0),
$this->verbraucher(20, 0, [0, 110, 120], 0),
], 'PrioritaetPV', [10 => 0.0, 20 => 50000.0]);
self::assertSame([10 => 100, 20 => 120], $ergebnis['Sollwerte']);
}
public function testPicoUndGoETeilenBudgetTrotzGrossemEnergieunterschied(): void
{
$ergebnis = ManagerRegler::verteile(0.0, 12312.0, [
$this->verbraucher(31986, 2, array_merge([0], range(4104, 12312, 684)), 0),
$this->verbraucher(43878, 2, array_merge([0], range(4104, 10944, 684)), 0),
], 'PrioritaetPV', [31986 => 41192.78, 43878 => 128082.87]);
self::assertSame([31986 => 6156, 43878 => 6156], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testNegativeDefizitverteilungBehaeltBisherigeEnergiegruppen(): void
{
$ergebnis = ManagerRegler::verteile(300.0, 0.0, [
$this->verbraucher(10, 0, [['Von_W' => -300, 'Bis_W' => 300]], 0),
$this->verbraucher(20, 0, [['Von_W' => -300, 'Bis_W' => 300]], 0),
], 'PrioritaetPeak', [10 => 999.0, 20 => 0.0]);
self::assertSame([10 => -300, 20 => 0], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testGleicheLeistungUndEnergieEntscheidetStabilNachInstanzID(): void
{
$ergebnis = ManagerRegler::verteile(0.0, 100.0, [
$this->verbraucher(20, 0, [0, 100], 0),
$this->verbraucher(10, 0, [0, 100], 0),
], 'PrioritaetPV');
self::assertSame([10 => 100, 20 => 0], $ergebnis['Sollwerte']);
}
public function testNichtPassenderSchrittBlockiertKeineAndereErreichbareStufe(): void
{
$ergebnis = ManagerRegler::verteile(0.0, 210.0, [
$this->verbraucher(10, 0, [0, 100, 200], 0),
$this->verbraucher(20, 0, [0, 150], 0),
], 'PrioritaetPV');
self::assertSame([10 => 200, 20 => 0], $ergebnis['Sollwerte']);
self::assertSame(10, $ergebnis['Restabweichung_W']);
}
public function testPeakVerwendetPeakPrioritaetVorLeistungUndEnergie(): void
{
$a = $this->verbraucher(10, 0, [0, 100, 200], 0);
$b = $this->verbraucher(20, 9, [0, 300], 0);
$a['Daten']['PrioritaetPeak'] = 9;
$b['Daten']['PrioritaetPeak'] = 0;
$ergebnis = ManagerRegler::verteile(0.0, 300.0, [$a, $b], 'PrioritaetPeak', [20 => 50000.0]);
self::assertSame([20 => 300, 10 => 0], $ergebnis['Sollwerte']);
}
public function testGrosseBereicheWerdenGemeinsamOhneWattweiseSchleifeVerteilt(): void
{
$ergebnis = ManagerRegler::verteile(0.0, 1000000001.0, [
$this->verbraucher(10, 0, [['Von_W' => 0, 'Bis_W' => 1000000000]], 0),
$this->verbraucher(20, 0, [['Von_W' => 0, 'Bis_W' => 1000000000]], 0),
], 'PrioritaetPV', [10 => 999.0, 20 => 0.0]);
self::assertSame([10 => 500000000, 20 => 500000001], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testBereicheUndEinzelwerteGegenWattweiseReferenz(): void
{
$angebote = [
[0, ['Von_W' => 3, 'Bis_W' => 9], 15],
[0, 5, ['Von_W' => 7, 'Bis_W' => 12], 18],
[0, 4, 8, ['Von_W' => 14, 'Bis_W' => 20]],
];
$energien = [10 => 10000.5, 20 => 10000.0, 30 => 10000.0];
$verbraucher = [];
$diskret = [];
foreach ($angebote as $index => $angebot) {
$id = ($index + 1) * 10;
$verbraucher[] = $this->verbraucher($id, 2, $angebot, 0);
$diskret[$id] = [];
foreach ($angebot as $wert) {
foreach (is_int($wert) ? [$wert] : range($wert['Von_W'], $wert['Bis_W']) as $watt) {
$diskret[$id][] = $watt;
}
}
}
for ($budget = 0; $budget <= 60; $budget++) {
$erwartet = [10 => 0, 20 => 0, 30 => 0];
$rest = $budget;
while (true) {
$schritte = [];
foreach ($diskret as $id => $werte) {
foreach ($werte as $wert) {
if ($wert > $erwartet[$id]) {
if ($wert - $erwartet[$id] <= $rest) {
$schritte[] = [$wert, $energien[$id], $id];
}
break;
}
}
}
if ($schritte === []) {
break;
}
sort($schritte);
[$wert, , $id] = $schritte[0];
$rest -= $wert - $erwartet[$id];
$erwartet[$id] = $wert;
}
$ergebnis = ManagerRegler::verteile(0.0, (float) $budget, array_reverse($verbraucher),
'PrioritaetPV', $energien);
$ist = $ergebnis['Sollwerte'];
ksort($ist);
self::assertSame($erwartet, $ist, 'Budget: ' . $budget);
self::assertSame($rest, $ergebnis['Restabweichung_W']);
}
}
public function testNichtVerfuegbareLastWirdReserviertUndNichtNeuZugeteilt(): void
{
$a = $this->verbraucher(10, 0, [0, 100, 200], 100);
$a['Daten']['Verfuegbar'] = false;
$ergebnis = ManagerRegler::verteile(100.0, 200.0, [
$a,
$this->verbraucher(20, 0, [0, 100, 200], 0),
], 'PrioritaetPV');
self::assertSame(100, $ergebnis['NichtSteuerbar_W']);
self::assertSame([20 => 100], $ergebnis['Sollwerte']);
} }
public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void
+425
View File
@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use InvalidArgumentException;
use Manager;
use ManagerSDLRuntime;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use RuntimeException;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
final class ManagerSDLTest extends TestCase
{
private Manager $manager;
protected function setUp(): void
{
require_once __DIR__ . '/Support/ManagerSDLRuntime.php';
require_once __DIR__ . '/../Manager/module.php';
ManagerSDLRuntime::reset();
$this->manager = new Manager(42);
$this->manager->Create();
foreach ([101 => 1000.0, 102 => 5000.0, 103 => 200.0, 104 => 1500.0, 105 => 70.0] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
$this->properties([
'NetzleistungVariableID' => 101,
'AnlagenWechselrichter' => $this->json([
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102],
]),
'AnlagenBatterien' => $this->json([
['ID' => 'battery', 'LeistungVariableID' => 103],
]),
'SDLLeistungVariableID' => 104,
'SDLSOCVariableID' => 105,
]);
foreach ([
'SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie',
] as $ident) {
ManagerSDLRuntime::child(42, $ident);
}
ManagerSDLRuntime::child(42, 'SDLStatus', 3);
}
public function testSDLByDefaultInactiveDoesNotRequireSources(): void
{
self::assertFalse($this->manager->testProperties['SDLAktiv']);
self::assertFalse($this->manager->testProperties['SDLEnergieflussAnzeigen']);
self::assertFalse($this->manager->testProperties['SDLDiagrammeAnzeigen']);
unset(ManagerSDLRuntime::$variables[104], ManagerSDLRuntime::$variables[105]);
$this->invoke('pruefeSDLKonfiguration');
$powers = $this->invoke('leseMessleistungen');
self::assertSame(0.0, $powers['SDL']);
self::assertSame(5800.0, $powers['Haus']);
self::assertNull($this->invoke('aktuelleSDLSOC'));
}
/** @dataProvider signedPowerProvider */
public function testSDLPowerIsExcludedFromHouseConsumption(float $raw, float $factor, float $sdl, float $house): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
ManagerSDLRuntime::variable(104, $raw);
$this->invoke('pruefeSDLKonfiguration');
self::assertSame([
'PV' => 5000.0, 'Haus' => $house, 'Netz' => 1000.0, 'Batterie' => 200.0, 'SDL' => $sdl,
], $this->invoke('leseMessleistungen'));
}
public static function signedPowerProvider(): array
{
return [
'charging' => [1500.0, 1.0, 1500.0, 4300.0],
'discharging' => [-1500.0, 1.0, -1500.0, 7300.0],
'kilowatts' => [2.5, 1000.0, 2500.0, 3300.0],
'inverted source' => [1500.0, -1.0, -1500.0, 7300.0],
];
}
/** @dataProvider duplicatePowerProvider */
public function testRejectsSDLPowerAlsoUsedByAnotherEnergySource(int $duplicateID): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungVariableID' => $duplicateID]);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('nicht zugleich');
$this->invoke('pruefeSDLKonfiguration');
}
public static function duplicatePowerProvider(): array
{
return ['grid' => [101], 'PV' => [102], 'battery' => [103]];
}
public function testRejectsSOCUsingThePowerVariable(): void
{
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 104]);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('separate numerische Variable');
$this->invoke('pruefeSDLKonfiguration');
}
/** @dataProvider invalidFactorProvider */
public function testRejectsInvalidSDLFactors(float $factor): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
$this->expectException(InvalidArgumentException::class);
$this->invoke('pruefeSDLKonfiguration');
}
public static function invalidFactorProvider(): array
{
return ['zero' => [0.0], 'infinite' => [INF], 'not a number' => [NAN]];
}
public function testMissingSDLPowerFailsWithoutReturningHouseConsumption(): void
{
$this->properties(['SDLAktiv' => true]);
unset(ManagerSDLRuntime::$variables[104]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('nicht verfuegbar');
$this->invoke('leseMessleistungen');
}
/** @dataProvider invalidPowerProvider */
public function testNonfiniteOrNonnumericPowerDoesNotBecomeZero($value): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(104, $value);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('keine endliche Zahl');
$this->invoke('leseMessleistungen');
}
public static function invalidPowerProvider(): array
{
return ['infinite' => [INF], 'negative infinite' => [-INF], 'not a number' => [NAN], 'string' => ['1500']];
}
public function testScaledSDLPowerOverflowIsRejected(): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => 2.0]);
ManagerSDLRuntime::variable(104, PHP_FLOAT_MAX);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('skalierte SDL-Istleistung');
$this->invoke('leseMessleistungen');
}
public function testSDLPowerMustBeANumericVariable(): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(104, '1500', 3);
$this->expectException(InvalidArgumentException::class);
$this->invoke('pruefeSDLKonfiguration');
}
/** @dataProvider socProvider */
public function testSDLSOCBoundaries(float $value, ?float $expected): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(105, $value);
self::assertSame($expected, $this->invoke('aktuelleSDLSOC'));
if ($expected === null) {
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
}
}
public static function socProvider(): array
{
return [
'empty' => [0.0, 0.0], 'full' => [100.0, 100.0], 'normal' => [37.5, 37.5],
'negative' => [-0.1, null], 'overfull' => [100.1, null], 'infinite' => [INF, null], 'NaN' => [NAN, null],
];
}
public function testSDLSOCMayBeOmittedAndDisappearingSourceIsIgnored(): void
{
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 0]);
$this->invoke('pruefeSDLKonfiguration');
self::assertNull($this->invoke('aktuelleSDLSOC'));
$this->properties(['SDLSOCVariableID' => 105]);
unset(ManagerSDLRuntime::$variables[105]);
self::assertNull($this->invoke('aktuelleSDLSOC'));
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
}
public function testPowerReadingPreservesSOCErrorUntilRecovery(): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(105, 101.0);
$this->invoke('aktuelleSDLLeistung');
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
$this->invoke('aktuelleSDLLeistung');
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
ManagerSDLRuntime::variable(105, 47.5);
$this->invoke('aktuelleSDLLeistung');
self::assertSame('Aktiv', $this->manager->GetValue('SDLStatus'));
}
public function testSDLSOCDoesNotChangeWeightedNormalBatterySOC(): void
{
ManagerSDLRuntime::variable(106, 20.0);
ManagerSDLRuntime::variable(107, 80.0);
$this->properties([
'SDLAktiv' => true,
'AnlagenBatterien' => $this->json([
['ID' => 'small', 'SOCVariableID' => 106, 'Nutzkapazitaet_kWh' => 10.0],
['ID' => 'large', 'SOCVariableID' => 107, 'Nutzkapazitaet_kWh' => 30.0],
]),
]);
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
self::assertSame(70.0, $this->invoke('aktuelleSDLSOC'));
ManagerSDLRuntime::variable(105, 0.0);
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
}
public function testCounterDeltasExcludeSDLChargingAndIncludeDischarging(): void
{
$this->configureCounters();
$baseline = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertCount(8, $baseline);
self::assertSame(array_fill_keys(array_keys($baseline), 0.0), $baseline);
$this->advanceCounters();
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertEqualsWithDelta(2.1, $deltas['Hausenergie'], 0.000001);
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
self::assertSame(0.2, $deltas['SDLEntladenEnergie']);
self::assertEqualsWithDelta(0.4, $deltas['BatterieLadenEnergie'], 0.000001);
self::assertEqualsWithDelta(0.1, $deltas['BatterieEntladenEnergie'], 0.000001);
}
public function testMissingSDLIntervalDoesNotPolluteHouseEnergyOrTheNextInterval(): void
{
$this->configureCounters();
$this->invoke('leseEnergieZaehlerDeltas');
$this->advanceCounters();
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.0, 0.0, false]);
self::assertSame(0.0, $deltas['Hausenergie']);
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
self::assertEqualsWithDelta(1.0, $deltas['NetzbezugEnergie'], 0.000001);
ManagerSDLRuntime::variable(201, 102.5);
$next = $this->invoke('leseEnergieZaehlerDeltas', [0.1, 0.0, true]);
self::assertEqualsWithDelta(0.4, $next['Hausenergie'], 0.000001);
}
public function testCounterResetSuppressesHouseEnergyForActiveSDL(): void
{
$this->configureCounters();
$this->invoke('leseEnergieZaehlerDeltas');
$this->advanceCounters();
ManagerSDLRuntime::variable(202, 0.0);
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertSame(0.0, $deltas['Hausenergie']);
self::assertSame(0.0, $deltas['NetzbezugEnergie']);
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
}
public function testWithoutConfiguredCountersReturnsNull(): void
{
$this->properties(['SDLAktiv' => true]);
self::assertNull($this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]));
}
public function testChangingSDLSourceResetsBaselinesButKeepsCollectedEnergy(): void
{
$this->properties(['SDLAktiv' => true]);
$this->invoke('synchronisiereSDLMesskonfiguration');
$this->manager->testAttributes['MessungLetzterZeitstempel'] = 123;
$this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung'] = 100;
$this->manager->testAttributes['MessungZaehlerstaende'] = '{"pv:pv":5.0}';
$this->manager->testAttributes['MessungEnergiePuffer'] = '{"Hausenergie":1.5}';
$this->invoke('synchronisiereSDLMesskonfiguration');
self::assertSame(123, $this->manager->testAttributes['MessungLetzterZeitstempel']);
$this->properties(['SDLLeistungsfaktor' => -1.0]);
$this->invoke('synchronisiereSDLMesskonfiguration');
self::assertSame(0, $this->manager->testAttributes['MessungLetzterZeitstempel']);
self::assertSame(0, $this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung']);
self::assertSame('{}', $this->manager->testAttributes['MessungZaehlerstaende']);
self::assertSame('{"Hausenergie":1.5}', $this->manager->testAttributes['MessungEnergiePuffer']);
}
/** @dataProvider chartProvider */
public function testChartMergePreservesCustomizationsAndOnlyRemovesSDL(bool $energy): void
{
$this->properties(['SDLAktiv' => true, 'SDLDiagrammeAnzeigen' => true]);
$powerIdent = $energy ? 'SDLLadenEnergie' : 'SDLLeistungArchiv';
$custom = ['variableID' => 999, 'title' => 'Custom series', 'strokeColor' => '#ABCDEF'];
$sdl = ['variableID' => $this->manager->GetIDForIdent($powerIdent), 'title' => 'Own SDL name', 'visible' => false];
ManagerSDLRuntime::$media[301] = base64_encode($this->json([
'datasets' => [$custom, $sdl], 'customOption' => ['keep' => true],
]));
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
$chart = $this->chart();
self::assertSame($custom, $chart['datasets'][0]);
self::assertSame($sdl, $chart['datasets'][1]);
self::assertSame(['keep' => true], $chart['customOption']);
self::assertCount($energy ? 4 : 3, $chart['datasets']);
$soc = $chart['datasets'][count($chart['datasets']) - 1];
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $soc['variableID']);
self::assertSame('right', $soc['side']);
if ($energy) {
self::assertSame('bar', $chart['datasets'][2]['type']);
self::assertSame($this->manager->GetIDForIdent('SDLEntladenEnergie'), $chart['datasets'][2]['variableID']);
}
$writes = ManagerSDLRuntime::$writes;
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
self::assertSame($writes, ManagerSDLRuntime::$writes);
$this->properties(['SDLDiagrammeAnzeigen' => false]);
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
self::assertSame([$custom], $this->chart()['datasets']);
self::assertSame(['keep' => true], $this->chart()['customOption']);
}
public static function chartProvider(): array
{
return ['power chart' => [false], 'energy chart' => [true]];
}
public function testEnergyFlowMergePreservesCustomNodesAndIsIdempotent(): void
{
$this->properties(['SDLAktiv' => true, 'SDLEnergieflussAnzeigen' => true]);
$custom = ['VariableID' => 999, 'Name' => 'Custom consumer', 'Type' => 'consumer', 'Invert' => true];
ManagerSDLRuntime::$properties[302]['Config'] = $this->json([$custom]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
$nodes = $this->flow();
self::assertSame($custom, $nodes[0]);
self::assertCount(2, $nodes);
self::assertSame('storage', $nodes[1]['Type']);
self::assertSame($this->manager->GetIDForIdent('EnergieflussSDL'), $nodes[1]['VariableID']);
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $nodes[1]['InfoVariableID']);
self::assertTrue(ManagerSDLRuntime::$hidden[$nodes[1]['VariableID']]);
$nodes[1]['Name'] = 'My SDL';
$nodes[1]['Invert'] = true;
ManagerSDLRuntime::$properties[302]['Config'] = $this->json($nodes);
$writes = ManagerSDLRuntime::$writes;
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame($nodes, $this->flow());
self::assertSame($writes, ManagerSDLRuntime::$writes);
$this->properties(['SDLSOCVariableID' => 0]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame(1, $this->flow()[1]['InfoVariableID']);
self::assertSame('My SDL', $this->flow()[1]['Name']);
$this->properties(['SDLAktiv' => false]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame([$custom], $this->flow());
}
private function configureCounters(): void
{
$this->properties([
'SDLAktiv' => true,
'NetzbezugEnergieVariableID' => 202,
'NetzeinspeisungEnergieVariableID' => 203,
'AnlagenWechselrichter' => $this->json([
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102, 'ErzeugungsenergieVariableID' => 201],
]),
'AnlagenBatterien' => $this->json([
['ID' => 'battery', 'LeistungVariableID' => 103, 'LadeenergieVariableID' => 204, 'EntladeenergieVariableID' => 205],
]),
]);
foreach ([201 => 100.0, 202 => 10.0, 203 => 5.0, 204 => 10.0, 205 => 8.0] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
}
private function advanceCounters(): void
{
foreach ([201 => 102.0, 202 => 11.0, 203 => 5.2, 204 => 10.4, 205 => 8.1] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
}
private function properties(array $properties): void
{
$this->manager->testProperties = array_replace($this->manager->testProperties, $properties);
}
private function invoke(string $name, array $arguments = [])
{
$method = new ReflectionMethod(Manager::class, $name);
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}
return $method->invokeArgs($this->manager, $arguments);
}
private function json(array $value): string
{
return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
}
private function chart(): array
{
return json_decode(base64_decode(ManagerSDLRuntime::$media[301]), true, 512, JSON_THROW_ON_ERROR);
}
private function flow(): array
{
return json_decode(ManagerSDLRuntime::$properties[302]['Config'], true, 512, JSON_THROW_ON_ERROR);
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
/** Minimal Symcon runtime, loaded only inside isolated ManagerSDLTest processes. */
final class ManagerSDLRuntime
{
public static array $variables = [];
public static array $children = [];
public static array $properties = [];
public static array $media = [];
public static array $writes = [];
public static array $hidden = [];
private static int $nextID = 10000;
public static function reset(): void
{
self::$variables = [];
self::$children = [];
self::$properties = [];
self::$media = [];
self::$writes = [];
self::$hidden = [];
self::$nextID = 10000;
}
public static function variable(int $id, $value, int $type = 2): int
{
self::$variables[$id] = ['VariableType' => $type, 'Value' => $value];
return $id;
}
public static function child(int $parent, string $ident, int $type = 2): int
{
if (!isset(self::$children[$parent][$ident])) {
$id = self::$nextID++;
self::$children[$parent][$ident] = $id;
self::variable($id, $type === 3 ? '' : 0.0, $type);
}
return self::$children[$parent][$ident];
}
}
class IPSModule
{
public int $InstanceID;
public array $testProperties = [];
public array $testAttributes = [];
public function __construct(int $instanceID)
{
$this->InstanceID = $instanceID;
}
public function Create(): void
{
}
public function __call(string $method, array $arguments)
{
if (preg_match('/^(Register|Read|Write)(Property|Attribute)(Boolean|Integer|Float|String)$/', $method, $parts)) {
$storage = $parts[2] === 'Property' ? 'testProperties' : 'testAttributes';
$ident = $arguments[0];
if ($parts[1] === 'Register') {
$this->{$storage}[$ident] = $this->{$storage}[$ident] ?? $arguments[1];
return null;
}
if ($parts[1] === 'Write') {
$this->{$storage}[$ident] = $arguments[1];
return null;
}
if (!array_key_exists($ident, $this->{$storage})) {
throw new RuntimeException('Unregistered test property or attribute: ' . $ident);
}
return $this->{$storage}[$ident];
}
if (preg_match('/^RegisterVariable(Boolean|Integer|Float|String)$/', $method, $parts)) {
$types = ['Boolean' => 0, 'Integer' => 1, 'Float' => 2, 'String' => 3];
return ManagerSDLRuntime::child($this->InstanceID, $arguments[0], $types[$parts[1]]);
}
throw new BadMethodCallException('Unsupported Symcon test call: ' . $method);
}
public function EnableAction(string $ident): void
{
}
public function RegisterTimer(string $ident, int $interval, string $script): void
{
}
public function GetIDForIdent(string $ident): int
{
if (!isset(ManagerSDLRuntime::$children[$this->InstanceID][$ident])) {
throw new RuntimeException('Unknown test child: ' . $ident);
}
return ManagerSDLRuntime::$children[$this->InstanceID][$ident];
}
public function SetValue(string $ident, $value): void
{
ManagerSDLRuntime::$variables[$this->GetIDForIdent($ident)]['Value'] = $value;
}
public function GetValue(string $ident)
{
return GetValue($this->GetIDForIdent($ident));
}
}
function IPS_VariableExists(int $variableID): bool
{
return isset(ManagerSDLRuntime::$variables[$variableID]);
}
function IPS_GetVariable(int $variableID): array
{
if (!IPS_VariableExists($variableID)) {
throw new RuntimeException('Unknown test variable: ' . $variableID);
}
return ManagerSDLRuntime::$variables[$variableID];
}
function GetValue(int $variableID)
{
return IPS_GetVariable($variableID)['Value'];
}
function IPS_GetObjectIDByIdent(string $ident, int $parentID)
{
return ManagerSDLRuntime::$children[$parentID][$ident] ?? false;
}
function IPS_GetProperty(int $instanceID, string $property)
{
return ManagerSDLRuntime::$properties[$instanceID][$property];
}
function IPS_SetProperty(int $instanceID, string $property, $value): void
{
ManagerSDLRuntime::$properties[$instanceID][$property] = $value;
ManagerSDLRuntime::$writes[] = ['property', $instanceID, $property];
}
function IPS_ApplyChanges(int $instanceID): void
{
ManagerSDLRuntime::$writes[] = ['apply', $instanceID];
}
function IPS_GetMediaContent(int $mediaID): string
{
return ManagerSDLRuntime::$media[$mediaID];
}
function IPS_SetMediaContent(int $mediaID, string $content): void
{
ManagerSDLRuntime::$media[$mediaID] = $content;
ManagerSDLRuntime::$writes[] = ['media', $mediaID];
}
function IPS_SetHidden(int $objectID, bool $hidden): void
{
ManagerSDLRuntime::$hidden[$objectID] = $hidden;
}
+6
View File
@@ -133,6 +133,7 @@ PHP
'GoodWe-Entladen verwendet negativen Managerwert und Modus 12', 'GoodWe-Entladen verwendet negativen Managerwert und Modus 12',
static function (TestContext $test) use ( static function (TestContext $test) use (
$managerdatenSenden, $managerdatenSenden,
$batterieID,
$modusID, $modusID,
$leistungID $leistungID
): void { ): void {
@@ -141,6 +142,10 @@ PHP
static fn (): bool => (float) GetValue($leistungID) === 843.0 static fn (): bool => (float) GetValue($leistungID) === 843.0
&& (int) GetValue($modusID) === 12 && (int) GetValue($modusID) === 12
); );
$test->assertSame(-843, GetValue($test->objectByIdent('Sollleistung', $batterieID)));
IPS_RequestAction($batterieID, 'VorgabeTimeout', 0);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $batterieID)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $batterieID)));
} }
); );
@@ -197,6 +202,7 @@ PHP
$leistungID $leistungID
): void { ): void {
$test->configureInstance($batterieID, ['Batteriemanagement' => 1]); $test->configureInstance($batterieID, ['Batteriemanagement' => 1]);
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $batterieID)));
$test->waitUntil( $test->waitUntil(
static fn (): bool => (float) GetValue($leistungID) === 0.0 static fn (): bool => (float) GetValue($leistungID) === 0.0
&& (int) GetValue($managementID) === 1 && (int) GetValue($managementID) === 1
+23 -3
View File
@@ -186,7 +186,7 @@ return static function (TestContext $test): void {
$test->runCase('Solarumschaltung wartet auf die neue Manager-Vorgabe', static function ( $test->runCase('Solarumschaltung wartet auf die neue Manager-Vorgabe', static function (
TestContext $test TestContext $test
) use ($station): void { ) use ($station, $sendeBeobachtung): void {
$managerId = $test->createInstance( $managerId = $test->createInstance(
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}', '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
'Hilfsmanager Easee-Uebergang', 'Hilfsmanager Easee-Uebergang',
@@ -221,10 +221,11 @@ return static function (TestContext $test): void {
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station)) (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station))
); );
$test->assertSame( $test->assertSame(
4104, 0,
GetValue($test->objectByIdent('Sollleistung', $station)), GetValue($test->objectByIdent('Sollleistung', $station)),
'Die Sollleistung muss bis zur neuen Manager-Vorgabe stabil bleiben.' 'Die verworfene Manager-Vorgabe darf nicht als aktuelle Sollleistung stehen bleiben.'
); );
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$sendeSollwert(10944); $sendeSollwert(10944);
$test->assertTrue(str_contains( $test->assertTrue(str_contains(
@@ -234,6 +235,25 @@ return static function (TestContext $test): void {
IPS_RequestAction($station, 'Solarladen', true); IPS_RequestAction($station, 'Solarladen', true);
$sendeSollwert(0); $sendeSollwert(0);
$test->assertSame(true, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $station)));
$sendeSollwert(10944);
$sendeBeobachtung('EH123456', 109, 1);
$test->assertSame('[0]', GetValue($test->objectByIdent('LeistungsangebotDiagnose', $station)));
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $station)));
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station)),
'"Ampere":0'
));
$sendeBeobachtung('EH123456', 110, 30);
$sendeBeobachtung('EH123456', 109, 3);
$sendeSollwert(10944);
IPS_RequestAction($station, 'VorgabeTimeout', 0);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $station)));
}); });
$test->runCase('SignalR-Abbruch erreicht die Ladestation', static function ( $test->runCase('SignalR-Abbruch erreicht die Ladestation', static function (
+26 -2
View File
@@ -387,10 +387,11 @@ return static function (TestContext $test): void {
'Die Umschaltung darf vor der neuen Manager-Vorgabe keinen Zwischenwert setzen.' 'Die Umschaltung darf vor der neuen Manager-Vorgabe keinen Zwischenwert setzen.'
); );
$test->assertSame( $test->assertSame(
4104, 0,
GetValue($test->objectByIdent('Sollleistung', $stationId)), GetValue($test->objectByIdent('Sollleistung', $stationId)),
'Die Sollleistung muss bis zur neuen Manager-Vorgabe stabil bleiben.' 'Die verworfene Manager-Vorgabe darf nicht als aktuelle Sollleistung stehen bleiben.'
); );
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $stationId)));
$angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId)); $angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId));
$test->assertSame('[10944]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.'); $test->assertSame('[10944]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.');
$abgewiesen = false; $abgewiesen = false;
@@ -502,6 +503,29 @@ return static function (TestContext $test): void {
); );
}); });
$test->runCase('Abziehen und Vorgabe-Timeout hinterlassen keine alte Sollleistung', static function (
TestContext $test
) use ($stationen, $goEAntwort, $anforderungSenden): void {
$stationId = $stationen['go-e alt'];
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(2, 10944, [160, 160, 160]));
IPS_ApplyChanges($stationId);
IPS_RequestAction($stationId, 'Aktiv', true);
IPS_RequestAction($stationId, 'Solarladen', true);
$anforderungSenden($stationId, 10944);
$test->assertSame(10944, GetValue($test->objectByIdent('Sollleistung', $stationId)));
IPS_RequestAction($stationId, 'VorgabeTimeout', 0);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $stationId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $stationId)));
$anforderungSenden($stationId, 10944);
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(1, 0));
IPS_ApplyChanges($stationId);
$test->assertSame('[0]', GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId)));
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $stationId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $stationId)));
});
$test->runCase('Leistungswerte und Fahrzeugstatus folgen ihrer Sichtbarkeitsgruppe', static function ( $test->runCase('Leistungswerte und Fahrzeugstatus folgen ihrer Sichtbarkeitsgruppe', static function (
TestContext $test TestContext $test
) use ($stationen): void { ) use ($stationen): void {
+213 -25
View File
@@ -6,8 +6,13 @@ use Belevo\EnelixEMS\SymconTest\TestContext;
return static function (TestContext $test): void { return static function (TestContext $test): void {
$netzleistungId = $test->createVariable(2, 'Netzleistung', -800.0); $netzleistungId = $test->createVariable(2, 'Netzleistung', -800.0);
$netzbezugEnergieId = $test->createVariable(2, 'Netzbezug-Energie', 32.0);
$netzeinspeisungEnergieId = $test->createVariable(2, 'Netzeinspeisung-Energie', 16.0);
$pvLeistungId = $test->createVariable(2, 'PV-Leistung', 4200.0); $pvLeistungId = $test->createVariable(2, 'PV-Leistung', 4200.0);
$pvEnergieId = $test->createVariable(2, 'PV-Energie', 120.0);
$batterieLeistungId = $test->createVariable(2, 'Batterieleistung', 700.0); $batterieLeistungId = $test->createVariable(2, 'Batterieleistung', 700.0);
$batterieLadeenergieId = $test->createVariable(2, 'Batterie-Ladeenergie', 18.0);
$batterieEntladeenergieId = $test->createVariable(2, 'Batterie-Entladeenergie', 12.0);
$batterieSocId = $test->createVariable(2, 'Batterie-Ladezustand', 62.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(
@@ -82,6 +87,8 @@ return static function (TestContext $test): void {
'ACNennleistung_kW' => 10.0, 'ACNennleistung_kW' => 10.0,
'IstleistungVariableID' => $pvLeistungId, 'IstleistungVariableID' => $pvLeistungId,
'Istleistungsfaktor' => 1.0, 'Istleistungsfaktor' => 1.0,
'ErzeugungsenergieVariableID' => $pvEnergieId,
'Erzeugungsenergiefaktor' => 1.0,
]]; ]];
$pvKonfiguration = [[ $pvKonfiguration = [[
'ID' => 'pv-1', 'ID' => 'pv-1',
@@ -106,6 +113,9 @@ return static function (TestContext $test): void {
'LeistungVariableID' => $batterieLeistungId, 'LeistungVariableID' => $batterieLeistungId,
'Leistungsfaktor' => 1.0, 'Leistungsfaktor' => 1.0,
'SOCVariableID' => $batterieSocId, 'SOCVariableID' => $batterieSocId,
'LadeenergieVariableID' => $batterieLadeenergieId,
'EntladeenergieVariableID' => $batterieEntladeenergieId,
'Energiefaktor' => 1.0,
]]; ]];
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE')); $lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
$managerId = $test->createInstance( $managerId = $test->createInstance(
@@ -113,6 +123,8 @@ return static function (TestContext $test): void {
'Manager', 'Manager',
[ [
'NetzleistungVariableID' => $netzleistungId, 'NetzleistungVariableID' => $netzleistungId,
'NetzbezugEnergieVariableID' => $netzbezugEnergieId,
'NetzeinspeisungEnergieVariableID' => $netzeinspeisungEnergieId,
'EnergyPieAnzeigen' => true, 'EnergyPieAnzeigen' => true,
'EnergiediagrammeAnzeigen' => true, 'EnergiediagrammeAnzeigen' => true,
'FunFactsAnzeigen' => true, 'FunFactsAnzeigen' => true,
@@ -226,11 +238,17 @@ return static function (TestContext $test): void {
}); });
$test->runCase('Energiewerte, Archivregeln und Unterobjekte werden erzeugt', static function ( $test->runCase('Energiewerte, Archivregeln und Unterobjekte werden erzeugt', static function (
TestContext $test TestContext $test
) use ($managerId): void { ) use ($managerId, $pvLeistungId): void {
IPS_RequestAction($managerId, 'MesswerteAufzeichnen', true); IPS_RequestAction($managerId, 'MesswerteAufzeichnen', true);
$test->assertSame( $test->assertSame(
4200.0, 4200.0,
GetValue($test->objectByIdent('PVLeistungArchiv', $managerId)) GetValue($test->objectByIdent('PVLeistungArchiv', $managerId)),
sprintf(
'PV-Archiv unveraendert; Quelle %.1f W, Managerstatus %d, Konfiguration %s.',
(float) GetValue($pvLeistungId),
(int) IPS_GetInstance($managerId)['InstanceStatus'],
(string) IPS_GetProperty($managerId, 'AnlagenWechselrichter')
)
); );
$test->assertSame( $test->assertSame(
2700.0, 2700.0,
@@ -288,18 +306,69 @@ return static function (TestContext $test): void {
} }
} }
$pvFlussID = $test->objectByIdent('EnergieflussPV', $managerId); $pvFlussID = $test->objectByIdent('EnergieflussPV', $managerId);
$test->assertSame(4200.0, (float) GetValue($pvFlussID)); $test->assertSame(
4200.0,
(float) GetValue($pvFlussID),
'Der anfaengliche PV-Energiefluss ist falsch.'
);
$test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']); $test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']);
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', 1); $pieID = $test->objectByIdent('EnergyPie', $managerId);
IPS_ApplyChanges($managerId); $leistungsdiagrammID = $test->objectByIdent('Leistungsdiagramm', $managerId);
$test->assertSame(4.2, (float) GetValue($pvFlussID)); $energiediagrammID = $test->objectByIdent('Energiediagramm', $managerId);
$test->assertSame('Enelix.Power.kW', IPS_GetVariable($pvFlussID)['VariableProfile']); $energiediagramm = json_decode(
base64_decode((string) IPS_GetMediaContent($energiediagrammID), true),
true,
512,
JSON_THROW_ON_ERROR
);
$energietitel = array_column($energiediagramm['datasets'] ?? [], 'title');
foreach ($energiediagramm['datasets'] ?? [] as $datensatz) {
$test->assertSame('bar', $datensatz['type'] ?? null);
}
foreach ([
'Netzbezug',
'Netzeinspeisung',
'Batterie laden gesamt',
'Batterie entladen gesamt',
] as $titel) {
$test->assertTrue(in_array($titel, $energietitel, true));
}
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false); $angepasstesDiagramm = base64_encode(
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']); json_encode(['datasets' => [], 'custom' => 'bleibt'], JSON_THROW_ON_ERROR)
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', true); );
$test->assertSame(false, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']); IPS_SetMediaContent($energiediagrammID, $angepasstesDiagramm);
IPS_SetProperty($pieID, 'Zaehlerfaktor', 2.0);
IPS_ApplyChanges($pieID);
IPS_SetProperty($energieflussID, 'Config', '[]');
IPS_ApplyChanges($energieflussID);
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', 1);
IPS_SetProperty($managerId, 'DiagrammPVModus', 2);
IPS_SetProperty($managerId, 'DiagrammBatterieModus', 2);
IPS_ApplyChanges($managerId);
$test->assertSame($pieID, $test->objectByIdent('EnergyPie', $managerId));
$test->assertSame(
$leistungsdiagrammID,
$test->objectByIdent('Leistungsdiagramm', $managerId)
);
$test->assertSame(
$energiediagrammID,
$test->objectByIdent('Energiediagramm', $managerId)
);
$test->assertSame(
$angepasstesDiagramm,
(string) IPS_GetMediaContent($energiediagrammID)
);
$test->assertSame(2.0, (float) IPS_GetProperty($pieID, 'Zaehlerfaktor'));
$test->assertSame('[]', (string) IPS_GetProperty($energieflussID, 'Config'));
$test->assertSame(
4200.0,
(float) GetValue($pvFlussID),
'Der bestehende PV-Energiefluss wurde beim Apply veraendert.'
);
$test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']);
foreach ([ foreach ([
'EnergyPieAnzeigen', 'EnergyPieAnzeigen',
@@ -322,7 +391,135 @@ return static function (TestContext $test): void {
$ident . ' wurde beim Ausschalten nicht entfernt.' $ident . ' wurde beim Ausschalten nicht entfernt.'
); );
} }
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']); $test->assertSame(
true,
(bool) IPS_GetObject($energieflussID)['ObjectIsHidden']
);
foreach ([
'EnergyPieAnzeigen',
'EnergiediagrammeAnzeigen',
'EnergieflussAnzeigen',
] as $property) {
IPS_SetProperty($managerId, $property, true);
}
IPS_ApplyChanges($managerId);
$neuerFlussID = $test->objectByIdent('Energiefluss', $managerId);
$test->assertSame($energieflussID, $neuerFlussID);
$test->assertSame(false, (bool) IPS_GetObject($neuerFlussID)['ObjectIsHidden']);
$test->assertTrue((string) IPS_GetProperty($neuerFlussID, 'Config') !== '[]');
$test->assertSame(4.2, (float) GetValue($pvFlussID));
$test->assertSame('Enelix.Power.kW', IPS_GetVariable($pvFlussID)['VariableProfile']);
$neuesLeistungsdiagramm = json_decode(
base64_decode(
(string) IPS_GetMediaContent(
$test->objectByIdent('Leistungsdiagramm', $managerId)
),
true
),
true,
512,
JSON_THROW_ON_ERROR
);
$leistungstitel = array_column(
$neuesLeistungsdiagramm['datasets'] ?? [],
'title'
);
foreach (['PV gesamt', 'Hybridwechselrichter', 'Batterie gesamt', 'Hausspeicher'] as $titel) {
$test->assertTrue(in_array($titel, $leistungstitel, true));
}
});
$test->runCase('SDL korrigiert Hauslast und ergaenzt bestehende Anzeigen idempotent', static function (
TestContext $test
) use ($managerId): void {
$leistungID = $test->createVariable(2, 'SDL Istleistung', -1.2);
$socID = $test->createVariable(2, 'SDL SOC', 47.5);
$flussID = $test->objectByIdent('Energiefluss', $managerId);
$chartID = $test->objectByIdent('Leistungsdiagramm', $managerId);
$flussVorher = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
$chartVorher = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
try {
IPS_SetProperty($managerId, 'SDLAktiv', true);
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $leistungID);
IPS_SetProperty($managerId, 'SDLLeistungsfaktor', -1000.0);
IPS_SetProperty($managerId, 'SDLSOCVariableID', $socID);
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', true);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
IPS_ApplyChanges($managerId);
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
$test->assertSame(1200.0, GetValue($test->objectByIdent('SDLLeistungArchiv', $managerId)));
$test->assertSame(47.5, GetValue($test->objectByIdent('SDLSOCArchiv', $managerId)));
$test->assertSame(1.2, GetValue($test->objectByIdent('EnergieflussSDL', $managerId)));
$test->assertSame('Aktiv', GetValue($test->objectByIdent('SDLStatus', $managerId)));
$fluss = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
$test->assertSame($flussVorher, array_slice($fluss, 0, count($flussVorher)));
$test->assertSame(count($flussVorher) + 1, count($fluss));
$test->assertSame($test->objectByIdent('SDLSOCArchiv', $managerId), $fluss[count($fluss) - 1]['InfoVariableID']);
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
$test->assertSame($chartVorher['datasets'], array_slice($chart['datasets'], 0, count($chartVorher['datasets'])));
$test->assertSame('right', $chart['datasets'][count($chart['datasets']) - 1]['side']);
$test->assertSame('~Intensity.100', $chart['datasets'][count($chart['datasets']) - 1]['profile']);
IPS_ApplyChanges($managerId);
$test->assertSame($fluss, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
$test->assertSame($chart, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
foreach ([0 => ['~Power', 1200.0], 1 => ['Enelix.Power.kW', 1.2]] as $einheit => $erwartet) {
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', false);
IPS_ApplyChanges($managerId);
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', $einheit);
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', true);
IPS_ApplyChanges($managerId);
$flussID = $test->objectByIdent('Energiefluss', $managerId);
$sdlID = $test->objectByIdent('EnergieflussSDL', $managerId);
$test->assertSame($erwartet[0], IPS_GetVariable($sdlID)['VariableProfile']);
$test->assertSame($erwartet[1], GetValue($sdlID));
}
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
IPS_ApplyChanges($managerId);
$test->assertSame($flussVorher, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
$test->assertSame($chartVorher, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
$test->assertEquals(1.9, GetValue($test->objectByIdent('EnergieflussBatterie', $managerId)), 0.000001);
IPS_SetProperty($managerId, 'SDLSOCVariableID', 0);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
IPS_ApplyChanges($managerId);
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
$test->assertSame(count($chartVorher['datasets']) + 1, count($chart['datasets']));
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', false);
$test->assertTrue(IPS_ApplyChanges($managerId));
$archivID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
foreach (['SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie'] as $ident) {
$test->assertSame(false, AC_GetLoggingStatus($archivID, $test->objectByIdent($ident, $managerId)));
}
} finally {
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', true);
IPS_SetProperty($managerId, 'SDLAktiv', false);
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
IPS_ApplyChanges($managerId);
}
$test->assertSame(2700.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
});
$test->runCase('SDL weist fehlende und doppelte Leistungsquellen zurueck', static function (
TestContext $test
) use ($managerId, $batterieLeistungId): void {
try {
IPS_SetProperty($managerId, 'SDLAktiv', true);
foreach ([0, $batterieLeistungId] as $quelle) {
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $quelle);
IPS_ApplyChanges($managerId);
$test->assertInstanceStatus($managerId, 202);
}
} finally {
IPS_SetProperty($managerId, 'SDLAktiv', false);
IPS_SetProperty($managerId, 'SDLLeistungVariableID', 0);
IPS_ApplyChanges($managerId);
}
}); });
$zustandswert = static function (array $daten, int $instanzId, string $kennung) { $zustandswert = static function (array $daten, int $instanzId, string $kennung) {
@@ -470,12 +667,11 @@ return static function (TestContext $test): void {
)); ));
}); });
$test->runCase('Veralteter Verbraucher blockiert aktuelle Verbraucher nicht', static function ( $test->runCase('Veraltete Verbraucher werden aktiv neu angefordert', static function (
TestContext $test TestContext $test
) use ( ) use (
$managerId, $managerId,
$netzleistungId, $netzleistungId,
$einStufigId,
$warmwasserId, $warmwasserId,
$pufferspeicherId, $pufferspeicherId,
$melden, $melden,
@@ -496,17 +692,9 @@ return static function (TestContext $test): void {
IPS_RequestAction($managerId, 'Regeln', true); IPS_RequestAction($managerId, 'Regeln', true);
$daten = $verbraucherstatus(); $daten = $verbraucherstatus();
$test->assertSame(false, $daten['Synchronisiert'] ?? null); $test->assertSame(true, $daten['Synchronisiert'] ?? null);
$test->assertSame(2, count($daten['Verbraucher'] ?? [])); $test->assertSame(3, count($daten['Verbraucher'] ?? []));
$test->assertSame(1, count($daten['NichtGeregelt'] ?? [])); $test->assertSame(0, count($daten['NichtGeregelt'] ?? []));
$test->assertSame(
$einStufigId,
$daten['NichtGeregelt'][0]['InstanzID'] ?? null
);
$test->assertSame(
'Meldung ist veraltet',
$daten['NichtGeregelt'][0]['Grund'] ?? null
);
$test->assertTrue( $test->assertTrue(
(float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) > 0.0 (float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) > 0.0
); );
+1
View File
@@ -105,6 +105,7 @@ return static function (TestContext $test): void {
$test->assertSame(false, GetValue($test->objectByIdent('Heizbedarf', $verbraucherId))); $test->assertSame(false, GetValue($test->objectByIdent('Heizbedarf', $verbraucherId)));
SetValue($pufferId, 55.0); SetValue($pufferId, 55.0);
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
IPS_RequestAction($verbraucherId, 'Melden', true); IPS_RequestAction($verbraucherId, 'Melden', true);
$test->assertSame([false, false], $kontaktzustand()); $test->assertSame([false, false], $kontaktzustand());
}); });
@@ -225,4 +225,18 @@ return static function (TestContext $test): void {
) === false); ) === false);
$test->assertInstanceStatus($verbraucherId); $test->assertInstanceStatus($verbraucherId);
}); });
$test->runCase('Deaktivierung verwirft eine positive Vorgabe vollstaendig', static function (
TestContext $test
) use ($verbraucherId, $anforderungSenden): void {
IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 0);
IPS_SetProperty($verbraucherId, 'Mindesteinschaltdauer', 0);
IPS_SetProperty($verbraucherId, 'Mindestausschaltdauer', 0);
IPS_ApplyChanges($verbraucherId);
$anforderungSenden(1200);
$test->assertSame(1200, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
IPS_RequestAction($verbraucherId, 'Aktiv', false);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $verbraucherId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
});
}; };
+2
View File
@@ -121,6 +121,8 @@ return static function (TestContext $test): void {
TestContext $test TestContext $test
) use ($waermepumpe, $kontakt1, $kontakt2): void { ) use ($waermepumpe, $kontakt1, $kontakt2): void {
IPS_RequestAction($waermepumpe, 'Aktiv', false); IPS_RequestAction($waermepumpe, 'Aktiv', false);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $waermepumpe)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $waermepumpe)));
IPS_RequestAction($waermepumpe, 'Aktiv', true); IPS_RequestAction($waermepumpe, 'Aktiv', true);
$test->waitUntil( $test->waitUntil(
static fn (): bool => GetValue($kontakt1) === false static fn (): bool => GetValue($kontakt1) === false
@@ -110,6 +110,33 @@ return static function (TestContext $test): void {
$temperaturSetzen(52.0); $temperaturSetzen(52.0);
}); });
$test->runCase('Abgelaufene Vorgabe wird auch in der Anzeige verworfen', static function (
TestContext $test
) use ($verbraucherId, $managerId, $kontakte): void {
IPS_SetProperty($verbraucherId, 'VorgabeTimeout', 1);
IPS_ApplyChanges($verbraucherId);
IPS_RequestAction($verbraucherId, 'Aktiv', true);
IPS_RequestAction($verbraucherId, 'ManagerdatenEmpfangen', json_encode([
'Kopf' => [
'Version' => '4.0',
'AbsenderID' => $managerId,
'EmpfaengerID' => $verbraucherId,
'Zeitpunkt' => time(),
],
'Betriebsart' => 'PV',
'Sollleistung_W' => 1000,
], JSON_THROW_ON_ERROR));
$test->assertSame(1000, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
IPS_Sleep(2100);
IPS_RequestAction($verbraucherId, 'Melden', true);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $verbraucherId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
$test->waitUntil(static fn (): bool => GetValue($kontakte[0]) === false);
IPS_RequestAction($verbraucherId, 'Aktiv', false);
IPS_SetProperty($verbraucherId, 'VorgabeTimeout', 120);
IPS_ApplyChanges($verbraucherId);
});
if ($lizenzcode === '') { if ($lizenzcode === '') {
$test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function ( $test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function (
TestContext $test TestContext $test
+109
View File
@@ -9,6 +9,86 @@ use PHPUnit\Framework\TestCase;
final class VerbraucherBasisTraitTest extends TestCase final class VerbraucherBasisTraitTest extends TestCase
{ {
/** @dataProvider sollwerte */
public function testVerwerfenLoeschtGespeicherteUndSichtbareVorgabe(int $leistung, bool $sichtbar): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => $leistung, 'SollwertGueltig' => true];
$modul->sichtbareWerte = $sichtbar ? $modul->zustand : [];
$modul->verwerfen();
$modul->verwerfen();
self::assertSame(['Sollleistung' => 0, 'SollwertGueltig' => false], $modul->zustand);
self::assertSame($sichtbar ? $modul->zustand : [], $modul->sichtbareWerte);
}
/** @dataProvider sollwerte */
public function testUpdateBereinigtAlteUngueltigeVorgaben(int $leistung, bool $sichtbar): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => $leistung, 'SollwertGueltig' => false];
$modul->sichtbareWerte = $sichtbar ? $modul->zustand : [];
$modul->bereinigen();
self::assertSame(['Sollleistung' => 0, 'SollwertGueltig' => false], $modul->zustand);
self::assertSame($sichtbar ? $modul->zustand : [], $modul->sichtbareWerte);
}
/** @dataProvider sollwerte */
public function testUpdateErhaeltGueltigeVorgabenAuchNullUndEntladen(int $leistung, bool $sichtbar): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => $leistung, 'SollwertGueltig' => true];
$modul->sichtbareWerte = $sichtbar ? $modul->zustand : [];
$vorher = $modul->zustand;
$modul->bereinigen();
self::assertSame($vorher, $modul->zustand);
self::assertSame($sichtbar ? $vorher : [], $modul->sichtbareWerte);
}
public static function sollwerte(): array
{
return [[10944, true], [10944, false], [-843, true], [-843, false], [0, true], [0, false]];
}
public function testWiederholtesVerwerfenLaesstNeuBerechneteLokaleSchutzleistungStehen(): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => 3000, 'SollwertGueltig' => false];
$modul->sichtbareWerte = $modul->zustand;
$modul->verwerfen();
self::assertSame(['Sollleistung' => 3000, 'SollwertGueltig' => false], $modul->zustand);
self::assertSame($modul->zustand, $modul->sichtbareWerte);
}
public function testAlleVerbraucherVerwerfenSollwerteUeberDieGemeinsameFunktion(): void
{
$anzahl = 0;
foreach (glob(__DIR__ . '/../*/module.php') as $datei) {
$quelle = (string) file_get_contents($datei);
if (!str_contains($quelle, 'implements VerbraucherSchnittstelle')) {
continue;
}
++$anzahl;
self::assertStringContainsString('use VerbraucherBasisTrait;', $quelle, $datei);
self::assertStringContainsString('$this->verwerfeSollwert();', $quelle, $datei);
self::assertStringNotContainsString("setzeZustand('SollwertGueltig', false)", $quelle, $datei);
self::assertStringNotContainsString("WriteAttributeBoolean('ZustandSollwertGueltig', false)", $quelle, $datei);
self::assertStringContainsString(
'$this->aktualisiereVariablen();' . "\n " . '$this->bereinigeUngueltigenSollwert();',
$quelle,
$datei
);
}
self::assertSame(7, $anzahl);
}
public function testGemeinsamePropertiesUndVariablenWerdenRegistriert(): void public function testGemeinsamePropertiesUndVariablenWerdenRegistriert(): void
{ {
$modul = new VerbraucherBasisDummy(); $modul = new VerbraucherBasisDummy();
@@ -50,6 +130,35 @@ final class VerbraucherBasisDummy
{ {
use VerbraucherBasisTrait; use VerbraucherBasisTrait;
/** @var array<string, int|bool> */
public array $zustand = [];
/** @var array<string, int|bool> */
public array $sichtbareWerte = [];
public function verwerfen(): void
{
$this->verwerfeSollwert();
}
public function bereinigen(): void
{
$this->bereinigeUngueltigenSollwert();
}
private function setzeZustand(string $ident, $wert): void
{
$this->zustand[$ident] = $wert;
if (array_key_exists($ident, $this->sichtbareWerte)) {
$this->sichtbareWerte[$ident] = $wert;
}
}
private function leseZustand(string $ident)
{
return $this->zustand[$ident];
}
/** @var array<string, mixed> */ /** @var array<string, mixed> */
public array $properties = []; public array $properties = [];