feat(manager): Energieaufzeichnung und Visualisierungen integrieren
Tests / test (push) Successful in 47s

This commit is contained in:
dh
2026-09-27 19:29:37 +00:00
parent 40f41a2fb8
commit 708a8ecea2
11 changed files with 1426 additions and 56 deletions
+126 -13
View File
@@ -321,6 +321,13 @@
"add": 0,
"edit": {"type": "SelectVariable"}
},
{
"caption": "Messfaktor",
"name": "Istleistungsfaktor",
"width": "110px",
"add": 1.0,
"edit": {"type": "NumberSpinner", "digits": 4}
},
{
"caption": "PV-Begrenzung",
"name": "BegrenzungVariableID",
@@ -534,6 +541,27 @@
{"caption": "Hybrid", "value": "hybrid"}
]
}
},
{
"caption": "Leistung",
"name": "LeistungVariableID",
"width": "140px",
"add": 0,
"edit": {"type": "SelectVariable"}
},
{
"caption": "Messfaktor",
"name": "Leistungsfaktor",
"width": "110px",
"add": 1.0,
"edit": {"type": "NumberSpinner", "digits": 4}
},
{
"caption": "Ladezustand",
"name": "SOCVariableID",
"width": "140px",
"add": 0,
"edit": {"type": "SelectVariable"}
}
]
}
@@ -591,24 +619,109 @@
}
]
},
{
"type": "ExpansionPanel",
"caption": "Energieaufzeichnung",
"expanded": true,
"items": [
{
"type": "CheckBox",
"name": "EnergieaufzeichnungAktiv",
"caption": "Leistungs- und Energiewerte automatisch aufzeichnen"
},
{
"type": "Label",
"caption": "PV und Batterie werden aus der Anlagentopologie summiert. Der Hausverbrauch wird aus PV + Netz - Batterie berechnet. Netz: positiv Bezug; Batterie: positiv Laden."
},
{
"type": "CheckBox",
"name": "MesswerteAnzeigen",
"caption": "Einzelne Leistungs- und Energiewerte unter dem Manager anzeigen"
},
{
"type": "RowLayout",
"items": [
{
"type": "Select",
"name": "LeistungsaufzeichnungMinuten",
"caption": "Leistung verdichten",
"options": [
{"caption": "1 Minute", "value": 1},
{"caption": "5 Minuten", "value": 5},
{"caption": "1 Stunde", "value": 60}
]
},
{
"type": "NumberSpinner",
"name": "LeistungLoeschenMonate",
"caption": "Leistung loeschen nach",
"minimum": 0,
"suffix": " Monaten (0 = nie)"
}
]
},
{
"type": "RowLayout",
"items": [
{
"type": "Select",
"name": "EnergieaufzeichnungMinuten",
"caption": "Energie verdichten",
"options": [
{"caption": "1 Minute", "value": 1},
{"caption": "5 Minuten", "value": 5},
{"caption": "1 Stunde", "value": 60}
]
},
{
"type": "NumberSpinner",
"name": "EnergieVerdichtenMonate",
"caption": "Auf Tageswerte verdichten nach",
"minimum": 0,
"suffix": " Monaten"
},
{
"type": "NumberSpinner",
"name": "EnergieLoeschenMonate",
"caption": "Energie loeschen nach",
"minimum": 0,
"suffix": " Monaten (0 = nie)"
}
]
},
{
"type": "CheckBox",
"name": "EnergyPieAnzeigen",
"caption": "Energy Pie unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormEnergyPieAnzeigen', $EnergyPieAnzeigen);"
},
{
"type": "CheckBox",
"name": "EnergiediagrammeAnzeigen",
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormEnergiediagrammeAnzeigen', $EnergiediagrammeAnzeigen);"
},
{
"type": "CheckBox",
"name": "FunFactsAnzeigen",
"caption": "Energy Facts unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormFunFactsAnzeigen', $FunFactsAnzeigen);"
},
{
"type": "CheckBox",
"name": "EnergieflussAnzeigen",
"caption": "Energiefluss unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);"
}
]
},
{
"type": "ExpansionPanel",
"caption": "Prognose / Forecast",
"items": [
{
"type": "SelectVariable",
"name": "PrognosePVVariableID",
"caption": "PV-Leistung"
},
{
"type": "SelectVariable",
"name": "PrognoseHausverbrauchVariableID",
"caption": "Hausverbrauch"
},
{
"type": "SelectVariable",
"name": "PrognoseSOCVariableID",
"caption": "Batterie-Ladezustand"
"type": "Label",
"caption": "Forecast verwendet dieselben Messquellen aus der Anlagentopologie und den daraus berechneten Hausverbrauch."
},
{
"type": "NumberSpinner",
+378 -27
View File
@@ -9,9 +9,13 @@ require_once __DIR__ . '/../libs/EinspeiseRegler.php';
require_once __DIR__ . '/../libs/Lizenzpruefung.php';
require_once __DIR__ . '/../libs/Anlagentopologie.php';
require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
require_once __DIR__ . '/../libs/EnergieMessung.php';
require_once __DIR__ . '/../libs/ManagerEnergieTrait.php';
use Belevo\EnelixEMS\Anlagentopologie;
use Belevo\EnelixEMS\EinspeiseRegler;
use Belevo\EnelixEMS\EnergieMessung;
use Belevo\EnelixEMS\ManagerEnergieTrait;
use Belevo\EnelixEMS\Lizenzpruefung;
use Belevo\EnelixEMS\ManagerRegler;
use Belevo\EnelixEMS\ManagerSchnittstelle;
@@ -20,6 +24,8 @@ use Belevo\EnelixEMS\StoerungsSnapshot;
class Manager extends IPSModule implements ManagerSchnittstelle
{
use ManagerEnergieTrait;
private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201;
private const STATUS_KONFIGURATION_UNGUELTIG = 202;
@@ -32,6 +38,23 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private const LIZENZ_RETRY_SEKUNDEN = 3600;
private const STOERUNGS_RETRY_MAX_SEKUNDEN = 900;
private const VM_UPDATE = 10603;
private const ARCHIVE_MODULE_ID = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
private const ENERGY_PIE_MODULE_ID = '{F73B8F8D-5E36-4B23-B379-4632A517A1D6}';
private const ENERGY_FLOW_MODULE_ID = '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}';
private const LEISTUNGS_VARIABLEN = [
'PVLeistungArchiv',
'HausverbrauchLeistungArchiv',
'NetzleistungArchiv',
'BatterieleistungArchiv',
];
private const ENERGIE_VARIABLEN = [
'PVEnergie',
'Hausenergie',
'NetzbezugEnergie',
'EinspeisungEnergie',
'BatterieLadenEnergie',
'BatterieEntladenEnergie',
];
private const VERBRAUCHER_LIZENZEN = [
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
'Katalog' => 'boiler_multi',
@@ -112,6 +135,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyString('AnlagenWechselrichter', '[]');
$this->RegisterPropertyString('AnlagenPVFlaechen', '[]');
$this->RegisterPropertyString('AnlagenBatterien', '[]');
// Verdeckte Altproperties; Messquellen liegen neu ausschliesslich in der Anlagentopologie.
$this->RegisterPropertyInteger('PrognosePVVariableID', 0);
$this->RegisterPropertyInteger('PrognoseHausverbrauchVariableID', 0);
$this->RegisterPropertyInteger('PrognoseSOCVariableID', 0);
@@ -127,6 +151,24 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('StoerungsSendeintervall', 300);
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
$this->RegisterPropertyBoolean('LoggingEin', false);
$this->RegisterPropertyBoolean('EnergieaufzeichnungAktiv', true);
// Verdeckte Altproperties; bestehende Konfigurationen bleiben lesbar.
$this->RegisterPropertyInteger('MessungPVLeistungVariableID', 0);
$this->RegisterPropertyInteger('MessungHausverbrauchLeistungVariableID', 0);
$this->RegisterPropertyInteger('MessungBatterieleistungVariableID', 0);
$this->RegisterPropertyFloat('MessungPVLeistungsfaktor', 1.0);
$this->RegisterPropertyFloat('MessungHausverbrauchLeistungsfaktor', 1.0);
$this->RegisterPropertyFloat('MessungBatterieleistungsfaktor', 1.0);
$this->RegisterPropertyBoolean('MesswerteAnzeigen', false);
$this->RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1);
$this->RegisterPropertyInteger('LeistungLoeschenMonate', 12);
$this->RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5);
$this->RegisterPropertyInteger('EnergieVerdichtenMonate', 12);
$this->RegisterPropertyInteger('EnergieLoeschenMonate', 0);
$this->RegisterPropertyBoolean('EnergyPieAnzeigen', false);
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false);
$this->RegisterPropertyBoolean('FunFactsAnzeigen', false);
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
$this->EnableAction('Aktiv');
@@ -150,6 +192,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeInteger('StoerungsLetzterErfolg', 0);
$this->RegisterAttributeInteger('StoerungsNaechsterVersuch', 0);
$this->RegisterAttributeInteger('StoerungsFehlversuche', 0);
$this->RegisterAttributeInteger('MessungLetzterZeitstempel', 0);
$this->RegisterAttributeInteger('MessungLetzteLeistungsaufzeichnung', 0);
$this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0);
$this->RegisterAttributeString('MessungEnergiePuffer', '{}');
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
$this->RegisterTimer('Regelzyklus', 0, '');
@@ -169,6 +215,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'StoerungenSenden', true);"
);
$this->RegisterTimer(
'MesswerteAufzeichnen',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'MesswerteAufzeichnen', true);"
);
}
public function ApplyChanges(): void
@@ -180,6 +231,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
try {
$this->pruefeKonfiguration();
$this->konfiguriereEnergiearchiv();
$this->synchronisiereEnergievisualisierungen();
} catch (Throwable $fehler) {
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->SetTimerInterval('Regelzyklus', 0);
@@ -187,6 +240,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->SetTimerInterval('KeepAlive', 0);
$this->SetTimerInterval('PrognoseSenden', 0);
$this->SetTimerInterval('StoerungenSenden', 0);
$this->SetTimerInterval('MesswerteAufzeichnen', 0);
$this->protokolliere('Konfiguration', $fehler->getMessage());
return;
}
@@ -200,6 +254,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'StoerungenSenden',
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 30000 : 0
);
$messungAktiv = $this->messungKonfiguriert();
$this->SetTimerInterval('MesswerteAufzeichnen', $messungAktiv ? 60000 : 0);
if ($messungAktiv) {
$this->zeichneMesswerteAuf();
}
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
$this->aktualisiereAnschlussstatus();
$geraetezugangFehlt = ($this->hatAnlagentopologie()
@@ -378,6 +437,26 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->sendeStoerungsSnapshot(false);
return;
case 'MesswerteAufzeichnen':
$this->zeichneMesswerteAuf();
return;
case 'FormEnergyPieAnzeigen':
$this->synchronisiereEnergyPie((bool) $wert);
return;
case 'FormEnergiediagrammeAnzeigen':
$this->synchronisiereEnergiediagramme((bool) $wert);
return;
case 'FormFunFactsAnzeigen':
$this->synchronisiereFunFacts((bool) $wert);
return;
case 'FormEnergieflussAnzeigen':
$this->synchronisiereEnergiefluss((bool) $wert);
return;
case 'FormLizenzPruefen':
if (!is_string($wert)) {
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
@@ -1430,6 +1509,32 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function aktualisiereVariablen(): void
{
$leistungsvariablen = [
'PVLeistungArchiv' => 'PV-Leistung',
'HausverbrauchLeistungArchiv' => 'Hausverbrauch',
'NetzleistungArchiv' => 'Netzleistung Aufzeichnung',
'BatterieleistungArchiv' => 'Batterieleistung',
];
$energievariablen = [
'PVEnergie' => 'PV-Energie',
'Hausenergie' => 'Hausenergie',
'NetzbezugEnergie' => 'Netzbezug Energie',
'EinspeisungEnergie' => 'Einspeisung Energie',
'BatterieLadenEnergie' => 'Batterie geladen',
'BatterieEntladenEnergie' => 'Batterie entladen',
];
$position = 200;
foreach ($leistungsvariablen as $ident => $name) {
$this->RegisterVariableFloat($ident, $name, '~Power', $position);
IPS_SetHidden($this->GetIDForIdent($ident), !$this->ReadPropertyBoolean('MesswerteAnzeigen'));
$position += 10;
}
foreach ($energievariablen as $ident => $name) {
$this->RegisterVariableFloat($ident, $name, '~Electricity', $position);
IPS_SetHidden($this->GetIDForIdent($ident), !$this->ReadPropertyBoolean('MesswerteAnzeigen'));
$position += 10;
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->RegisterVariableBoolean(
'NetzleistungGueltig',
@@ -1495,6 +1600,249 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
}
private function messungKonfiguriert(): bool
{
return $this->ReadPropertyBoolean('EnergieaufzeichnungAktiv')
&& $this->messquellenKonfiguriert();
}
private function messquellenKonfiguriert(): bool
{
try {
return $this->istNumerischeVariable($this->ReadPropertyInteger('NetzleistungVariableID'))
&& $this->pvMessquellen() !== [];
} catch (Throwable $fehler) {
return false;
}
}
/** @return list<array{VariableID: int, Faktor: float}> */
private function pvMessquellen(): array
{
$wechselrichter = $this->leseAnlagenKonfigurationsliste('AnlagenWechselrichter');
$pvFlaechen = $this->leseAnlagenKonfigurationsliste('AnlagenPVFlaechen');
$verwendeteWechselrichter = [];
foreach ($pvFlaechen as $pvFlaeche) {
$id = trim((string) ($pvFlaeche['WechselrichterID'] ?? ''));
if ($id !== '') {
$verwendeteWechselrichter[$id] = true;
}
}
$quellen = [];
foreach ($wechselrichter as $index => $eintrag) {
$typ = (string) ($eintrag['Typ'] ?? '');
$id = trim((string) ($eintrag['ID'] ?? ''));
if ($verwendeteWechselrichter !== []) {
if (!isset($verwendeteWechselrichter[$id])) {
continue;
}
} elseif (!in_array($typ, ['pv', 'hybrid'], true)) {
continue;
}
$variableID = $eintrag['IstleistungVariableID'] ?? 0;
if ($variableID === 0) {
continue;
}
if (!is_int($variableID) || !$this->istNumerischeVariable($variableID)) {
throw new InvalidArgumentException(
'Wechselrichter ' . ($index + 1) . ': Istleistung muss eine numerische Variable sein.'
);
}
$quellen[] = [
'VariableID' => $variableID,
'Faktor' => $this->anlagenMessfaktor(
$eintrag['Istleistungsfaktor'] ?? 1.0,
'Wechselrichter ' . ($index + 1)
),
];
}
return $quellen;
}
/** @return list<array{VariableID: int, Faktor: float}> */
private function batterieMessquellen(): array
{
$quellen = [];
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenBatterien') as $index => $eintrag) {
$variableID = $eintrag['LeistungVariableID'] ?? 0;
if ($variableID === 0) {
continue;
}
if (!is_int($variableID) || !$this->istNumerischeVariable($variableID)) {
throw new InvalidArgumentException(
'Batterie ' . ($index + 1) . ': Leistung muss eine numerische Variable sein.'
);
}
$quellen[] = [
'VariableID' => $variableID,
'Faktor' => $this->anlagenMessfaktor(
$eintrag['Leistungsfaktor'] ?? 1.0,
'Batterie ' . ($index + 1)
),
];
}
return $quellen;
}
/** @return list<array{VariableID: int, Gewicht: float}> */
private function batterieSocQuellen(): array
{
$quellen = [];
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenBatterien') as $index => $eintrag) {
$variableID = $eintrag['SOCVariableID'] ?? 0;
if ($variableID === 0) {
continue;
}
if (!is_int($variableID) || !$this->istNumerischeVariable($variableID)) {
throw new InvalidArgumentException(
'Batterie ' . ($index + 1) . ': SOC muss eine numerische Variable sein.'
);
}
$kapazitaet = $eintrag['Nutzkapazitaet_kWh'] ?? 0.0;
$quellen[] = [
'VariableID' => $variableID,
'Gewicht' => is_numeric($kapazitaet) && (float) $kapazitaet > 0.0
? (float) $kapazitaet
: 1.0,
];
}
return $quellen;
}
/** @return list<array<string, mixed>> */
private function leseAnlagenKonfigurationsliste(string $property): array
{
$liste = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($liste) || !array_is_list($liste)) {
throw new InvalidArgumentException($property . ' muss eine JSON-Liste sein.');
}
return array_values(array_filter($liste, 'is_array'));
}
/** @param mixed $wert */
private function anlagenMessfaktor($wert, string $bezeichnung): float
{
if (!is_int($wert) && !is_float($wert)) {
throw new InvalidArgumentException($bezeichnung . ': Messfaktor muss numerisch sein.');
}
$faktor = (float) $wert;
if (!is_finite($faktor) || $faktor === 0.0) {
throw new InvalidArgumentException($bezeichnung . ': Messfaktor muss endlich und ungleich 0 sein.');
}
return $faktor;
}
/** @return array{PV: float, Haus: float, Netz: float, Batterie: float} */
private function leseMessleistungen(): array
{
$pv = 0.0;
foreach ($this->pvMessquellen() as $quelle) {
$pv += max(0.0, (float) GetValue($quelle['VariableID']) * $quelle['Faktor']);
}
$batterie = 0.0;
foreach ($this->batterieMessquellen() as $quelle) {
$batterie += (float) GetValue($quelle['VariableID']) * $quelle['Faktor'];
}
$netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID'))
* $this->ReadPropertyFloat('Netzleistungsfaktor');
return [
'PV' => $pv,
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie),
'Netz' => $netz,
'Batterie' => $batterie,
];
}
private function aktuelleBatterieSoc(): ?float
{
$quellen = $this->batterieSocQuellen();
if ($quellen === []) {
return $this->leseAnlagenKonfigurationsliste('AnlagenBatterien') === [] ? 0.0 : null;
}
$summe = 0.0;
$gewicht = 0.0;
foreach ($quellen as $quelle) {
$wert = (float) GetValue($quelle['VariableID']);
if (!is_finite($wert) || $wert < 0.0 || $wert > 100.0) {
return null;
}
$summe += $wert * $quelle['Gewicht'];
$gewicht += $quelle['Gewicht'];
}
return $gewicht > 0.0 ? $summe / $gewicht : null;
}
private function zeichneMesswerteAuf(): void
{
if (!$this->messungKonfiguriert()) {
return;
}
$jetzt = time();
$leistungen = $this->leseMessleistungen();
$letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel');
$puffer = $this->leseJsonAttribut('MessungEnergiePuffer');
if ($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;
}
}
$this->WriteAttributeInteger('MessungLetzterZeitstempel', $jetzt);
$this->WriteAttributeString(
'MessungEnergiePuffer',
json_encode($puffer, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION)
);
$leistungsintervall = $this->ReadPropertyInteger('LeistungsaufzeichnungMinuten') * 60;
$letzteLeistung = $this->ReadAttributeInteger('MessungLetzteLeistungsaufzeichnung');
if ($letzteLeistung === 0 || $jetzt - $letzteLeistung >= $leistungsintervall) {
foreach ([
'PVLeistungArchiv' => 'PV',
'HausverbrauchLeistungArchiv' => 'Haus',
'NetzleistungArchiv' => 'Netz',
'BatterieleistungArchiv' => 'Batterie',
] as $ident => $quelle) {
$this->SetValue($ident, $leistungen[$quelle]);
}
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt);
}
$energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60;
$letzteEnergie = $this->ReadAttributeInteger('MessungLetzteEnergieaufzeichnung');
if ($letzteEnergie === 0) {
$this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt);
} elseif ($jetzt - $letzteEnergie >= $energieintervall) {
foreach (self::ENERGIE_VARIABLEN as $ident) {
$this->SetValue($ident, (float) $this->GetValue($ident) + (float) ($puffer[$ident] ?? 0.0));
}
$this->WriteAttributeString('MessungEnergiePuffer', '{}');
$this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt);
}
$funFactsID = @IPS_GetObjectIDByIdent('EnergieFunFacts', $this->InstanceID);
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
$this->aktualisiereFunFacts($funFactsID);
}
}
private function istNumerischeVariable(int $variableID): bool
{
if ($variableID <= 0 || !IPS_VariableExists($variableID)) {
return false;
}
$variable = IPS_GetVariable($variableID);
return in_array((int) ($variable['VariableType'] ?? -1), [1, 2], true);
}
/** @param bool|float|int|string $wert */
private function setzeDiagnosewert(string $ident, $wert): void
{
@@ -1929,6 +2277,22 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
}
}
foreach (['LeistungsaufzeichnungMinuten', 'EnergieaufzeichnungMinuten'] as $property) {
$wert = $this->ReadPropertyInteger($property);
if (!in_array($wert, [1, 5, 60], true)) {
throw new InvalidArgumentException($property . ' muss 1, 5 oder 60 Minuten betragen.');
}
}
foreach (['LeistungLoeschenMonate', 'EnergieVerdichtenMonate', 'EnergieLoeschenMonate'] as $property) {
if ($this->ReadPropertyInteger($property) < 0) {
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
}
}
$energieLoeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate');
$energieVerdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate');
if ($energieLoeschen > 0 && $energieLoeschen <= $energieVerdichten) {
throw new InvalidArgumentException('Energie darf erst nach der taeglichen Verdichtung geloescht werden.');
}
$intervall = $this->ReadPropertyInteger('PrognoseSendeintervall');
if ($intervall < 60 || $intervall > 3600) {
throw new InvalidArgumentException('PrognoseSendeintervall muss zwischen 60 und 3600 Sekunden liegen.');
@@ -1937,25 +2301,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if ($stoerungsIntervall < 60 || $stoerungsIntervall > 3600) {
throw new InvalidArgumentException('StoerungsSendeintervall muss zwischen 60 und 3600 Sekunden liegen.');
}
$telemetrieIDs = [
$this->ReadPropertyInteger('PrognosePVVariableID'),
$this->ReadPropertyInteger('PrognoseHausverbrauchVariableID'),
$this->ReadPropertyInteger('PrognoseSOCVariableID'),
];
$konfigurierteIDs = array_values(array_filter($telemetrieIDs, static fn (int $id): bool => $id > 0));
if ($konfigurierteIDs !== [] && count($konfigurierteIDs) !== count($telemetrieIDs)) {
throw new InvalidArgumentException('Fuer die Prognose muessen PV, Hausverbrauch und SOC gemeinsam konfiguriert sein.');
}
foreach ($konfigurierteIDs as $variableID) {
if (!IPS_VariableExists($variableID)) {
throw new InvalidArgumentException('Eine konfigurierte Prognosevariable existiert nicht.');
}
}
if ($konfigurierteIDs !== []
&& !IPS_VariableExists($this->ReadPropertyInteger('NetzleistungVariableID'))
) {
throw new InvalidArgumentException('Die Netzleistungsvariable fuer die Prognose existiert nicht.');
}
if ($this->ReadPropertyFloat('Umschaltdifferenz') < 0
|| $this->ReadPropertyFloat('Umschaltdifferenz') > 100
) {
@@ -1981,6 +2326,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
$this->leseAnlagentopologie();
$this->pvMessquellen();
$this->batterieMessquellen();
$this->batterieSocQuellen();
foreach (['PrognoseAnschluss', 'SDLAnschluss'] as $property) {
$wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($wert)) {
@@ -2011,9 +2359,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function prognoseTelemetrieKonfiguriert(): bool
{
return $this->ReadPropertyInteger('PrognosePVVariableID') > 0
&& $this->ReadPropertyInteger('PrognoseHausverbrauchVariableID') > 0
&& $this->ReadPropertyInteger('PrognoseSOCVariableID') > 0;
return $this->messquellenKonfiguriert()
&& $this->aktuelleBatterieSoc() !== null;
}
private function aktuelleNetzfahrplanVorgabe(): ?float
@@ -2158,11 +2505,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.');
}
$leistungen = $this->leseMessleistungen();
$soc = $this->aktuelleBatterieSoc();
if ($soc === null) {
throw new RuntimeException('Der Batterie-Ladezustand ist nicht verfuegbar.');
}
$werte = [
'PV' => GetValue($this->ReadPropertyInteger('PrognosePVVariableID')),
'Hausverbrauch' => GetValue($this->ReadPropertyInteger('PrognoseHausverbrauchVariableID')),
'Netzleistung' => GetValue($this->ReadPropertyInteger('NetzleistungVariableID')),
'SOC' => GetValue($this->ReadPropertyInteger('PrognoseSOCVariableID')),
'PV' => $leistungen['PV'],
'Hausverbrauch' => $leistungen['Haus'],
'Netzleistung' => $leistungen['Netz'],
'SOC' => $soc,
];
foreach ($werte as $name => $wert) {
if (!is_int($wert) && !is_float($wert)) {
@@ -2173,7 +2525,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new RuntimeException('Der Prognosewert ' . $name . ' ist nicht endlich.');
}
}
$werte['Netzleistung'] *= $this->ReadPropertyFloat('Netzleistungsfaktor');
if ($werte['PV'] < 0.0 || $werte['Hausverbrauch'] < 0.0
|| $werte['SOC'] < 0.0 || $werte['SOC'] > 100.0
) {