feat(manager): account for SDL power and SOC separately
Tests / test (push) Failing after 1m12s

Add opt-in SDL measurement sources and independent visualization toggles. Exclude balancing power from household telemetry and new household energy. Preserve custom views and cover measurement, migration and Symcon 8.0 behavior.
This commit is contained in:
dh
2026-10-06 13:35:17 +00:00
parent 58f91e4319
commit ebec355c96
12 changed files with 1243 additions and 35 deletions
+44
View File
@@ -835,6 +835,50 @@
"type": "ExpansionPanel",
"caption": "SDL / VGT",
"items": [
{
"type": "CheckBox",
"name": "SDLAktiv",
"caption": "SDL aktiv",
"onChange": "IPS_RequestAction($id, 'FormSDLAktiv', $SDLAktiv);"
},
{
"type": "ExpansionPanel",
"name": "SDLKonfiguration",
"caption": "SDL / Regelenergie Messung",
"visible": false,
"items": [
{
"type": "SelectVariable",
"name": "SDLLeistungVariableID",
"caption": "SDL Istleistung"
},
{
"type": "NumberSpinner",
"name": "SDLLeistungsfaktor",
"caption": "SDL Leistungsfaktor nach Watt",
"digits": 3
},
{
"type": "SelectVariable",
"name": "SDLSOCVariableID",
"caption": "SDL SOC in Prozent (optional)"
},
{
"type": "CheckBox",
"name": "SDLEnergieflussAnzeigen",
"caption": "SDL mit SOC im Energiefluss anzeigen"
},
{
"type": "CheckBox",
"name": "SDLDiagrammeAnzeigen",
"caption": "SDL mit SOC in den Diagrammen anzeigen"
},
{
"type": "Label",
"caption": "Nach Skalierung: positiv = Laden / Bezug, negativ = Entladen / Abgabe. SDL muss separat gemessen sein und darf nicht bereits in der normalen Batterieleistung enthalten sein. Die Hauslast fuer Prognosen wird unabhaengig von den Anzeigeoptionen korrigiert."
}
]
},
{
"type": "ValidationTextBox",
"name": "SDLAnschluss",
+84 -15
View File
@@ -11,6 +11,7 @@ require_once __DIR__ . '/../libs/Anlagentopologie.php';
require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
require_once __DIR__ . '/../libs/EnergieMessung.php';
require_once __DIR__ . '/../libs/ManagerEnergieTrait.php';
require_once __DIR__ . '/../libs/ManagerSDLTrait.php';
use Belevo\EnelixEMS\Anlagentopologie;
use Belevo\EnelixEMS\EinspeiseRegler;
@@ -25,6 +26,7 @@ use Belevo\EnelixEMS\StoerungsSnapshot;
class Manager extends IPSModule implements ManagerSchnittstelle
{
use ManagerEnergieTrait;
use \Belevo\EnelixEMS\ManagerSDLTrait;
private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201;
@@ -46,6 +48,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv',
'NetzleistungArchiv',
'BatterieleistungArchiv',
'SDLLeistungArchiv',
'SDLSOCArchiv',
];
private const ENERGIE_VARIABLEN = [
'PVEnergie',
@@ -54,6 +58,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'EinspeisungEnergie',
'BatterieLadenEnergie',
'BatterieEntladenEnergie',
'SDLLadenEnergie',
'SDLEntladenEnergie',
];
private const VERBRAUCHER_LIZENZEN = [
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
@@ -146,6 +152,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('PrognoseSendeintervall', 60);
$this->RegisterPropertyString('PrognoseAnschluss', '{}');
$this->RegisterPropertyString('SDLAnschluss', '{}');
$this->RegisterPropertyBoolean('SDLAktiv', false);
$this->RegisterPropertyInteger('SDLLeistungVariableID', 0);
$this->RegisterPropertyFloat('SDLLeistungsfaktor', 1.0);
$this->RegisterPropertyInteger('SDLSOCVariableID', 0);
$this->RegisterPropertyBoolean('SDLEnergieflussAnzeigen', false);
$this->RegisterPropertyBoolean('SDLDiagrammeAnzeigen', false);
$this->RegisterPropertyString('Lizenzcode', '');
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
$this->RegisterPropertyString('LizenzAnschluss', '{}');
@@ -210,6 +222,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeString('EnergiediagrammKonfiguration', '{}');
$this->RegisterAttributeString('EnergieflussKonfiguration', '{}');
$this->RegisterAttributeBoolean('EnergieflussNeuErzeugen', false);
$this->RegisterAttributeString('SDLMesskonfiguration', '');
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
$this->RegisterTimer('Regelzyklus', 0, '');
@@ -250,6 +263,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
try {
$this->pruefeKonfiguration();
$this->synchronisiereSDLMesskonfiguration();
$this->synchronisiereEnergievisualisierungen();
$this->konfiguriereEnergiearchiv();
} catch (Throwable $fehler) {
@@ -430,6 +444,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->monatsgrenzenFuerFormular()
);
$this->setzeFormularParameter(
$formular['elements'],
'SDLKonfiguration',
'visible',
$this->ReadPropertyBoolean('SDLAktiv')
);
return json_encode($formular, JSON_THROW_ON_ERROR);
}
@@ -501,6 +522,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
return;
case 'FormSDLAktiv':
$this->UpdateFormField('SDLKonfiguration', 'visible', (bool) $wert);
return;
case 'FormEnergyPieAnzeigen':
$this->synchronisiereEnergyPie((bool) $wert);
return;
@@ -1676,6 +1701,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv' => 'Hausverbrauch',
'NetzleistungArchiv' => 'Netzleistung Aufzeichnung',
'BatterieleistungArchiv' => 'Batterieleistung',
'SDLLeistungArchiv' => 'SDL Istleistung',
];
$energievariablen = [
'PVEnergie' => 'PV-Energie',
@@ -1684,6 +1710,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'EinspeisungEnergie' => 'Einspeisung Energie',
'BatterieLadenEnergie' => 'Batterie geladen',
'BatterieEntladenEnergie' => 'Batterie entladen',
'SDLLadenEnergie' => 'SDL geladen',
'SDLEntladenEnergie' => 'SDL entladen',
];
$position = 200;
foreach ($leistungsvariablen as $ident => $name) {
@@ -1697,6 +1725,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$position += 10;
}
$this->RegisterVariableFloat('SDLSOCArchiv', 'SDL Ladezustand', '~Intensity.100', 390);
IPS_SetHidden($this->GetIDForIdent('SDLSOCArchiv'), !$this->ReadPropertyBoolean('MesswerteAnzeigen'));
if ($this->ReadPropertyBoolean('SDLAktiv')) {
$this->RegisterVariableString('SDLStatus', 'SDL-Status', '', 120);
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->RegisterVariableBoolean(
'NetzleistungGueltig',
@@ -1755,6 +1789,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
if ($ident === 'SDLStatus' && $this->ReadPropertyBoolean('SDLAktiv')) {
continue;
}
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
$this->UnregisterVariable($ident);
@@ -2030,7 +2067,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
];
}
/** @return array{PV: float, Haus: float, Netz: float, Batterie: float} */
/** @return array{PV: float, Haus: float, Netz: float, Batterie: float, SDL: float} */
private function leseMessleistungen(): array
{
$pv = 0.0;
@@ -2043,12 +2080,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
$netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID'))
* $this->ReadPropertyFloat('Netzleistungsfaktor');
$sdl = $this->aktuelleSDLLeistung();
return [
'PV' => $pv,
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie),
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie, $sdl),
'Netz' => $netz,
'Batterie' => $batterie,
'SDL' => $sdl,
];
}
@@ -2073,7 +2112,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
/** @return array<string, float>|null */
private function leseEnergieZaehlerDeltas(): ?array
private function leseEnergieZaehlerDeltas(
float $sdlLaden = 0.0,
float $sdlEntladen = 0.0,
bool $hausVollstaendig = true
): ?array
{
$quellen = $this->energieZaehlerquellen();
if ($quellen === null) {
@@ -2102,6 +2145,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$delta = $stand - (float) $alt[$schluessel];
if ($delta >= 0.0) {
$deltas[$ident] += $delta;
} elseif ($this->ReadPropertyBoolean('SDLAktiv')) {
$hausVollstaendig = false;
}
}
}
@@ -2113,13 +2158,17 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return array_fill_keys(self::ENERGIE_VARIABLEN, 0.0);
}
$deltas['Hausenergie'] = EnergieMessung::hausenergie(
$deltas['SDLLadenEnergie'] = $sdlLaden;
$deltas['SDLEntladenEnergie'] = $sdlEntladen;
$deltas['Hausenergie'] = $hausVollstaendig ? EnergieMessung::hausenergie(
$deltas['PVEnergie'],
$deltas['NetzbezugEnergie'],
$deltas['EinspeisungEnergie'],
$deltas['BatterieLadenEnergie'],
$deltas['BatterieEntladenEnergie']
);
$deltas['BatterieEntladenEnergie'],
$sdlLaden,
$sdlEntladen
) : 0.0;
return $deltas;
}
@@ -2130,13 +2179,31 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return;
}
$jetzt = time();
$leistungen = $this->leseMessleistungen();
try {
$leistungen = $this->leseMessleistungen();
} catch (Throwable $fehler) {
// Keine falsche Hauslast aufzeichnen oder ueber die Messluecke integrieren.
$this->WriteAttributeInteger('MessungLetzterZeitstempel', 0);
$this->WriteAttributeString('MessungZaehlerstaende', '{}');
$this->setzeDiagnosewert('SDLStatus', 'Fehler: ' . $fehler->getMessage());
$this->protokolliere('Energiemessung', $fehler->getMessage());
return;
}
$letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel');
$puffer = $this->leseJsonAttribut('MessungEnergiePuffer');
$energieDeltas = $this->leseEnergieZaehlerDeltas();
$dauer = $letzterZeitstempel > 0 ? max(0, $jetzt - $letzterZeitstempel) : 0;
$sdlIntervallGueltig = $letzterZeitstempel > 0 && $dauer <= 300;
$integriert = EnergieMessung::energieDeltas(
$leistungen,
$this->ReadPropertyBoolean('SDLAktiv') && !$sdlIntervallGueltig ? 0 : min(300, $dauer)
);
$energieDeltas = $this->leseEnergieZaehlerDeltas(
$integriert['SDLLadenEnergie'],
$integriert['SDLEntladenEnergie'],
!$this->ReadPropertyBoolean('SDLAktiv') || $sdlIntervallGueltig
);
if ($energieDeltas === null && $letzterZeitstempel > 0) {
$sekunden = min(300, max(0, $jetzt - $letzterZeitstempel));
$energieDeltas = EnergieMessung::energieDeltas($leistungen, $sekunden);
$energieDeltas = $integriert;
}
foreach ($energieDeltas ?? [] as $ident => $delta) {
$puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta;
@@ -2155,10 +2222,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv' => 'Haus',
'NetzleistungArchiv' => 'Netz',
'BatterieleistungArchiv' => 'Batterie',
'SDLLeistungArchiv' => 'SDL',
] as $ident => $quelle) {
$this->SetValue($ident, $leistungen[$quelle]);
}
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt);
$soc = $this->aktuelleSDLSOC();
if ($soc !== null) {
$this->SetValue('SDLSOCArchiv', $soc);
}
}
$energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60;
@@ -2672,11 +2744,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'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');
}
$this->aktualisiereSDLStatus();
$this->setzeDiagnosewert(
'Stoerueberwachungsstatus',
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet'
@@ -2685,6 +2753,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function pruefeKonfiguration(): void
{
$this->pruefeSDLKonfiguration();
foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');