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
+14 -5
View File
@@ -8,11 +8,11 @@ final class EnergieMessung
{
private const WATTSEKUNDEN_PRO_KWH = 3600000.0;
public static function hausleistung(float $pv, float $netz, float $batterie): float
public static function hausleistung(float $pv, float $netz, float $batterie, float $sdl = 0.0): float
{
return max(
0.0,
self::endlich($pv) + self::endlich($netz) - self::endlich($batterie)
self::endlich($pv) + self::endlich($netz) - self::endlich($batterie) - self::endlich($sdl)
);
}
@@ -21,7 +21,9 @@ final class EnergieMessung
float $netzbezug,
float $netzeinspeisung,
float $batterieLaden,
float $batterieEntladen
float $batterieEntladen,
float $sdlLaden = 0.0,
float $sdlEntladen = 0.0
): float {
return max(
0.0,
@@ -30,18 +32,22 @@ final class EnergieMessung
+ self::endlich($batterieEntladen)
- self::endlich($netzeinspeisung)
- self::endlich($batterieLaden)
- self::endlich($sdlLaden)
+ self::endlich($sdlEntladen)
);
}
/**
* @param array{PV: float, Haus: float, Netz: float, Batterie: float} $leistungen
* @param array{PV: float, Haus: float, Netz: float, Batterie: float, SDL?: float} $leistungen
* @return array{
* PVEnergie: float,
* Hausenergie: float,
* NetzbezugEnergie: float,
* EinspeisungEnergie: float,
* BatterieLadenEnergie: float,
* BatterieEntladenEnergie: float
* BatterieEntladenEnergie: float,
* SDLLadenEnergie: float,
* SDLEntladenEnergie: float
* }
*/
public static function energieDeltas(array $leistungen, int $sekunden): array
@@ -51,6 +57,7 @@ final class EnergieMessung
$haus = self::endlich($leistungen['Haus']);
$netz = self::endlich($leistungen['Netz']);
$batterie = self::endlich($leistungen['Batterie']);
$sdl = self::endlich($leistungen['SDL'] ?? 0.0);
return [
'PVEnergie' => max(0.0, $pv) * $faktor,
@@ -59,6 +66,8 @@ final class EnergieMessung
'EinspeisungEnergie' => max(0.0, -$netz) * $faktor,
'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor,
'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor,
'SDLLadenEnergie' => max(0.0, $sdl) * $faktor,
'SDLEntladenEnergie' => max(0.0, -$sdl) * $faktor,
];
}
+42 -7
View File
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace Belevo\EnelixEMS;
use RuntimeException;
use Throwable;
trait ManagerEnergieTrait
{
private function konfiguriereEnergiearchiv(): void
@@ -23,9 +26,13 @@ trait ManagerEnergieTrait
);
foreach (self::LEISTUNGS_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
$loggen = $aktiv && (!str_starts_with($ident, 'SDL') || $this->ReadPropertyBoolean('SDLAktiv'))
&& ($ident !== 'SDLSOCArchiv' || $this->ReadPropertyInteger('SDLSOCVariableID') > 0);
if (!$this->setzeArchivLogging($archivID, $variableID, $loggen)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, 0);
AC_SetGraphStatus($archivID, $variableID, $aktiv);
AC_SetGraphStatus($archivID, $variableID, $loggen);
$regeln = [-1 => $leistungstyp];
$loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate');
if ($loeschen > 0) {
@@ -38,7 +45,9 @@ trait ManagerEnergieTrait
foreach (self::ENERGIE_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, 1);
AC_SetGraphStatus($archivID, $variableID, $aktiv);
$regeln = [-1 => $energietyp];
@@ -66,7 +75,9 @@ trait ManagerEnergieTrait
$istEnergie = str_contains($ident, 'Energie')
|| str_contains($ident, 'BatterieLaden_')
|| str_contains($ident, 'BatterieEntladen_');
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, $istEnergie ? 1 : 0);
AC_SetGraphStatus($archivID, $variableID, $aktiv);
$regeln = [-1 => $istEnergie ? $energietyp : $leistungstyp];
@@ -93,6 +104,17 @@ trait ManagerEnergieTrait
}
}
private function setzeArchivLogging(int $archivID, int $variableID, bool $aktiv): bool
{
// Archiv-Metadaten sind nur bei aktivierter Aufzeichnung aenderbar.
if (!$aktiv && AC_GetLoggingStatus($archivID, $variableID)) {
AC_SetGraphStatus($archivID, $variableID, false);
}
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
return $aktiv;
}
/** @param array<int, int> $regeln */
private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool
{
@@ -292,6 +314,7 @@ trait ManagerEnergieTrait
}
if (!$neuErzeugen) {
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
$this->synchronisiereSDLEnergiefluss($instanzID);
IPS_SetHidden($instanzID, false);
$this->aktualisiereEnergieflusswerte();
@@ -374,6 +397,7 @@ trait ManagerEnergieTrait
IPS_SetProperty($instanzID, 'Restrictions', '[]');
IPS_ApplyChanges($instanzID);
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', false);
$this->synchronisiereSDLEnergiefluss($instanzID, true);
$this->aktualisiereEnergieflusswerte($verbraucher);
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
@@ -564,8 +588,17 @@ trait ManagerEnergieTrait
);
$this->setzeEnergieflussLeistungswert(
'EnergieflussBatterie',
$leistungen['Batterie']
$leistungen['Batterie'] + ($this->sdlAnzeigen('SDLEnergieflussAnzeigen') ? 0.0 : $leistungen['SDL'])
);
$this->setzeEnergieflussLeistungswert('EnergieflussSDL', $leistungen['SDL']);
$soc = $this->aktuelleSDLSOC();
if ($soc !== null) {
$this->SetValue('SDLSOCArchiv', $soc);
}
$flussID = $this->findeUnterobjekt('Energiefluss');
if ($flussID > 0) {
$this->synchronisiereSDLEnergiefluss($flussID);
}
}
private function setzeEnergieflussLeistungswert(string $ident, float $watt): void
@@ -742,8 +775,10 @@ trait ManagerEnergieTrait
);
}
$this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
$this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
$leistungID = $this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
$energieID = $this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
$this->synchronisiereSDLDiagramm($leistungID, false);
$this->synchronisiereSDLDiagramm($energieID, true);
return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.';
}
+211
View File
@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
trait ManagerSDLTrait
{
private function pruefeSDLKonfiguration(): void
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return;
}
$leistungID = $this->ReadPropertyInteger('SDLLeistungVariableID');
if (!$this->istNumerischeVariable($leistungID)) {
throw new InvalidArgumentException('SDL benoetigt eine numerische Istleistungsvariable.');
}
$this->anlagenMessfaktor($this->ReadPropertyFloat('SDLLeistungsfaktor'), 'SDL');
$socID = $this->ReadPropertyInteger('SDLSOCVariableID');
if ($socID !== 0 && (!$this->istNumerischeVariable($socID) || $socID === $leistungID)) {
throw new InvalidArgumentException('SDL-SOC muss eine separate numerische Variable sein.');
}
$andereQuellen = array_merge($this->pvMessquellen(), $this->batterieMessquellen());
$andereQuellen[] = ['VariableID' => $this->ReadPropertyInteger('NetzleistungVariableID')];
foreach ($andereQuellen as $quelle) {
if ($leistungID === $quelle['VariableID']) {
throw new InvalidArgumentException(
'SDL-Leistung darf nicht zugleich als PV-, Netz- oder Batterieleistung bilanziert werden.'
);
}
}
}
private function synchronisiereSDLMesskonfiguration(): void
{
$neu = json_encode([
$this->ReadPropertyBoolean('SDLAktiv'),
$this->ReadPropertyInteger('SDLLeistungVariableID'),
$this->ReadPropertyFloat('SDLLeistungsfaktor'),
], JSON_THROW_ON_ERROR);
$alt = $this->ReadAttributeString('SDLMesskonfiguration');
if ($neu !== $alt && ($alt !== '' || $this->ReadPropertyBoolean('SDLAktiv'))) {
// Bereits erfasste Energien bleiben erhalten; keine Rueckrechnung mit neuen Quellen.
$this->WriteAttributeInteger('MessungLetzterZeitstempel', 0);
$this->WriteAttributeString('MessungZaehlerstaende', '{}');
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', 0);
}
$this->WriteAttributeString('SDLMesskonfiguration', $neu);
}
private function aktuelleSDLLeistung(): float
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return 0.0;
}
$variableID = $this->ReadPropertyInteger('SDLLeistungVariableID');
if (!$this->istNumerischeVariable($variableID)) {
throw new RuntimeException('Die SDL-Istleistung ist nicht verfuegbar.');
}
$wert = GetValue($variableID);
if ((!is_int($wert) && !is_float($wert)) || !is_finite((float) $wert)) {
throw new RuntimeException('Die SDL-Istleistung ist keine endliche Zahl.');
}
$leistung = (float) $wert * $this->ReadPropertyFloat('SDLLeistungsfaktor');
if (!is_finite($leistung)) {
throw new RuntimeException('Die skalierte SDL-Istleistung ist nicht endlich.');
}
if ($this->ReadPropertyInteger('SDLSOCVariableID') === 0 || $this->aktuelleSDLSOC() !== null) {
$this->setzeDiagnosewert('SDLStatus', 'Aktiv');
}
return $leistung;
}
private function aktuelleSDLSOC(): ?float
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return null;
}
$variableID = $this->ReadPropertyInteger('SDLSOCVariableID');
if ($variableID === 0) {
return null;
}
if ($this->istNumerischeVariable($variableID)) {
$wert = (float) GetValue($variableID);
if (is_finite($wert) && $wert >= 0.0 && $wert <= 100.0) {
return $wert;
}
}
$this->setzeDiagnosewert('SDLStatus', 'Fehler: SDL-SOC muss zwischen 0 und 100 Prozent liegen.');
return null;
}
private function sdlAnzeigen(string $property): bool
{
return $this->ReadPropertyBoolean('SDLAktiv') && $this->ReadPropertyBoolean($property);
}
private function aktualisiereSDLStatus(): void
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
$this->setzeDiagnosewert('SDLStatus', 'NichtVerwendet');
return;
}
try {
$this->aktuelleSDLLeistung();
$this->aktuelleSDLSOC();
} catch (Throwable $fehler) {
$this->setzeDiagnosewert('SDLStatus', 'Fehler: ' . $fehler->getMessage());
}
}
private function synchronisiereSDLEnergiefluss(int $instanzID, bool $profilAktualisieren = false): void
{
$anzeigen = $this->sdlAnzeigen('SDLEnergieflussAnzeigen');
$variableID = $this->findeUnterobjekt('EnergieflussSDL');
if (!$anzeigen && $variableID === 0) {
return;
}
if ($anzeigen && ($variableID === 0 || $profilAktualisieren)) {
$variableID = $this->energieflussLeistungsvariable(
'EnergieflussSDL', 'Energiefluss SDL', $this->energieflussLeistungsprofil(), 635
);
}
$alt = json_decode(IPS_GetProperty($instanzID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($alt) || !array_is_list($alt)) {
throw new RuntimeException('Die bestehende Energiefluss-Konfiguration ist keine Liste.');
}
$neu = [];
$gefunden = false;
$socID = $this->aktuelleSDLSOC() !== null
? $this->GetIDForIdent('SDLSOCArchiv') : 1;
foreach ($alt as $knoten) {
if ((int) ($knoten['VariableID'] ?? 0) !== $variableID) {
$neu[] = $knoten;
} elseif ($anzeigen && !$gefunden) {
$knoten['InfoVariableID'] = $socID;
$neu[] = $knoten;
$gefunden = true;
}
}
if ($anzeigen && !$gefunden) {
$neu[] = $this->energieflussKnoten($variableID, 'SDL / Regelenergie', 'storage', false, $socID);
}
if ($neu !== $alt) {
IPS_SetProperty($instanzID, 'Config', json_encode($neu, JSON_THROW_ON_ERROR));
IPS_ApplyChanges($instanzID);
}
}
private function synchronisiereSDLDiagramm(int $medienID, bool $energie): void
{
$idents = $energie
? ['SDLLadenEnergie', 'SDLEntladenEnergie', 'SDLSOCArchiv']
: ['SDLLeistungArchiv', 'SDLSOCArchiv'];
$verwaltet = array_map(fn (string $ident): int => $this->GetIDForIdent($ident), $idents);
$gewuenscht = [];
if ($this->sdlAnzeigen('SDLDiagrammeAnzeigen')) {
if ($energie) {
$gewuenscht[] = $this->chartDatensatz(
'SDLLadenEnergie', 'SDL laden', '#007C91', '~Electricity', 'bar'
);
$gewuenscht[] = $this->chartDatensatz(
'SDLEntladenEnergie', 'SDL entladen', '#C43E1C', '~Electricity', 'bar'
);
} else {
$gewuenscht[] = $this->chartDatensatz(
'SDLLeistungArchiv', 'SDL Istleistung', '#007C91', '~Power'
);
}
if ($this->ReadPropertyInteger('SDLSOCVariableID') > 0) {
$soc = $this->chartDatensatz('SDLSOCArchiv', 'SDL SOC', '#C43E1C', '~Intensity.100');
$soc['side'] = 'right';
$gewuenscht[] = $soc;
}
}
$inhalt = json_decode((string) base64_decode(IPS_GetMediaContent($medienID), true), true);
if (!is_array($inhalt) || !isset($inhalt['datasets']) || !is_array($inhalt['datasets'])) {
if (!$this->sdlAnzeigen('SDLDiagrammeAnzeigen')) {
return;
}
throw new RuntimeException('Das bestehende Diagramm hat keine gueltigen Datensaetze.');
}
$alt = $inhalt['datasets'];
$neu = [];
foreach ($alt as $datensatz) {
$id = (int) ($datensatz['variableID'] ?? 0);
if (!in_array($id, $verwaltet, true)) {
$neu[] = $datensatz;
continue;
}
foreach ($gewuenscht as $index => $ziel) {
if ($id === $ziel['variableID']) {
$neu[] = $datensatz;
unset($gewuenscht[$index]);
break;
}
}
}
$neu = array_merge($neu, array_values($gewuenscht));
if ($neu !== $alt) {
$inhalt['datasets'] = $neu;
IPS_SetMediaContent($medienID, base64_encode(json_encode($inhalt, JSON_THROW_ON_ERROR)));
}
}
}