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'] === []