From cf2ce3aff40f5542b3fec70a42f988bdda39ba29 Mon Sep 17 00:00:00 2001 From: dh_Agent Date: Mon, 28 Sep 2026 17:47:22 +0000 Subject: [PATCH] feat(manager): Lizenzierung und Energietopologie verfeinern --- Manager/form.json | 122 ++++--- Manager/module.php | 374 +++++++++++++++++++--- docs/module/Manager/README.md | 51 +-- examples/Demoanlage/bootstrap.php | 4 +- libs/Anlagentopologie.php | 22 -- libs/EinspeiseRegler.php | 13 +- libs/EnergieMessung.php | 17 + tests/AnlagentopologieTest.php | 4 +- tests/DemoanlageBootstrapTest.php | 3 +- tests/EinspeiseReglerTest.php | 22 +- tests/EnergieMessungTest.php | 6 + tests/ManagerEnergieModulstrukturTest.php | 7 + tests/ManagerModulstrukturTest.php | 21 +- tests/Symcon/modules/Manager.php | 2 - 14 files changed, 517 insertions(+), 151 deletions(-) diff --git a/Manager/form.json b/Manager/form.json index f1fe230..851eaea 100644 --- a/Manager/form.json +++ b/Manager/form.json @@ -129,6 +129,7 @@ }, { "type": "Label", + "name": "PeakShavingTitel", "caption": "Peak Shaving am Netzanschlusspunkt" }, { @@ -144,19 +145,6 @@ "minimum": 0.0, "digits": 1 }, - { - "type": "NumberSpinner", - "name": "Einspeisetoleranz", - "caption": "Regeltoleranz", - "suffix": " W", - "minimum": 0.0, - "digits": 1 - }, - { - "type": "CheckBox", - "name": "NetzfahrplanAktiv", - "caption": "Intelligenten Netzfahrplan zur Vermeidung von Abregelung verwenden" - }, { "type": "CheckBox", "name": "AutomatischeSuche", @@ -241,6 +229,37 @@ "caption": "Anlagentopologie", "expanded": true, "items": [ + { + "type": "RowLayout", + "items": [ + { + "type": "SelectVariable", + "name": "NetzbezugEnergieVariableID", + "caption": "Netzbezugszaehler" + }, + { + "type": "NumberSpinner", + "name": "NetzbezugEnergiefaktor", + "caption": "Faktor", + "digits": 6 + }, + { + "type": "SelectVariable", + "name": "NetzeinspeisungEnergieVariableID", + "caption": "Einspeisezaehler" + }, + { + "type": "NumberSpinner", + "name": "NetzeinspeisungEnergiefaktor", + "caption": "Faktor", + "digits": 6 + } + ] + }, + { + "type": "Label", + "caption": "Energiezaehler werden in kWh erwartet; ueber den Faktor koennen beispielsweise Wh (0,001) umgerechnet werden." + }, { "type": "List", "name": "AnlagenWechselrichter", @@ -290,30 +309,6 @@ "digits": 3 } }, - { - "caption": "Einspeisegrenze", - "name": "ACEinspeisegrenze_kW", - "width": "150px", - "add": 0.0, - "edit": { - "type": "NumberSpinner", - "suffix": " kW", - "minimum": 0.0, - "digits": 3 - } - }, - { - "caption": "Bezugsgrenze", - "name": "ACBezugsgrenze_kW", - "width": "140px", - "add": 0.0, - "edit": { - "type": "NumberSpinner", - "suffix": " kW", - "minimum": 0.0, - "digits": 3 - } - }, { "caption": "Istleistung", "name": "IstleistungVariableID", @@ -328,6 +323,20 @@ "add": 1.0, "edit": {"type": "NumberSpinner", "digits": 4} }, + { + "caption": "Erzeugungsenergie", + "name": "ErzeugungsenergieVariableID", + "width": "160px", + "add": 0, + "edit": {"type": "SelectVariable"} + }, + { + "caption": "Energiefaktor", + "name": "Erzeugungsenergiefaktor", + "width": "110px", + "add": 1.0, + "edit": {"type": "NumberSpinner", "digits": 6} + }, { "caption": "PV-Begrenzung", "name": "BegrenzungVariableID", @@ -562,6 +571,27 @@ "width": "140px", "add": 0, "edit": {"type": "SelectVariable"} + }, + { + "caption": "Ladeenergie", + "name": "LadeenergieVariableID", + "width": "140px", + "add": 0, + "edit": {"type": "SelectVariable"} + }, + { + "caption": "Entladeenergie", + "name": "EntladeenergieVariableID", + "width": "150px", + "add": 0, + "edit": {"type": "SelectVariable"} + }, + { + "caption": "Energiefaktor", + "name": "Energiefaktor", + "width": "110px", + "add": 1.0, + "edit": {"type": "NumberSpinner", "digits": 6} } ] } @@ -719,14 +749,30 @@ "type": "ExpansionPanel", "caption": "Prognose / Forecast", "items": [ + { + "type": "CheckBox", + "name": "PrognoseAktiv", + "caption": "Prognosen aktivieren", + "onChange": "IPS_RequestAction($id, 'FormPrognoseAktiv', $PrognoseAktiv);" + }, + { + "type": "Label", + "name": "PrognoseLizenzInformation", + "caption": "Prognoselizenz wird geprueft." + }, { "type": "Label", "caption": "Forecast verwendet dieselben Messquellen aus der Anlagentopologie und den daraus berechneten Hausverbrauch." }, + { + "type": "CheckBox", + "name": "NetzfahrplanAktiv", + "caption": "Intelligenten Netzfahrplan zur Vermeidung von PV-Abregelung verwenden" + }, { "type": "NumberSpinner", "name": "PrognoseSendeintervall", - "caption": "Sendeintervall", + "caption": "Aktuelle Messwerte an den Prognosedienst senden alle", "minimum": 60, "maximum": 3600, "suffix": " Sekunden" diff --git a/Manager/module.php b/Manager/module.php index e62963e..ee2406b 100644 --- a/Manager/module.php +++ b/Manager/module.php @@ -130,11 +130,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->RegisterPropertyFloat('Umschaltdifferenz', 5.0); $this->RegisterPropertyBoolean('EinspeisebegrenzungAktiv', false); $this->RegisterPropertyFloat('Einspeisegrenze', 0.0); - $this->RegisterPropertyFloat('Einspeisetoleranz', 100.0); + $this->RegisterPropertyBoolean('PrognoseAktiv', false); $this->RegisterPropertyBoolean('NetzfahrplanAktiv', false); $this->RegisterPropertyString('AnlagenWechselrichter', '[]'); $this->RegisterPropertyString('AnlagenPVFlaechen', '[]'); $this->RegisterPropertyString('AnlagenBatterien', '[]'); + $this->RegisterPropertyInteger('NetzbezugEnergieVariableID', 0); + $this->RegisterPropertyFloat('NetzbezugEnergiefaktor', 1.0); + $this->RegisterPropertyInteger('NetzeinspeisungEnergieVariableID', 0); + $this->RegisterPropertyFloat('NetzeinspeisungEnergiefaktor', 1.0); // Verdeckte Altproperties; Messquellen liegen neu ausschliesslich in der Anlagentopologie. $this->RegisterPropertyInteger('PrognosePVVariableID', 0); $this->RegisterPropertyInteger('PrognoseHausverbrauchVariableID', 0); @@ -196,6 +200,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->RegisterAttributeInteger('MessungLetzteLeistungsaufzeichnung', 0); $this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0); $this->RegisterAttributeString('MessungEnergiePuffer', '{}'); + $this->RegisterAttributeString('MessungZaehlerstaende', '{}'); // Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren. $this->RegisterTimer('Regelzyklus', 0, ''); @@ -261,8 +266,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle } $this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen')); $this->aktualisiereAnschlussstatus(); - $geraetezugangFehlt = ($this->hatAnlagentopologie() - || $this->prognoseTelemetrieKonfiguriert() + $geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv') || $this->ReadPropertyBoolean('StoerueberwachungAktiv')) && $this->ReadAttributeString('PrognoseInstallationsToken') === ''; if (!$this->aktualisiereLizenzfreigabe( @@ -306,6 +310,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle $automatisch = $this->ReadPropertyBoolean('AutomatischeSuche'); $lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus'); $peakShavingLizenziert = $this->peakShavingLizenziert(); + $prognoseLizenziert = $this->prognoseLizenziert(); $netzfahrplanLizenziert = $this->berechtigungLizenziert( Lizenzpruefung::NETZFAHRPLAN ); @@ -352,19 +357,32 @@ class Manager extends IPSModule implements ManagerSchnittstelle 'options', $lastspitzenOptionen ); - foreach (['EinspeisebegrenzungAktiv', 'Einspeisegrenze', 'Einspeisetoleranz'] as $feld) { + foreach (['PeakShavingTitel', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze'] as $feld) { $this->setzeFormularParameter( $formular['elements'], $feld, - 'enabled', + 'visible', $peakShavingLizenziert ); } + $prognoseAktiv = $this->ReadPropertyBoolean('PrognoseAktiv'); + $this->setzeFormularParameter( + $formular['elements'], + 'PrognoseAktiv', + 'enabled', + $prognoseLizenziert || $prognoseAktiv + ); + $this->setzeFormularParameter( + $formular['elements'], + 'PrognoseLizenzInformation', + 'caption', + $this->prognoseLizenzStatustext($prognoseAktiv, $prognoseLizenziert) + ); $this->setzeFormularParameter( $formular['elements'], 'NetzfahrplanAktiv', 'enabled', - $peakShavingLizenziert && $netzfahrplanLizenziert + $prognoseAktiv && $peakShavingLizenziert && $netzfahrplanLizenziert ); $this->setzeFormularParameter( $formular['elements'], @@ -441,6 +459,27 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->zeichneMesswerteAuf(); return; + case 'FormPrognoseAktiv': + $aktiv = (bool) $wert; + $lizenziert = $this->prognoseLizenziert(); + $netzfahrplanLizenziert = $this->berechtigungLizenziert( + Lizenzpruefung::NETZFAHRPLAN + ); + $this->UpdateFormField( + 'PrognoseLizenzInformation', + 'caption', + $this->prognoseLizenzStatustext($aktiv, $lizenziert) + ); + $this->UpdateFormField( + 'NetzfahrplanAktiv', + 'enabled', + $aktiv && $netzfahrplanLizenziert && $this->peakShavingLizenziert() + ); + if (!$aktiv) { + $this->UpdateFormField('NetzfahrplanAktiv', 'value', false); + } + return; + case 'FormEnergyPieAnzeigen': $this->synchronisiereEnergyPie((bool) $wert); return; @@ -463,7 +502,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle } $this->aktualisiereLizenzfreigabe($wert, true); $this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert)); - $this->aktualisiereLastspitzenformular(); + $this->aktualisiereLastspitzenformular($wert); + $this->aktualisierePrognoseformular($wert); return; case 'FormAutomatischeSuche': @@ -737,7 +777,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle $ergebnis = EinspeiseRegler::verteile( $netzleistung, $this->ReadPropertyFloat('Einspeisegrenze'), - $this->ReadPropertyFloat('Einspeisetoleranz'), $wechselrichter, $letzteGrenzen ); @@ -1713,6 +1752,105 @@ class Manager extends IPSModule implements ManagerSchnittstelle return $quellen; } + /** + * @return array>|null + */ + private function energieZaehlerquellen(): ?array + { + $wechselrichter = $this->leseAnlagenKonfigurationsliste('AnlagenWechselrichter'); + $pvFlaechen = $this->leseAnlagenKonfigurationsliste('AnlagenPVFlaechen'); + $batterien = $this->leseAnlagenKonfigurationsliste('AnlagenBatterien'); + $pvWechselrichter = []; + foreach ($pvFlaechen as $pvFlaeche) { + $id = trim((string) ($pvFlaeche['WechselrichterID'] ?? '')); + if ($id !== '') { + $pvWechselrichter[$id] = true; + } + } + + $hatZaehler = $this->ReadPropertyInteger('NetzbezugEnergieVariableID') > 0 + || $this->ReadPropertyInteger('NetzeinspeisungEnergieVariableID') > 0; + foreach ($wechselrichter as $eintrag) { + $hatZaehler = $hatZaehler || (int) ($eintrag['ErzeugungsenergieVariableID'] ?? 0) > 0; + } + foreach ($batterien as $eintrag) { + $hatZaehler = $hatZaehler + || (int) ($eintrag['LadeenergieVariableID'] ?? 0) > 0 + || (int) ($eintrag['EntladeenergieVariableID'] ?? 0) > 0; + } + if (!$hatZaehler) { + return null; + } + + $quellen = [ + 'PVEnergie' => [], + 'NetzbezugEnergie' => [], + 'EinspeisungEnergie' => [], + 'BatterieLadenEnergie' => [], + 'BatterieEntladenEnergie' => [], + ]; + $netz = [ + 'NetzbezugEnergie' => [ + $this->ReadPropertyInteger('NetzbezugEnergieVariableID'), + $this->ReadPropertyFloat('NetzbezugEnergiefaktor'), + 'Netzbezug', + ], + 'EinspeisungEnergie' => [ + $this->ReadPropertyInteger('NetzeinspeisungEnergieVariableID'), + $this->ReadPropertyFloat('NetzeinspeisungEnergiefaktor'), + 'Netzeinspeisung', + ], + ]; + foreach ($netz as $ident => [$variableID, $faktor, $name]) { + $quellen[$ident][] = $this->energieZaehlerquelle( + $name, + $variableID, + $faktor, + strtolower($ident) + ); + } + + foreach ($wechselrichter as $index => $eintrag) { + $typ = (string) ($eintrag['Typ'] ?? ''); + $id = trim((string) ($eintrag['ID'] ?? '')); + if ($pvWechselrichter !== []) { + if (!isset($pvWechselrichter[$id])) { + continue; + } + } elseif (!in_array($typ, ['pv', 'hybrid'], true)) { + continue; + } + $quellen['PVEnergie'][] = $this->energieZaehlerquelle( + 'Wechselrichter ' . ($index + 1) . ': Erzeugungsenergie', + (int) ($eintrag['ErzeugungsenergieVariableID'] ?? 0), + $eintrag['Erzeugungsenergiefaktor'] ?? 1.0, + 'pv:' . $id + ); + } + if ($quellen['PVEnergie'] === []) { + throw new InvalidArgumentException('Mindestens ein PV-Erzeugungszaehler ist erforderlich.'); + } + + foreach ($batterien as $index => $eintrag) { + $id = trim((string) ($eintrag['ID'] ?? '')); + $faktor = $eintrag['Energiefaktor'] ?? 1.0; + $quellen['BatterieLadenEnergie'][] = $this->energieZaehlerquelle( + 'Batterie ' . ($index + 1) . ': Ladeenergie', + (int) ($eintrag['LadeenergieVariableID'] ?? 0), + $faktor, + 'batterie-laden:' . $id + ); + $quellen['BatterieEntladenEnergie'][] = $this->energieZaehlerquelle( + 'Batterie ' . ($index + 1) . ': Entladeenergie', + (int) ($eintrag['EntladeenergieVariableID'] ?? 0), + $faktor, + 'batterie-entladen:' . $id + ); + } + + return $quellen; + } + /** @return list> */ private function leseAnlagenKonfigurationsliste(string $property): array { @@ -1738,6 +1876,37 @@ class Manager extends IPSModule implements ManagerSchnittstelle return $faktor; } + /** + * @param mixed $faktor + * @return array{Schluessel: string, VariableID: int, Faktor: float} + */ + private function energieZaehlerquelle( + string $bezeichnung, + int $variableID, + $faktor, + string $schluessel + ): array { + if (!$this->istNumerischeVariable($variableID)) { + throw new InvalidArgumentException( + $bezeichnung . ' muss eine numerische Variable sein.' + ); + } + if ((!is_int($faktor) && !is_float($faktor)) + || !is_finite((float) $faktor) + || (float) $faktor <= 0.0 + ) { + throw new InvalidArgumentException( + $bezeichnung . ': Energiefaktor muss groesser als 0 sein.' + ); + } + + return [ + 'Schluessel' => $schluessel, + 'VariableID' => $variableID, + 'Faktor' => (float) $faktor, + ]; + } + /** @return array{PV: float, Haus: float, Netz: float, Batterie: float} */ private function leseMessleistungen(): array { @@ -1780,6 +1949,58 @@ class Manager extends IPSModule implements ManagerSchnittstelle return $gewicht > 0.0 ? $summe / $gewicht : null; } + /** @return array|null */ + private function leseEnergieZaehlerDeltas(): ?array + { + $quellen = $this->energieZaehlerquellen(); + if ($quellen === null) { + return null; + } + + $alt = $this->leseJsonAttribut('MessungZaehlerstaende'); + $neu = []; + $deltas = []; + $vollstaendigeBasis = true; + foreach ($quellen as $ident => $identQuellen) { + $deltas[$ident] = 0.0; + foreach ($identQuellen as $quelle) { + $schluessel = $quelle['Schluessel']; + $stand = (float) GetValue($quelle['VariableID']) * $quelle['Faktor']; + if (!is_finite($stand) || $stand < 0.0) { + throw new RuntimeException( + 'Energiezaehler ' . $schluessel . ' liefert keinen gueltigen Stand.' + ); + } + $neu[$schluessel] = $stand; + if (!isset($alt[$schluessel]) || !is_numeric($alt[$schluessel])) { + $vollstaendigeBasis = false; + continue; + } + $delta = $stand - (float) $alt[$schluessel]; + if ($delta >= 0.0) { + $deltas[$ident] += $delta; + } + } + } + $this->WriteAttributeString( + 'MessungZaehlerstaende', + json_encode($neu, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION) + ); + if (!$vollstaendigeBasis) { + return array_fill_keys(self::ENERGIE_VARIABLEN, 0.0); + } + + $deltas['Hausenergie'] = EnergieMessung::hausenergie( + $deltas['PVEnergie'], + $deltas['NetzbezugEnergie'], + $deltas['EinspeisungEnergie'], + $deltas['BatterieLadenEnergie'], + $deltas['BatterieEntladenEnergie'] + ); + + return $deltas; + } + private function zeichneMesswerteAuf(): void { if (!$this->messungKonfiguriert()) { @@ -1789,11 +2010,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle $leistungen = $this->leseMessleistungen(); $letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel'); $puffer = $this->leseJsonAttribut('MessungEnergiePuffer'); - if ($letzterZeitstempel > 0) { + $energieDeltas = $this->leseEnergieZaehlerDeltas(); + if ($energieDeltas === null && $letzterZeitstempel > 0) { $sekunden = min(300, max(0, $jetzt - $letzterZeitstempel)); - foreach (EnergieMessung::energieDeltas($leistungen, $sekunden) as $ident => $delta) { - $puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta; - } + $energieDeltas = EnergieMessung::energieDeltas($leistungen, $sekunden); + } + foreach ($energieDeltas ?? [] as $ident => $delta) { + $puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta; } $this->WriteAttributeInteger('MessungLetzterZeitstempel', $jetzt); $this->WriteAttributeString( @@ -1899,8 +2122,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle && $this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus); $gueltig = $leaseGueltig && $berechtigungenPassen; $berechtigungenVeraltet = $basisGueltig && !$berechtigungenPassen; - $geraetezugangFehlt = ($this->hatAnlagentopologie() - || $this->prognoseTelemetrieKonfiguriert() + $geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv') || $this->ReadPropertyBoolean('StoerueberwachungAktiv')) && $this->ReadAttributeString('PrognoseInstallationsToken') === ''; $routineFaellig = (!$gueltig && !$berechtigungenVeraltet) @@ -2022,15 +2244,36 @@ class Manager extends IPSModule implements ManagerSchnittstelle return $this->ReadPropertyBoolean('EinspeisebegrenzungAktiv') ? 1 : 0; } - private function peakShavingLizenziert(): bool + private function peakShavingLizenziert(?string $lizenzcode = null): bool { - return $this->berechtigungLizenziert(Lizenzpruefung::MANAGER_PEAK); + return $this->berechtigungLizenziert(Lizenzpruefung::MANAGER_PEAK, $lizenzcode); } - private function berechtigungLizenziert(string $kennung): bool + private function prognoseLizenziert(?string $lizenzcode = null): bool + { + return $this->berechtigungLizenziert(Lizenzpruefung::NETZFAHRPLAN, $lizenzcode) + || ($this->berechtigungLizenziert(Lizenzpruefung::PROGNOSE_PV, $lizenzcode) + && $this->berechtigungLizenziert( + Lizenzpruefung::PROGNOSE_VERBRAUCH, + $lizenzcode + )); + } + + /** @param array $lease */ + private function prognoseBerechtigungInLease(array $lease): bool + { + return Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::NETZFAHRPLAN) > 0 + || (Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::PROGNOSE_PV) > 0 + && Lizenzpruefung::berechtigungsMenge( + $lease, + Lizenzpruefung::PROGNOSE_VERBRAUCH + ) > 0); + } + + private function berechtigungLizenziert(string $kennung, ?string $lizenzcode = null): bool { $lizenzcode = Lizenzpruefung::normalisiereCode( - $this->ReadPropertyString('Lizenzcode') + $lizenzcode ?? $this->ReadPropertyString('Lizenzcode') ); if (!Lizenzpruefung::istLizenzcodeGueltig($lizenzcode)) { return false; @@ -2068,6 +2311,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle ) { return false; } + if ($this->ReadPropertyBoolean('PrognoseAktiv') + && !$this->prognoseBerechtigungInLease($lease) + ) { + return false; + } if ($this->ReadPropertyBoolean('NetzfahrplanAktiv') && Lizenzpruefung::berechtigungsMenge( $lease, @@ -2108,9 +2356,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle return $optionen; } - private function aktualisiereLastspitzenformular(): void + private function aktualisiereLastspitzenformular(?string $lizenzcode = null): void { - $lizenziert = $this->peakShavingLizenziert(); + $lizenziert = $this->peakShavingLizenziert($lizenzcode); $modus = $lizenziert ? $this->ReadPropertyInteger('Lastspitzenmodus') : 0; $this->UpdateFormField( 'Lastspitzenmodus', @@ -2118,6 +2366,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle json_encode($this->lastspitzenOptionen($lizenziert), JSON_THROW_ON_ERROR) ); $this->UpdateFormField('Lastspitzenmodus', 'value', $modus); + foreach (['PeakShavingTitel', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze'] as $feld) { + $this->UpdateFormField($feld, 'visible', $lizenziert); + } $this->UpdateFormField( 'Lastspitzengrenze', 'visible', @@ -2134,6 +2385,43 @@ class Manager extends IPSModule implements ManagerSchnittstelle } } + private function aktualisierePrognoseformular(?string $lizenzcode = null): void + { + $aktiv = $this->ReadPropertyBoolean('PrognoseAktiv'); + $lizenziert = $this->prognoseLizenziert($lizenzcode); + $netzfahrplanLizenziert = $this->berechtigungLizenziert( + Lizenzpruefung::NETZFAHRPLAN, + $lizenzcode + ); + $this->UpdateFormField('PrognoseAktiv', 'enabled', $lizenziert || $aktiv); + $this->UpdateFormField( + 'PrognoseLizenzInformation', + 'caption', + $this->prognoseLizenzStatustext($aktiv, $lizenziert) + ); + $this->UpdateFormField( + 'NetzfahrplanAktiv', + 'enabled', + $aktiv && $netzfahrplanLizenziert + && $this->peakShavingLizenziert($lizenzcode) + ); + } + + private function prognoseLizenzStatustext(bool $aktiv, bool $lizenziert): string + { + $managerID = $this->ReadAttributeString('LizenzInstallationID'); + if (!$lizenziert) { + return 'Prognosen sind deaktiviert. Fuer die Manager-ID ' + . $managerID + . ' fehlen forecast_pv plus forecast_load oder grid_schedule.'; + } + + return ($aktiv ? 'Prognosen sind aktiviert. ' : 'Prognosen sind deaktiviert. ') + . 'Die passende Lizenz fuer die Manager-ID ' + . $managerID + . ' ist vorhanden.'; + } + private function lizenzStatustext(?string $lizenzcode = null): string { $code = Lizenzpruefung::normalisiereCode( @@ -2184,8 +2472,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle $nutzlast = json_encode([ 'code' => $lizenzcode, 'installationId' => $installationID, - 'issueDeviceToken' => ($this->hatAnlagentopologie() - || $this->prognoseTelemetrieKonfiguriert() + 'issueDeviceToken' => ($this->ReadPropertyBoolean('PrognoseAktiv') || $this->ReadPropertyBoolean('StoerueberwachungAktiv')) && $this->ReadAttributeString('PrognoseInstallationsToken') === '', ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); @@ -2256,10 +2543,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle private function aktualisiereAnschlussstatus(): void { - foreach ([ - 'PrognoseAnschluss' => 'Prognosestatus', - 'SDLAnschluss' => 'SDLStatus', - ] as $property => $variable) { + $this->setzeDiagnosewert( + 'Prognosestatus', + $this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet' + ); + foreach (['SDLAnschluss' => 'SDLStatus'] as $property => $variable) { $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'); @@ -2306,16 +2594,22 @@ class Manager extends IPSModule implements ManagerSchnittstelle ) { throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 liegen.'); } - if ($this->ReadPropertyFloat('Einspeisegrenze') < 0.0 - || $this->ReadPropertyFloat('Einspeisetoleranz') < 0.0 - ) { - throw new InvalidArgumentException('Einspeisegrenze und Einspeisetoleranz duerfen nicht negativ sein.'); + if ($this->ReadPropertyFloat('Einspeisegrenze') < 0.0) { + throw new InvalidArgumentException('Einspeisegrenze darf nicht negativ sein.'); } if ($this->ReadPropertyBoolean('EinspeisebegrenzungAktiv')) { $this->regelbareWechselrichter(); } + if ($this->ReadPropertyBoolean('PrognoseAktiv') + && !$this->prognoseTelemetrieKonfiguriert() + ) { + throw new InvalidArgumentException( + 'Aktive Prognosen benoetigen Netzleistung, PV-Leistung und Batterie-Ladezustand.' + ); + } if ($this->ReadPropertyBoolean('NetzfahrplanAktiv') - && (!$this->ReadPropertyBoolean('EinspeisebegrenzungAktiv') + && (!$this->ReadPropertyBoolean('PrognoseAktiv') + || !$this->ReadPropertyBoolean('EinspeisebegrenzungAktiv') || !$this->prognoseTelemetrieKonfiguriert()) ) { throw new InvalidArgumentException( @@ -2329,6 +2623,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->pvMessquellen(); $this->batterieMessquellen(); $this->batterieSocQuellen(); + $this->energieZaehlerquellen(); foreach (['PrognoseAnschluss', 'SDLAnschluss'] as $property) { $wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR); if (!is_array($wert)) { @@ -2348,18 +2643,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle ); } - private function hatAnlagentopologie(): bool - { - $topologie = $this->leseAnlagentopologie(); - - return $topologie['inverters'] !== [] - || $topologie['pvArrays'] !== [] - || $topologie['batteries'] !== []; - } - private function prognoseTelemetrieKonfiguriert(): bool { - return $this->messquellenKonfiguriert() + return $this->ReadPropertyBoolean('PrognoseAktiv') + && $this->messquellenKonfiguriert() && $this->aktuelleBatterieSoc() !== null; } @@ -2407,6 +2694,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle private function synchronisiereAnlagentopologie(): void { + if (!$this->ReadPropertyBoolean('PrognoseAktiv')) { + $this->setzeDiagnosewert('Prognosestatus', 'NichtVerwendet'); + return; + } + try { $topologie = $this->leseAnlagentopologie(); if ($topologie['inverters'] === [] diff --git a/docs/module/Manager/README.md b/docs/module/Manager/README.md index 30e4eac..dbb8876 100644 --- a/docs/module/Manager/README.md +++ b/docs/module/Manager/README.md @@ -62,12 +62,15 @@ Die drei Regelungsvariablen `Aktiv`, `Betriebsart` und `Netzleistung` sind immer | `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. | | `EinspeisebegrenzungAktiv` | Boolean | `false`; gemeinsame Exportgrenze am Netzanschlusspunkt aktivieren. | | `Einspeisegrenze` | Float | Maximale Einspeisung der Gesamtanlage in W; `0` bedeutet Nulleinspeisung. | -| `Einspeisetoleranz` | Float | `100` W; Totband gegen pendelnde Wechselrichtervorgaben. | +| `PrognoseAktiv` | Boolean | `false`; Prognosetelemetrie und Topologiesynchronisation mit `forecast_pv` plus `forecast_load` oder `grid_schedule` aktivieren. | | `NetzfahrplanAktiv` | Boolean | `false`; lizenzierten, tarif- und prognosebasierten Netzzielwert verwenden. | -| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Grenzen, Istleistungsmessung und optionalem Begrenzungsregister. | +| `NetzbezugEnergieVariableID` | Integer | Optionaler kumulativer Netzbezugszaehler. | +| `NetzeinspeisungEnergieVariableID` | Integer | Optionaler kumulativer Einspeisezaehler. | +| `NetzbezugEnergiefaktor`, `NetzeinspeisungEnergiefaktor` | Float | Umrechnung der Netzenergiezaehler nach kWh. | +| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Nennleistung, Leistungs- und Erzeugungsenergiemessung sowie optionalem Begrenzungsregister. | | `AnlagenPVFlaechen` | String/JSON | `[]`; PV-Flaechen mit DC-Leistung, Ausrichtung und Wechselrichter-ID. | -| `AnlagenBatterien` | String/JSON | `[]`; Batteriespeicher mit Kapazitaeten, Kopplung, Leistungs- und SOC-Messung. | -| `PrognoseSendeintervall` | Integer | `60` s; zulaessig sind 60 bis 3600 Sekunden. | +| `AnlagenBatterien` | String/JSON | `[]`; Batteriespeicher mit Kapazitaeten, Kopplung, Leistung, SOC sowie Lade- und Entladeenergiezaehlern. | +| `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. | | `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. | | `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. | | `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. | @@ -99,9 +102,14 @@ konfiguriert sind, tastet der Manager die Quellen minuetlich ab. PV-Leistung ist die Summe der den PV-Flaechen zugeordneten Wechselrichter. Batterieleistung und der kapazitaetsgewichtete SOC stammen aus den Batteriespeichern. Positive Batterieleistung bedeutet Laden, negative Entladen. Der Hausverbrauch wird als -`PV + Netz - Batterie` bilanziert. Dieselben Werte versorgen ohne zweite -Eingabe auch die Prognosetelemetrie. Ausfallluecken ueber fuenf Minuten werden -nicht nachberechnet. +`PV + Netz - Batterie` bilanziert. Sind Netzbezugs-, Einspeise-, PV-Erzeugungs- +sowie gegebenenfalls Batterie-Lade- und Entladezaehler vollstaendig hinterlegt, +verwendet der Manager deren Deltas fuer die Energieaufzeichnung. Die Hausenergie +wird dann als `PV + Netzbezug + Batterieentladung - Einspeisung - Batterieladung` +berechnet. Ohne vollstaendigen Zaehlerdatensatz integriert der Manager weiterhin +die Leistungswerte. Dieselben Quellen versorgen ohne zweite Eingabe auch die +Prognosetelemetrie. Ausfallluecken ueber fuenf Minuten werden bei der +Leistungsintegration nicht nachberechnet. Batteriespeicher bleiben als physische Komponenten samt Messquellen in der Anlagentopologie. Ein zusaetzliches Batterieverbrauchermodul beschreibt dagegen @@ -136,9 +144,10 @@ absolute Leistung in Watt ausgegeben werden. Der Manager berechnet eine einzige Grenze fuer die Gesamtanlage. Beim Abregeln verteilt er sie nach der tatsaechlichen Erzeugung, beim Freigeben nach der -AC-Nennleistung auf alle angebotenen Wechselrichter. Das Totband verhindert -Pendeln am Grenzwert. Beim Abschalten des Managers oder der -Funktion werden zuvor gesetzte Grenzen kontrolliert auf die jeweilige +AC-Nennleistung auf alle angebotenen Wechselrichter. Jede gemessene Abweichung +wird stufenlos nachgefuehrt; ein zusaetzliches Toleranzfenster wird nicht +verwendet. Beim Abschalten des Managers oder der Funktion werden zuvor gesetzte +Grenzen kontrolliert auf die jeweilige Nennleistung zurueckgesetzt. Wechselrichter ohne beide Register werden nicht geregelt; sobald eines der beiden Register gesetzt ist, muessen beide gueltig sein und das Begrenzungsregister eine IP-Symcon-Aktion besitzen. @@ -157,8 +166,10 @@ davon aktiv. Der Manager ist die fuehrende Quelle fuer die technischen Stammdaten. Die Konfiguration trennt Wechselrichter, PV-Flaechen und Batteriespeicher. AC-Leistung -wird am Wechselrichter, DC-Leistung an der PV-Flaeche gepflegt. Modulanzahl und -Modulleistung sind optional und muessen gemeinsam gesetzt werden. +wird am Wechselrichter, DC-Leistung an der PV-Flaeche gepflegt. Individuelle +AC-Einspeise- und Bezugsgrenzen werden nicht erfasst, weil die Begrenzung +anlagenweit am Netzanschlusspunkt erfolgt. Modulanzahl und Modulleistung sind +optional und muessen gemeinsam gesetzt werden. PV-Flaechen und Batterien referenzieren ihren Wechselrichter ueber dessen stabile ID. Bei `hybrid` koennen beide denselben Wechselrichter verwenden. Der Export @@ -167,9 +178,10 @@ AC-Grenze aus, damit PV und Batterie die Nennleistung nicht unabhaengig doppelt beanspruchen. Der JSON-Export enthaelt die Vertragsversion `1.0`, die Lizenz-Installations-ID, -die drei Komponentenlisten, gemeinsame AC-Grenzen und aggregierte Summen fuer -die schrittweise Prognoseanbindung. Tarif- und Variantenparameter bleiben im -Prognoseportal. Eine nichtleere Topologie wird beim Speichern automatisch ueber +die drei Komponentenlisten, gemeinsame AC-Nennleistungen und aggregierte Summen +fuer die schrittweise Prognoseanbindung. Interne Symcon-Variablen-IDs werden +nicht exportiert. Tarif- und Variantenparameter bleiben im Prognoseportal. Bei +aktivierter Prognose wird eine nichtleere Topologie beim Speichern automatisch ueber einen separaten, widerrufbaren Installationszugang synchronisiert. Der Lizenzserver liefert diesen Zugang nur auf ausdrueckliche Geraeteanforderung; der Manager entfernt ihn vor dem Speichern aus der Lease und haelt ihn in einem @@ -177,9 +189,12 @@ internen Attribut. Der Lizenzcode wird nicht als API-Token verwendet. Ein Synchronisationsfehler erscheint im `Prognosestatus`, blockiert die lokale EMS-Regelung aber nicht. -Sind PV-Leistung, Hausverbrauch und Batterie-SOC gemeinsam konfiguriert, sendet -der Manager diese Werte zusammen mit der faktorbereinigten Netzleistung und -einem UTC-Zeitstempel im eingestellten Intervall. Das Minimum von 60 Sekunden +Der Forecast-Schalter zeigt direkt an, ob fuer die Manager-ID eine passende +Kombination `forecast_pv` plus `forecast_load` oder `grid_schedule` vorhanden ist. Nach der Aktivierung sendet der +Manager PV-Leistung, berechneten Hausverbrauch, Netzleistung und Batterie-SOC +mit UTC-Zeitstempel im eingestellten Sendeintervall. Dieses Intervall bestimmt +also, wie oft aktuelle Messwerte zum Prognosedienst hochgeladen werden; es ist +kein Regelintervall. Das Minimum von 60 Sekunden passt zum Rate-Limit des Lizenzportals. Die Uebertragung nutzt denselben widerrufbaren Installationszugang wie die Topologie; Lizenzcode und Geraet Token werden weder als Telemetriefelder noch im Debug-Log ausgegeben. Bei HTTP 401 diff --git a/examples/Demoanlage/bootstrap.php b/examples/Demoanlage/bootstrap.php index 1f8e7fb..c9c8497 100644 --- a/examples/Demoanlage/bootstrap.php +++ b/examples/Demoanlage/bootstrap.php @@ -285,8 +285,6 @@ function enelixDemoInstall(): array 'Name' => 'Demo-Hybridwechselrichter', 'Typ' => 'hybrid', 'ACNennleistung_kW' => $pvNennleistungKw, - 'ACEinspeisegrenze_kW' => 0.0, - 'ACBezugsgrenze_kW' => 0.0, 'IstleistungVariableID' => $inverterActual, 'Istleistungsfaktor' => 1.0, 'BegrenzungVariableID' => $inverterLimit, @@ -338,7 +336,7 @@ function enelixDemoInstall(): array 'Umschaltdifferenz' => 0.0, 'EinspeisebegrenzungAktiv' => true, 'Einspeisegrenze' => 3000.0, - 'Einspeisetoleranz' => 100.0, + 'PrognoseAktiv' => false, 'NetzfahrplanAktiv' => false, 'AnlagenWechselrichter' => json_encode( $wechselrichterKonfiguration, diff --git a/libs/Anlagentopologie.php b/libs/Anlagentopologie.php index 43b2019..627eb7c 100644 --- a/libs/Anlagentopologie.php +++ b/libs/Anlagentopologie.php @@ -47,16 +47,6 @@ final class Anlagentopologie 'name' => self::name($eintrag['Name'] ?? null, $id), 'type' => $typ, 'acRatedKw' => $acRatedKw, - 'acExportLimitKw' => self::optionaleGrenze( - $eintrag['ACEinspeisegrenze_kW'] ?? null, - $acRatedKw, - $pfad . ': AC-Einspeisegrenze' - ), - 'acImportLimitKw' => self::optionaleGrenze( - $eintrag['ACBezugsgrenze_kW'] ?? null, - $acRatedKw, - $pfad . ': AC-Bezugsgrenze' - ), ]; $inverters[] = $inverter; $inverterById[$id] = $inverter; @@ -304,18 +294,6 @@ final class Anlagentopologie return (float) $wert; } - private static function optionaleGrenze($wert, float $nennleistung, string $feld): ?float - { - $grenze = self::optionalePositiveZahl($wert, $feld); - if ($grenze !== null && $grenze > $nennleistung) { - throw new InvalidArgumentException( - $feld . ' darf die AC-Nennleistung nicht uebersteigen.' - ); - } - - return $grenze; - } - private static function optionalerText($wert): ?string { if ($wert === null || $wert === '') { diff --git a/libs/EinspeiseRegler.php b/libs/EinspeiseRegler.php index d4c7577..ebacb0d 100644 --- a/libs/EinspeiseRegler.php +++ b/libs/EinspeiseRegler.php @@ -26,16 +26,12 @@ final class EinspeiseRegler public static function verteile( float $netzleistung, float $einspeisegrenze, - float $toleranz, array $wechselrichter, array $letzteGrenzen = [] ): array { if ($einspeisegrenze < 0.0) { throw new InvalidArgumentException('Einspeisegrenze darf nicht negativ sein.'); } - if ($toleranz < 0.0) { - throw new InvalidArgumentException('Einspeisetoleranz darf nicht negativ sein.'); - } if ($wechselrichter === []) { throw new InvalidArgumentException('Mindestens ein regelbarer Wechselrichter ist erforderlich.'); } @@ -81,12 +77,11 @@ final class EinspeiseRegler $netzabweichung = $netzleistung + $einspeisegrenze; $zielGesamt = $bisherigeGrenzeGesamt; $nachIstleistungVerteilen = false; - if ($netzabweichung < -$toleranz) { + if ($netzabweichung < 0.0) { $nachIstleistungVerteilen = $istleistungGesamt > 0.0; - $wirksameAbweichung = $netzabweichung + $toleranz; - $zielGesamt = min($bisherigeGrenzeGesamt, $istleistungGesamt) + $wirksameAbweichung; - } elseif ($netzabweichung > $toleranz) { - $zielGesamt = $bisherigeGrenzeGesamt + $netzabweichung - $toleranz; + $zielGesamt = min($bisherigeGrenzeGesamt, $istleistungGesamt) + $netzabweichung; + } elseif ($netzabweichung > 0.0) { + $zielGesamt = $bisherigeGrenzeGesamt + $netzabweichung; } $zielGesamt = max(0.0, min($zielGesamt, $nennleistungGesamt)); diff --git a/libs/EnergieMessung.php b/libs/EnergieMessung.php index 0eb31f2..cd9d009 100644 --- a/libs/EnergieMessung.php +++ b/libs/EnergieMessung.php @@ -16,6 +16,23 @@ final class EnergieMessung ); } + public static function hausenergie( + float $pv, + float $netzbezug, + float $netzeinspeisung, + float $batterieLaden, + float $batterieEntladen + ): float { + return max( + 0.0, + self::endlich($pv) + + self::endlich($netzbezug) + + self::endlich($batterieEntladen) + - self::endlich($netzeinspeisung) + - self::endlich($batterieLaden) + ); + } + /** * @param array{PV: float, Haus: float, Netz: float, Batterie: float} $leistungen * @return array{ diff --git a/tests/AnlagentopologieTest.php b/tests/AnlagentopologieTest.php index eb04132..b9a4471 100644 --- a/tests/AnlagentopologieTest.php +++ b/tests/AnlagentopologieTest.php @@ -18,8 +18,6 @@ final class AnlagentopologieTest extends TestCase 'Name' => 'Hybrid Ost', 'Typ' => 'hybrid', 'ACNennleistung_kW' => 10.0, - 'ACEinspeisegrenze_kW' => 8.0, - 'ACBezugsgrenze_kW' => 0.0, ]], JSON_THROW_ON_ERROR), json_encode([ [ @@ -62,6 +60,8 @@ final class AnlagentopologieTest extends TestCase self::assertSame(12.8, $topologie['summary']['pvDcKwp']); self::assertSame(10.0, $topologie['summary']['pvAcKw']); self::assertSame(12.8, $topologie['summary']['usableBatteryKwh']); + self::assertArrayNotHasKey('acExportLimitKw', $topologie['inverters'][0]); + self::assertArrayNotHasKey('acImportLimitKw', $topologie['inverters'][0]); self::assertSame([[ 'inverterId' => 'hybrid-1', 'acRatedKw' => 10.0, diff --git a/tests/DemoanlageBootstrapTest.php b/tests/DemoanlageBootstrapTest.php index 7bc252d..983a6ff 100644 --- a/tests/DemoanlageBootstrapTest.php +++ b/tests/DemoanlageBootstrapTest.php @@ -131,8 +131,9 @@ final class DemoanlageBootstrapTest extends TestCase self::assertStringContainsString("'Begrenzungsart' => 'watt'", $source); self::assertStringContainsString("'EinspeisebegrenzungAktiv' => true", $source); self::assertStringContainsString("'Einspeisegrenze' => 3000.0", $source); - self::assertStringContainsString("'Einspeisetoleranz' => 100.0", $source); + self::assertStringContainsString("'PrognoseAktiv' => false", $source); self::assertStringContainsString("'NetzfahrplanAktiv' => false", $source); + self::assertStringNotContainsString("'Einspeisetoleranz'", $source); self::assertStringContainsString("'demo-hybrid-1'", $source); self::assertStringContainsString("'demo-pv-1'", $source); self::assertStringContainsString("'demo-battery-1'", $source); diff --git a/tests/EinspeiseReglerTest.php b/tests/EinspeiseReglerTest.php index 33bf2df..4cada29 100644 --- a/tests/EinspeiseReglerTest.php +++ b/tests/EinspeiseReglerTest.php @@ -15,17 +15,16 @@ final class EinspeiseReglerTest extends TestCase $ergebnis = EinspeiseRegler::verteile( -15000.0, 10000.0, - 100.0, [ ['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 10000], ['ID' => 'wr-2', 'Nennleistung_W' => 5000, 'Istleistung_W' => 5000], ] ); - self::assertSame(10100.0, $ergebnis['ZielGesamtleistung_W']); - self::assertSame(4900.0, $ergebnis['Abregelbedarf_W']); - self::assertSame(6733.333, $ergebnis['Grenzen_W']['wr-1']); - self::assertSame(3366.667, $ergebnis['Grenzen_W']['wr-2']); + self::assertSame(10000.0, $ergebnis['ZielGesamtleistung_W']); + self::assertSame(5000.0, $ergebnis['Abregelbedarf_W']); + self::assertSame(6666.667, $ergebnis['Grenzen_W']['wr-1']); + self::assertSame(3333.333, $ergebnis['Grenzen_W']['wr-2']); } public function testAbregelungFolgtDerTatsaechlichenErzeugung(): void @@ -33,7 +32,6 @@ final class EinspeiseReglerTest extends TestCase $ergebnis = EinspeiseRegler::verteile( -10000.0, 5000.0, - 0.0, [ ['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 10000], ['ID' => 'wr-2', 'Nennleistung_W' => 10000, 'Istleistung_W' => 0], @@ -49,7 +47,6 @@ final class EinspeiseReglerTest extends TestCase $ergebnis = EinspeiseRegler::verteile( -6000.0, 10000.0, - 100.0, [ ['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 4000], ['ID' => 'wr-2', 'Nennleistung_W' => 10000, 'Istleistung_W' => 2000], @@ -57,22 +54,21 @@ final class EinspeiseReglerTest extends TestCase ['wr-1' => 5000, 'wr-2' => 5000] ); - self::assertSame(13900.0, $ergebnis['ZielGesamtleistung_W']); - self::assertSame(6950.0, $ergebnis['Grenzen_W']['wr-1']); - self::assertSame(6950.0, $ergebnis['Grenzen_W']['wr-2']); + self::assertSame(14000.0, $ergebnis['ZielGesamtleistung_W']); + self::assertSame(7000.0, $ergebnis['Grenzen_W']['wr-1']); + self::assertSame(7000.0, $ergebnis['Grenzen_W']['wr-2']); } - public function testToleranzVerhindertPendelnAmGrenzwert(): void + public function testRegelungFolgtAuchKleinenAbweichungenStufenlos(): void { $ergebnis = EinspeiseRegler::verteile( -10050.0, 10000.0, - 100.0, [['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 9000]], ['wr-1' => 8000] ); - self::assertSame(8000.0, $ergebnis['ZielGesamtleistung_W']); + self::assertSame(7950.0, $ergebnis['ZielGesamtleistung_W']); } public function testStellwerteWerdenInProzentOderWattAusgegeben(): void diff --git a/tests/EnergieMessungTest.php b/tests/EnergieMessungTest.php index 8d6d4af..6844758 100644 --- a/tests/EnergieMessungTest.php +++ b/tests/EnergieMessungTest.php @@ -50,6 +50,12 @@ final class EnergieMessungTest extends TestCase self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0)); } + public function testBerechnetHausenergieAusDenZaehlerdeltas(): void + { + self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0)); + self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 5.0, 0.0, 0.0)); + } + public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void { $werte = EnergieMessung::energieDeltas([ diff --git a/tests/ManagerEnergieModulstrukturTest.php b/tests/ManagerEnergieModulstrukturTest.php index 5163730..4c245e4 100644 --- a/tests/ManagerEnergieModulstrukturTest.php +++ b/tests/ManagerEnergieModulstrukturTest.php @@ -52,6 +52,11 @@ final class ManagerEnergieModulstrukturTest extends TestCase 'IstleistungVariableID', 'LeistungVariableID', 'SOCVariableID', + 'NetzbezugEnergieVariableID', + 'NetzeinspeisungEnergieVariableID', + 'ErzeugungsenergieVariableID', + 'LadeenergieVariableID', + 'EntladeenergieVariableID', ] as $erwartet) { self::assertStringContainsString($erwartet, $formular); } @@ -78,6 +83,8 @@ final class ManagerEnergieModulstrukturTest extends TestCase self::assertStringContainsString("RegisterTimer( 'MesswerteAufzeichnen'", $modul); self::assertStringContainsString('EnergieMessung::energieDeltas', $modul); + self::assertStringContainsString('EnergieMessung::hausenergie', $modul); + self::assertStringContainsString("RegisterAttributeString('MessungZaehlerstaende'", $modul); } public function testFunFactsSindResponsivUndErklaerenUmrechnungen(): void diff --git a/tests/ManagerModulstrukturTest.php b/tests/ManagerModulstrukturTest.php index bb2adba..fcdd65a 100644 --- a/tests/ManagerModulstrukturTest.php +++ b/tests/ManagerModulstrukturTest.php @@ -50,9 +50,14 @@ final class ManagerModulstrukturTest extends TestCase 'Umschaltdifferenz', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze', - 'Einspeisetoleranz', + 'PrognoseAktiv', + 'PrognoseLizenzInformation', 'NetzfahrplanAktiv', + 'NetzbezugEnergieVariableID', + 'NetzeinspeisungEnergieVariableID', 'IstleistungVariableID', + 'ErzeugungsenergieVariableID', + 'Erzeugungsenergiefaktor', 'BegrenzungVariableID', 'Begrenzungsart', 'DiagnosevariablenAnzeigen', @@ -70,6 +75,9 @@ final class ManagerModulstrukturTest extends TestCase 'LeistungVariableID', 'Leistungsfaktor', 'SOCVariableID', + 'LadeenergieVariableID', + 'EntladeenergieVariableID', + 'Energiefaktor', 'EnergyPieAnzeigen', 'EnergiediagrammeAnzeigen', 'FunFactsAnzeigen', @@ -81,7 +89,8 @@ final class ManagerModulstrukturTest extends TestCase self::assertStringContainsString($property, $json); } foreach (['Regelintervall', 'Vorgabeintervall', 'UmschaltschwelleUeberschuss', - 'UmschaltschwelleLastspitze', 'Optionale obere Anschluesse', 'LizenzAnschluss'] as $entfernt + 'UmschaltschwelleLastspitze', 'Optionale obere Anschluesse', 'LizenzAnschluss', + 'Einspeisetoleranz', 'ACEinspeisegrenze_kW', 'ACBezugsgrenze_kW'] as $entfernt ) { self::assertStringNotContainsString($entfernt, $json); } @@ -294,8 +303,16 @@ final class ManagerModulstrukturTest extends TestCase $inhalt ); self::assertStringContainsString('Lizenzpruefung::NETZFAHRPLAN', $inhalt); + self::assertStringContainsString('Lizenzpruefung::PROGNOSE_PV', $inhalt); + self::assertStringContainsString('Lizenzpruefung::PROGNOSE_VERBRAUCH', $inhalt); + self::assertStringContainsString('prognoseLizenzStatustext(', $inhalt); + self::assertStringContainsString( + "foreach (['PeakShavingTitel', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze']", + $inhalt + ); self::assertStringContainsString('aktuelleNetzfahrplanVorgabe()', $inhalt); self::assertStringContainsString('gibEinspeisebegrenzungFrei()', $inhalt); + self::assertStringNotContainsString("RegisterPropertyFloat('Einspeisetoleranz'", $inhalt); } public function testManagerVerwendetDieNeuenZeitdefaults(): void diff --git a/tests/Symcon/modules/Manager.php b/tests/Symcon/modules/Manager.php index 5b44a81..e4b0e6a 100644 --- a/tests/Symcon/modules/Manager.php +++ b/tests/Symcon/modules/Manager.php @@ -80,8 +80,6 @@ return static function (TestContext $test): void { 'Name' => 'Hybridwechselrichter', 'Typ' => 'hybrid', 'ACNennleistung_kW' => 10.0, - 'ACEinspeisegrenze_kW' => 8.0, - 'ACBezugsgrenze_kW' => 0.0, 'IstleistungVariableID' => $pvLeistungId, 'Istleistungsfaktor' => 1.0, ]];