feat(manager): Lizenzierung und Energietopologie verfeinern
Tests / test (push) Successful in 46s

This commit is contained in:
dh
2026-09-28 17:47:22 +00:00
parent 708a8ecea2
commit cf2ce3aff4
14 changed files with 517 additions and 151 deletions
+84 -38
View File
@@ -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"
+333 -41
View File
@@ -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<string, list<array{Schluessel: string, VariableID: int, Faktor: float}>>|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<array<string, mixed>> */
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<string, float>|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<string, mixed> $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'] === []
+33 -18
View File
@@ -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
+1 -3
View File
@@ -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,
-22
View File
@@ -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 === '') {
+4 -9
View File
@@ -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));
+17
View File
@@ -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{
+2 -2
View File
@@ -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,
+2 -1
View File
@@ -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);
+9 -13
View File
@@ -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
+6
View File
@@ -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([
@@ -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
+19 -2
View File
@@ -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
-2
View File
@@ -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,
]];