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:
@@ -4,6 +4,13 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Manager: SDL-/Regelenergie als separate Istleistungsquelle mit optionalem SOC,
|
||||||
|
Leistungsfaktor und getrennten Anzeigeoptionen fuer Energiefluss und Diagramme.
|
||||||
|
SDL wird unabhaengig von der Anzeige aus Hauslast und neuer Hausenergie
|
||||||
|
herausgerechnet; die Prognosetelemetrie verwendet die korrigierte Hauslast.
|
||||||
|
Bestehende Visualisierungen werden ausschliesslich um eigene SDL-Eintraege
|
||||||
|
ergaenzt. Standardmaessig deaktiviert, keine automatische Quellenmigration,
|
||||||
|
keine rueckwirkende Datenkorrektur und keine Aenderung externer SDL-Steuerungen.
|
||||||
- Manager: Positives Budget wird bei gleicher Prioritaet nach der kleinsten
|
- Manager: Positives Budget wird bei gleicher Prioritaet nach der kleinsten
|
||||||
naechsten erreichbaren Sollleistung verteilt. Nur bei gleicher Leistungsstufe
|
naechsten erreichbaren Sollleistung verteilt. Nur bei gleicher Leistungsstufe
|
||||||
entscheiden exakter Energiebezug und anschliessend die Instanz-ID. Einzelstufen
|
entscheiden exakter Energiebezug und anschliessend die Instanz-ID. Einzelstufen
|
||||||
|
|||||||
@@ -835,6 +835,50 @@
|
|||||||
"type": "ExpansionPanel",
|
"type": "ExpansionPanel",
|
||||||
"caption": "SDL / VGT",
|
"caption": "SDL / VGT",
|
||||||
"items": [
|
"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",
|
"type": "ValidationTextBox",
|
||||||
"name": "SDLAnschluss",
|
"name": "SDLAnschluss",
|
||||||
|
|||||||
+84
-15
@@ -11,6 +11,7 @@ require_once __DIR__ . '/../libs/Anlagentopologie.php';
|
|||||||
require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
|
require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
|
||||||
require_once __DIR__ . '/../libs/EnergieMessung.php';
|
require_once __DIR__ . '/../libs/EnergieMessung.php';
|
||||||
require_once __DIR__ . '/../libs/ManagerEnergieTrait.php';
|
require_once __DIR__ . '/../libs/ManagerEnergieTrait.php';
|
||||||
|
require_once __DIR__ . '/../libs/ManagerSDLTrait.php';
|
||||||
|
|
||||||
use Belevo\EnelixEMS\Anlagentopologie;
|
use Belevo\EnelixEMS\Anlagentopologie;
|
||||||
use Belevo\EnelixEMS\EinspeiseRegler;
|
use Belevo\EnelixEMS\EinspeiseRegler;
|
||||||
@@ -25,6 +26,7 @@ use Belevo\EnelixEMS\StoerungsSnapshot;
|
|||||||
class Manager extends IPSModule implements ManagerSchnittstelle
|
class Manager extends IPSModule implements ManagerSchnittstelle
|
||||||
{
|
{
|
||||||
use ManagerEnergieTrait;
|
use ManagerEnergieTrait;
|
||||||
|
use \Belevo\EnelixEMS\ManagerSDLTrait;
|
||||||
|
|
||||||
private const STATUS_AKTIV = 102;
|
private const STATUS_AKTIV = 102;
|
||||||
private const STATUS_MESSWERT_UNGUELTIG = 201;
|
private const STATUS_MESSWERT_UNGUELTIG = 201;
|
||||||
@@ -46,6 +48,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'HausverbrauchLeistungArchiv',
|
'HausverbrauchLeistungArchiv',
|
||||||
'NetzleistungArchiv',
|
'NetzleistungArchiv',
|
||||||
'BatterieleistungArchiv',
|
'BatterieleistungArchiv',
|
||||||
|
'SDLLeistungArchiv',
|
||||||
|
'SDLSOCArchiv',
|
||||||
];
|
];
|
||||||
private const ENERGIE_VARIABLEN = [
|
private const ENERGIE_VARIABLEN = [
|
||||||
'PVEnergie',
|
'PVEnergie',
|
||||||
@@ -54,6 +58,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'EinspeisungEnergie',
|
'EinspeisungEnergie',
|
||||||
'BatterieLadenEnergie',
|
'BatterieLadenEnergie',
|
||||||
'BatterieEntladenEnergie',
|
'BatterieEntladenEnergie',
|
||||||
|
'SDLLadenEnergie',
|
||||||
|
'SDLEntladenEnergie',
|
||||||
];
|
];
|
||||||
private const VERBRAUCHER_LIZENZEN = [
|
private const VERBRAUCHER_LIZENZEN = [
|
||||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
|
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
|
||||||
@@ -146,6 +152,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$this->RegisterPropertyInteger('PrognoseSendeintervall', 60);
|
$this->RegisterPropertyInteger('PrognoseSendeintervall', 60);
|
||||||
$this->RegisterPropertyString('PrognoseAnschluss', '{}');
|
$this->RegisterPropertyString('PrognoseAnschluss', '{}');
|
||||||
$this->RegisterPropertyString('SDLAnschluss', '{}');
|
$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', '');
|
$this->RegisterPropertyString('Lizenzcode', '');
|
||||||
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
|
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
|
||||||
$this->RegisterPropertyString('LizenzAnschluss', '{}');
|
$this->RegisterPropertyString('LizenzAnschluss', '{}');
|
||||||
@@ -210,6 +222,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$this->RegisterAttributeString('EnergiediagrammKonfiguration', '{}');
|
$this->RegisterAttributeString('EnergiediagrammKonfiguration', '{}');
|
||||||
$this->RegisterAttributeString('EnergieflussKonfiguration', '{}');
|
$this->RegisterAttributeString('EnergieflussKonfiguration', '{}');
|
||||||
$this->RegisterAttributeBoolean('EnergieflussNeuErzeugen', false);
|
$this->RegisterAttributeBoolean('EnergieflussNeuErzeugen', false);
|
||||||
|
$this->RegisterAttributeString('SDLMesskonfiguration', '');
|
||||||
|
|
||||||
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
|
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
|
||||||
$this->RegisterTimer('Regelzyklus', 0, '');
|
$this->RegisterTimer('Regelzyklus', 0, '');
|
||||||
@@ -250,6 +263,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$this->pruefeKonfiguration();
|
$this->pruefeKonfiguration();
|
||||||
|
$this->synchronisiereSDLMesskonfiguration();
|
||||||
$this->synchronisiereEnergievisualisierungen();
|
$this->synchronisiereEnergievisualisierungen();
|
||||||
$this->konfiguriereEnergiearchiv();
|
$this->konfiguriereEnergiearchiv();
|
||||||
} catch (Throwable $fehler) {
|
} catch (Throwable $fehler) {
|
||||||
@@ -430,6 +444,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$this->monatsgrenzenFuerFormular()
|
$this->monatsgrenzenFuerFormular()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->setzeFormularParameter(
|
||||||
|
$formular['elements'],
|
||||||
|
'SDLKonfiguration',
|
||||||
|
'visible',
|
||||||
|
$this->ReadPropertyBoolean('SDLAktiv')
|
||||||
|
);
|
||||||
|
|
||||||
return json_encode($formular, JSON_THROW_ON_ERROR);
|
return json_encode($formular, JSON_THROW_ON_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,6 +522,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case 'FormSDLAktiv':
|
||||||
|
$this->UpdateFormField('SDLKonfiguration', 'visible', (bool) $wert);
|
||||||
|
return;
|
||||||
|
|
||||||
case 'FormEnergyPieAnzeigen':
|
case 'FormEnergyPieAnzeigen':
|
||||||
$this->synchronisiereEnergyPie((bool) $wert);
|
$this->synchronisiereEnergyPie((bool) $wert);
|
||||||
return;
|
return;
|
||||||
@@ -1676,6 +1701,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'HausverbrauchLeistungArchiv' => 'Hausverbrauch',
|
'HausverbrauchLeistungArchiv' => 'Hausverbrauch',
|
||||||
'NetzleistungArchiv' => 'Netzleistung Aufzeichnung',
|
'NetzleistungArchiv' => 'Netzleistung Aufzeichnung',
|
||||||
'BatterieleistungArchiv' => 'Batterieleistung',
|
'BatterieleistungArchiv' => 'Batterieleistung',
|
||||||
|
'SDLLeistungArchiv' => 'SDL Istleistung',
|
||||||
];
|
];
|
||||||
$energievariablen = [
|
$energievariablen = [
|
||||||
'PVEnergie' => 'PV-Energie',
|
'PVEnergie' => 'PV-Energie',
|
||||||
@@ -1684,6 +1710,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'EinspeisungEnergie' => 'Einspeisung Energie',
|
'EinspeisungEnergie' => 'Einspeisung Energie',
|
||||||
'BatterieLadenEnergie' => 'Batterie geladen',
|
'BatterieLadenEnergie' => 'Batterie geladen',
|
||||||
'BatterieEntladenEnergie' => 'Batterie entladen',
|
'BatterieEntladenEnergie' => 'Batterie entladen',
|
||||||
|
'SDLLadenEnergie' => 'SDL geladen',
|
||||||
|
'SDLEntladenEnergie' => 'SDL entladen',
|
||||||
];
|
];
|
||||||
$position = 200;
|
$position = 200;
|
||||||
foreach ($leistungsvariablen as $ident => $name) {
|
foreach ($leistungsvariablen as $ident => $name) {
|
||||||
@@ -1697,6 +1725,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$position += 10;
|
$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')) {
|
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||||
$this->RegisterVariableBoolean(
|
$this->RegisterVariableBoolean(
|
||||||
'NetzleistungGueltig',
|
'NetzleistungGueltig',
|
||||||
@@ -1755,6 +1789,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
|
if ($ident === 'SDLStatus' && $this->ReadPropertyBoolean('SDLAktiv')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||||
$this->UnregisterVariable($ident);
|
$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
|
private function leseMessleistungen(): array
|
||||||
{
|
{
|
||||||
$pv = 0.0;
|
$pv = 0.0;
|
||||||
@@ -2043,12 +2080,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
$netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID'))
|
$netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID'))
|
||||||
* $this->ReadPropertyFloat('Netzleistungsfaktor');
|
* $this->ReadPropertyFloat('Netzleistungsfaktor');
|
||||||
|
$sdl = $this->aktuelleSDLLeistung();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'PV' => $pv,
|
'PV' => $pv,
|
||||||
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie),
|
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie, $sdl),
|
||||||
'Netz' => $netz,
|
'Netz' => $netz,
|
||||||
'Batterie' => $batterie,
|
'Batterie' => $batterie,
|
||||||
|
'SDL' => $sdl,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2073,7 +2112,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, float>|null */
|
/** @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();
|
$quellen = $this->energieZaehlerquellen();
|
||||||
if ($quellen === null) {
|
if ($quellen === null) {
|
||||||
@@ -2102,6 +2145,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$delta = $stand - (float) $alt[$schluessel];
|
$delta = $stand - (float) $alt[$schluessel];
|
||||||
if ($delta >= 0.0) {
|
if ($delta >= 0.0) {
|
||||||
$deltas[$ident] += $delta;
|
$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);
|
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['PVEnergie'],
|
||||||
$deltas['NetzbezugEnergie'],
|
$deltas['NetzbezugEnergie'],
|
||||||
$deltas['EinspeisungEnergie'],
|
$deltas['EinspeisungEnergie'],
|
||||||
$deltas['BatterieLadenEnergie'],
|
$deltas['BatterieLadenEnergie'],
|
||||||
$deltas['BatterieEntladenEnergie']
|
$deltas['BatterieEntladenEnergie'],
|
||||||
);
|
$sdlLaden,
|
||||||
|
$sdlEntladen
|
||||||
|
) : 0.0;
|
||||||
|
|
||||||
return $deltas;
|
return $deltas;
|
||||||
}
|
}
|
||||||
@@ -2130,13 +2179,31 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$jetzt = time();
|
$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');
|
$letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel');
|
||||||
$puffer = $this->leseJsonAttribut('MessungEnergiePuffer');
|
$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) {
|
if ($energieDeltas === null && $letzterZeitstempel > 0) {
|
||||||
$sekunden = min(300, max(0, $jetzt - $letzterZeitstempel));
|
$energieDeltas = $integriert;
|
||||||
$energieDeltas = EnergieMessung::energieDeltas($leistungen, $sekunden);
|
|
||||||
}
|
}
|
||||||
foreach ($energieDeltas ?? [] as $ident => $delta) {
|
foreach ($energieDeltas ?? [] as $ident => $delta) {
|
||||||
$puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta;
|
$puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta;
|
||||||
@@ -2155,10 +2222,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'HausverbrauchLeistungArchiv' => 'Haus',
|
'HausverbrauchLeistungArchiv' => 'Haus',
|
||||||
'NetzleistungArchiv' => 'Netz',
|
'NetzleistungArchiv' => 'Netz',
|
||||||
'BatterieleistungArchiv' => 'Batterie',
|
'BatterieleistungArchiv' => 'Batterie',
|
||||||
|
'SDLLeistungArchiv' => 'SDL',
|
||||||
] as $ident => $quelle) {
|
] as $ident => $quelle) {
|
||||||
$this->SetValue($ident, $leistungen[$quelle]);
|
$this->SetValue($ident, $leistungen[$quelle]);
|
||||||
}
|
}
|
||||||
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt);
|
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt);
|
||||||
|
$soc = $this->aktuelleSDLSOC();
|
||||||
|
if ($soc !== null) {
|
||||||
|
$this->SetValue('SDLSOCArchiv', $soc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60;
|
$energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60;
|
||||||
@@ -2672,11 +2744,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'Prognosestatus',
|
'Prognosestatus',
|
||||||
$this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet'
|
$this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet'
|
||||||
);
|
);
|
||||||
foreach (['SDLAnschluss' => 'SDLStatus'] as $property => $variable) {
|
$this->aktualisiereSDLStatus();
|
||||||
$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->setzeDiagnosewert(
|
$this->setzeDiagnosewert(
|
||||||
'Stoerueberwachungsstatus',
|
'Stoerueberwachungsstatus',
|
||||||
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet'
|
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet'
|
||||||
@@ -2685,6 +2753,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
|
|
||||||
private function pruefeKonfiguration(): void
|
private function pruefeKonfiguration(): void
|
||||||
{
|
{
|
||||||
|
$this->pruefeSDLKonfiguration();
|
||||||
foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) {
|
foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) {
|
||||||
if ($this->ReadPropertyInteger($property) <= 0) {
|
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||||
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ Die drei Regelungsvariablen `Aktiv`, `Betriebsart` und `Netzleistung` sind immer
|
|||||||
| `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. |
|
| `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. |
|
||||||
| `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. |
|
| `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. |
|
||||||
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
|
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
|
||||||
|
| `SDLAktiv` | Boolean | `false`; separat gemessene SDL-/Regelenergie in der Anlagenbilanz beruecksichtigen. Keine SDL-Steuerung. |
|
||||||
|
| `SDLLeistungVariableID` | Integer | `0`; bei aktiver SDL erforderliche numerische Istleistungsquelle. |
|
||||||
|
| `SDLLeistungsfaktor` | Float | `1`; Umrechnung nach W, positiv Laden/Bezug, negativ Entladen/Abgabe. Fuer kW `1000`, bei umgekehrtem Vorzeichen negativ. |
|
||||||
|
| `SDLSOCVariableID` | Integer | `0`; optionaler separater SDL-Ladezustand in Prozent, 0 bis 100. |
|
||||||
|
| `SDLEnergieflussAnzeigen` | Boolean | `false`; SDL mit optionalem SOC als eigenen Speicher im aktivierten Energiefluss anzeigen. |
|
||||||
|
| `SDLDiagrammeAnzeigen` | Boolean | `false`; SDL-Leistung, Lade-/Entladeenergie und optionalen SOC in aktivierten Diagrammen anzeigen. |
|
||||||
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. |
|
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. |
|
||||||
| `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. |
|
| `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. |
|
||||||
| `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. |
|
| `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. |
|
||||||
@@ -134,6 +140,59 @@ entfernt beziehungsweise die native Energy Distribution ausgeblendet:
|
|||||||
- responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten,
|
- responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten,
|
||||||
- native Energy Distribution fuer PV, Netz, Haus und Batterie.
|
- native Energy Distribution fuer PV, Netz, Haus und Batterie.
|
||||||
|
|
||||||
|
## SDL / Regelenergie
|
||||||
|
|
||||||
|
`SDL aktiv` blendet die Quellen- und Anzeigeauswahl ein. Die SDL-Istleistung
|
||||||
|
beschreibt einen **separat gemessenen AC-Zweig am selben Netzanschlusspunkt**.
|
||||||
|
Sie darf weder in der normalen Batterieleistung noch in einer PV-Leistung
|
||||||
|
enthalten sein. Eine identische Variablen-ID fuer SDL und Netz/PV/Batterie wird
|
||||||
|
abgewiesen. Ueberschneidungen in extern gebildeten Summen muss die
|
||||||
|
Anlagenkonfiguration ausschliessen. Ein gemeinsamer Speicher mit nur einem
|
||||||
|
Gesamtleistungsmesswert kann damit nicht in EMS- und SDL-Anteile zerlegt werden.
|
||||||
|
|
||||||
|
Bei aktiver SDL lautet die Hauslast `max(0, PV + Netz - Batterie - SDL)`.
|
||||||
|
Die Korrektur geschieht vor der Begrenzung auf null und unabhaengig davon,
|
||||||
|
ob SDL angezeigt wird. Diese Hauslast wird an den Prognosedienst gesendet;
|
||||||
|
Netzleistung und der kapazitaetsgewichtete SOC der normalen Batterien bleiben
|
||||||
|
unveraendert. SDL-SOC ersetzt keinen Batterie-SOC und wird nicht als
|
||||||
|
planbare EMS-Speicherkapazitaet exportiert. Ungueltige SDL-Leistungswerte
|
||||||
|
unterbrechen die Messaufzeichnung und den jeweiligen Telemetrieversand,
|
||||||
|
statt als null in die Hauslast einzugehen. Der SDL-Status ist bei aktiver
|
||||||
|
SDL auch ohne eingeschaltete Diagnosevariablen sichtbar.
|
||||||
|
|
||||||
|
`SDLLeistungArchiv` zeichnet Watt auf, `SDLSOCArchiv` gueltige Prozentwerte.
|
||||||
|
`SDLLadenEnergie` und `SDLEntladenEnergie` integrieren die getrennten Richtungen
|
||||||
|
in kWh. Die Abtastung erfolgt wie bei den anderen Leistungen minuetlich, nicht
|
||||||
|
als abrechnungsgenaue SDL-Energiemessung. Auch bei vorhandenen physikalischen
|
||||||
|
Energiezaehlern wird die Hausenergie um die im selben Intervall integrierte
|
||||||
|
SDL-Ladung vermindert und um SDL-Entladung erhoeht. Bei Messluecken ueber
|
||||||
|
fuenf Minuten, Quellenwechseln oder Zaehlerresets wird keine unvollstaendige
|
||||||
|
Hausenergie nachberechnet; Historie und bereits erfasste Zaehler bleiben erhalten.
|
||||||
|
|
||||||
|
Die Anzeigeoptionen wirken beim Anwenden direkt auf die vorhandenen
|
||||||
|
Manager-Visualisierungen. Nur die eigenen SDL-Knoten und -Datensaetze werden
|
||||||
|
ergaenzt oder entfernt. Andere Knoten, Farben, Datensaetze und sonstige
|
||||||
|
Anpassungen bleiben bestehen. Im Energiefluss verwendet SDL dieselbe W/kW-
|
||||||
|
Einheit wie die anderen Manager-Knoten; ein ungueltiger SOC wird dort nicht
|
||||||
|
als aktueller Zusatzwert angezeigt. Ohne separaten SDL-Knoten enthaelt der
|
||||||
|
Manager-Sammelknoten Batteriespeicher die normale Batterie plus SDL, damit
|
||||||
|
die dargestellte Leistungsbilanz vollstaendig bleibt. Im Leistungsdiagramm
|
||||||
|
erscheint die signierte SDL-Istleistung, im Energiediagramm erscheinen Laden
|
||||||
|
und Entladen getrennt. SOC verwendet in beiden Diagrammen eine eigene rechte
|
||||||
|
Prozentachse. Die allgemeinen Schalter fuer Energiefluss, Diagramme und
|
||||||
|
Energieaufzeichnung bleiben erforderlich.
|
||||||
|
|
||||||
|
Migration: SDL bleibt nach dem Modulupdate standardmaessig aus. Bestehende
|
||||||
|
Installationen behalten damit ihre bisherige Bilanz. Erst Quellen und
|
||||||
|
Vorzeichen pruefen, SDL aktivieren und anwenden. Historische Hauslasten oder
|
||||||
|
bereits trainierte Prognosen werden nicht rueckwirkend korrigiert; neue
|
||||||
|
Prognoseeingaben sind ab Aktivierung bereinigt. Ausschalten entfernt nur
|
||||||
|
SDL-Anzeigen, nicht die aufgezeichnete Historie. Der alte `SDLAnschluss`
|
||||||
|
bleibt lesbar; externe SDL-Regelung und Stellbefehle werden nicht veraendert.
|
||||||
|
|
||||||
|
Native Darstellungsvertraege: [Energy Distribution](https://www.symcon.de/en/service/documentation/module-reference/energy/energy-distribution/)
|
||||||
|
und [Diagramme](https://www.symcon.de/en/service/documentation/basics/media/charts/).
|
||||||
|
|
||||||
## Anlagenweite Einspeisebegrenzung
|
## Anlagenweite Einspeisebegrenzung
|
||||||
|
|
||||||
Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der
|
Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der
|
||||||
|
|||||||
+14
-5
@@ -8,11 +8,11 @@ final class EnergieMessung
|
|||||||
{
|
{
|
||||||
private const WATTSEKUNDEN_PRO_KWH = 3600000.0;
|
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(
|
return max(
|
||||||
0.0,
|
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 $netzbezug,
|
||||||
float $netzeinspeisung,
|
float $netzeinspeisung,
|
||||||
float $batterieLaden,
|
float $batterieLaden,
|
||||||
float $batterieEntladen
|
float $batterieEntladen,
|
||||||
|
float $sdlLaden = 0.0,
|
||||||
|
float $sdlEntladen = 0.0
|
||||||
): float {
|
): float {
|
||||||
return max(
|
return max(
|
||||||
0.0,
|
0.0,
|
||||||
@@ -30,18 +32,22 @@ final class EnergieMessung
|
|||||||
+ self::endlich($batterieEntladen)
|
+ self::endlich($batterieEntladen)
|
||||||
- self::endlich($netzeinspeisung)
|
- self::endlich($netzeinspeisung)
|
||||||
- self::endlich($batterieLaden)
|
- 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{
|
* @return array{
|
||||||
* PVEnergie: float,
|
* PVEnergie: float,
|
||||||
* Hausenergie: float,
|
* Hausenergie: float,
|
||||||
* NetzbezugEnergie: float,
|
* NetzbezugEnergie: float,
|
||||||
* EinspeisungEnergie: float,
|
* EinspeisungEnergie: float,
|
||||||
* BatterieLadenEnergie: float,
|
* BatterieLadenEnergie: float,
|
||||||
* BatterieEntladenEnergie: float
|
* BatterieEntladenEnergie: float,
|
||||||
|
* SDLLadenEnergie: float,
|
||||||
|
* SDLEntladenEnergie: float
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public static function energieDeltas(array $leistungen, int $sekunden): array
|
public static function energieDeltas(array $leistungen, int $sekunden): array
|
||||||
@@ -51,6 +57,7 @@ final class EnergieMessung
|
|||||||
$haus = self::endlich($leistungen['Haus']);
|
$haus = self::endlich($leistungen['Haus']);
|
||||||
$netz = self::endlich($leistungen['Netz']);
|
$netz = self::endlich($leistungen['Netz']);
|
||||||
$batterie = self::endlich($leistungen['Batterie']);
|
$batterie = self::endlich($leistungen['Batterie']);
|
||||||
|
$sdl = self::endlich($leistungen['SDL'] ?? 0.0);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'PVEnergie' => max(0.0, $pv) * $faktor,
|
'PVEnergie' => max(0.0, $pv) * $faktor,
|
||||||
@@ -59,6 +66,8 @@ final class EnergieMessung
|
|||||||
'EinspeisungEnergie' => max(0.0, -$netz) * $faktor,
|
'EinspeisungEnergie' => max(0.0, -$netz) * $faktor,
|
||||||
'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor,
|
'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor,
|
||||||
'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor,
|
'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor,
|
||||||
|
'SDLLadenEnergie' => max(0.0, $sdl) * $faktor,
|
||||||
|
'SDLEntladenEnergie' => max(0.0, -$sdl) * $faktor,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Belevo\EnelixEMS;
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
trait ManagerEnergieTrait
|
trait ManagerEnergieTrait
|
||||||
{
|
{
|
||||||
private function konfiguriereEnergiearchiv(): void
|
private function konfiguriereEnergiearchiv(): void
|
||||||
@@ -23,9 +26,13 @@ trait ManagerEnergieTrait
|
|||||||
);
|
);
|
||||||
foreach (self::LEISTUNGS_VARIABLEN as $ident) {
|
foreach (self::LEISTUNGS_VARIABLEN as $ident) {
|
||||||
$variableID = $this->GetIDForIdent($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_SetAggregationType($archivID, $variableID, 0);
|
||||||
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
AC_SetGraphStatus($archivID, $variableID, $loggen);
|
||||||
$regeln = [-1 => $leistungstyp];
|
$regeln = [-1 => $leistungstyp];
|
||||||
$loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate');
|
$loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate');
|
||||||
if ($loeschen > 0) {
|
if ($loeschen > 0) {
|
||||||
@@ -38,7 +45,9 @@ trait ManagerEnergieTrait
|
|||||||
|
|
||||||
foreach (self::ENERGIE_VARIABLEN as $ident) {
|
foreach (self::ENERGIE_VARIABLEN as $ident) {
|
||||||
$variableID = $this->GetIDForIdent($ident);
|
$variableID = $this->GetIDForIdent($ident);
|
||||||
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
|
if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
AC_SetAggregationType($archivID, $variableID, 1);
|
AC_SetAggregationType($archivID, $variableID, 1);
|
||||||
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
||||||
$regeln = [-1 => $energietyp];
|
$regeln = [-1 => $energietyp];
|
||||||
@@ -66,7 +75,9 @@ trait ManagerEnergieTrait
|
|||||||
$istEnergie = str_contains($ident, 'Energie')
|
$istEnergie = str_contains($ident, 'Energie')
|
||||||
|| str_contains($ident, 'BatterieLaden_')
|
|| str_contains($ident, 'BatterieLaden_')
|
||||||
|| str_contains($ident, 'BatterieEntladen_');
|
|| str_contains($ident, 'BatterieEntladen_');
|
||||||
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
|
if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
AC_SetAggregationType($archivID, $variableID, $istEnergie ? 1 : 0);
|
AC_SetAggregationType($archivID, $variableID, $istEnergie ? 1 : 0);
|
||||||
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
||||||
$regeln = [-1 => $istEnergie ? $energietyp : $leistungstyp];
|
$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 */
|
/** @param array<int, int> $regeln */
|
||||||
private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool
|
private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool
|
||||||
{
|
{
|
||||||
@@ -292,6 +314,7 @@ trait ManagerEnergieTrait
|
|||||||
}
|
}
|
||||||
if (!$neuErzeugen) {
|
if (!$neuErzeugen) {
|
||||||
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
|
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
|
||||||
|
$this->synchronisiereSDLEnergiefluss($instanzID);
|
||||||
IPS_SetHidden($instanzID, false);
|
IPS_SetHidden($instanzID, false);
|
||||||
$this->aktualisiereEnergieflusswerte();
|
$this->aktualisiereEnergieflusswerte();
|
||||||
|
|
||||||
@@ -374,6 +397,7 @@ trait ManagerEnergieTrait
|
|||||||
IPS_SetProperty($instanzID, 'Restrictions', '[]');
|
IPS_SetProperty($instanzID, 'Restrictions', '[]');
|
||||||
IPS_ApplyChanges($instanzID);
|
IPS_ApplyChanges($instanzID);
|
||||||
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', false);
|
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', false);
|
||||||
|
$this->synchronisiereSDLEnergiefluss($instanzID, true);
|
||||||
$this->aktualisiereEnergieflusswerte($verbraucher);
|
$this->aktualisiereEnergieflusswerte($verbraucher);
|
||||||
|
|
||||||
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
|
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
|
||||||
@@ -564,8 +588,17 @@ trait ManagerEnergieTrait
|
|||||||
);
|
);
|
||||||
$this->setzeEnergieflussLeistungswert(
|
$this->setzeEnergieflussLeistungswert(
|
||||||
'EnergieflussBatterie',
|
'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
|
private function setzeEnergieflussLeistungswert(string $ident, float $watt): void
|
||||||
@@ -742,8 +775,10 @@ trait ManagerEnergieTrait
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
|
$leistungID = $this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
|
||||||
$this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
|
$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.';
|
return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ final class EnergieMessungTest extends TestCase
|
|||||||
self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001);
|
self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001);
|
||||||
self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001);
|
self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001);
|
||||||
self::assertSame(0.0, $werte['BatterieEntladenEnergie']);
|
self::assertSame(0.0, $werte['BatterieEntladenEnergie']);
|
||||||
|
self::assertSame(0.0, $werte['SDLLadenEnergie']);
|
||||||
|
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTrenntBezugUndBatterieentladung(): void
|
public function testTrenntBezugUndBatterieentladung(): void
|
||||||
@@ -50,23 +52,100 @@ final class EnergieMessungTest extends TestCase
|
|||||||
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
|
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBeruecksichtigtSDLVorDerBegrenzungDerHausleistung(): void
|
||||||
|
{
|
||||||
|
self::assertSame(1700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 1000.0));
|
||||||
|
self::assertSame(3700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, -1000.0));
|
||||||
|
self::assertSame(1000.0, EnergieMessung::hausleistung(0.0, -1000.0, 0.0, -2000.0));
|
||||||
|
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, 0.0, 0.0, 1000.0));
|
||||||
|
self::assertSame(
|
||||||
|
EnergieMessung::hausleistung(4200.0, -800.0, 700.0),
|
||||||
|
EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 0.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testBerechnetHausenergieAusDenZaehlerdeltas(): void
|
public function testBerechnetHausenergieAusDenZaehlerdeltas(): void
|
||||||
{
|
{
|
||||||
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0));
|
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));
|
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 5.0, 0.0, 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBereinigtHausenergieUmSDLLadungUndEntladung(): void
|
||||||
|
{
|
||||||
|
self::assertSame(5.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 0.0));
|
||||||
|
self::assertSame(11.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 3.0));
|
||||||
|
self::assertSame(7.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 2.0));
|
||||||
|
self::assertSame(1.0, EnergieMessung::hausenergie(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0));
|
||||||
|
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0));
|
||||||
|
self::assertSame(
|
||||||
|
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0),
|
||||||
|
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 0.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIntegriertSDLGetrenntVonBatterieUndHaus(): void
|
||||||
|
{
|
||||||
|
$leistungen = [
|
||||||
|
'PV' => 7200.0,
|
||||||
|
'Haus' => 3600.0,
|
||||||
|
'Netz' => -1800.0,
|
||||||
|
'Batterie' => 900.0,
|
||||||
|
'SDL' => 1800.0,
|
||||||
|
];
|
||||||
|
$laden = EnergieMessung::energieDeltas($leistungen, 300);
|
||||||
|
self::assertEqualsWithDelta(0.15, $laden['SDLLadenEnergie'], 0.000001);
|
||||||
|
self::assertSame(0.0, $laden['SDLEntladenEnergie']);
|
||||||
|
self::assertEqualsWithDelta(0.075, $laden['BatterieLadenEnergie'], 0.000001);
|
||||||
|
self::assertEqualsWithDelta(0.3, $laden['Hausenergie'], 0.000001);
|
||||||
|
|
||||||
|
$leistungen['SDL'] = -1800.0;
|
||||||
|
$entladen = EnergieMessung::energieDeltas($leistungen, 300);
|
||||||
|
self::assertSame(0.0, $entladen['SDLLadenEnergie']);
|
||||||
|
self::assertEqualsWithDelta(0.15, $entladen['SDLEntladenEnergie'], 0.000001);
|
||||||
|
self::assertSame($laden['Hausenergie'], $entladen['Hausenergie']);
|
||||||
|
self::assertSame($laden['BatterieLadenEnergie'], $entladen['BatterieLadenEnergie']);
|
||||||
|
|
||||||
|
$leistungen['SDL'] = 0.0;
|
||||||
|
$mitNull = EnergieMessung::energieDeltas($leistungen, 300);
|
||||||
|
unset($leistungen['SDL']);
|
||||||
|
self::assertSame($mitNull, EnergieMessung::energieDeltas($leistungen, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIgnoriertNichtEndlicheSDLWerte(): void
|
||||||
|
{
|
||||||
|
foreach ([INF, -INF, NAN] as $ungueltig) {
|
||||||
|
self::assertSame(2700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, $ungueltig));
|
||||||
|
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, $ungueltig, $ungueltig));
|
||||||
|
$werte = EnergieMessung::energieDeltas([
|
||||||
|
'PV' => 0.0,
|
||||||
|
'Haus' => 1000.0,
|
||||||
|
'Netz' => 1000.0,
|
||||||
|
'Batterie' => 0.0,
|
||||||
|
'SDL' => $ungueltig,
|
||||||
|
], 3600);
|
||||||
|
|
||||||
|
self::assertSame(0.0, $werte['SDLLadenEnergie']);
|
||||||
|
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
|
||||||
|
self::assertSame(1.0, $werte['Hausenergie']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
|
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
|
||||||
{
|
{
|
||||||
$werte = EnergieMessung::energieDeltas([
|
foreach ([-1, 0] as $dauer) {
|
||||||
'PV' => INF,
|
foreach ([-1000.0, 1000.0] as $sdl) {
|
||||||
'Haus' => NAN,
|
$werte = EnergieMessung::energieDeltas([
|
||||||
'Netz' => 1000.0,
|
'PV' => INF,
|
||||||
'Batterie' => -1000.0,
|
'Haus' => NAN,
|
||||||
], -1);
|
'Netz' => 1000.0,
|
||||||
|
'Batterie' => -1000.0,
|
||||||
|
'SDL' => $sdl,
|
||||||
|
], $dauer);
|
||||||
|
|
||||||
foreach ($werte as $wert) {
|
foreach ($werte as $wert) {
|
||||||
self::assertSame(0.0, $wert);
|
self::assertSame(0.0, $wert);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
'EinspeisebegrenzungAktiv',
|
'EinspeisebegrenzungAktiv',
|
||||||
'Einspeisegrenze',
|
'Einspeisegrenze',
|
||||||
'PrognoseAktiv',
|
'PrognoseAktiv',
|
||||||
|
'SDLAktiv',
|
||||||
|
'SDLLeistungVariableID',
|
||||||
|
'SDLLeistungsfaktor',
|
||||||
|
'SDLSOCVariableID',
|
||||||
|
'SDLEnergieflussAnzeigen',
|
||||||
|
'SDLDiagrammeAnzeigen',
|
||||||
'PrognoseLizenzInformation',
|
'PrognoseLizenzInformation',
|
||||||
'NetzfahrplanAktiv',
|
'NetzfahrplanAktiv',
|
||||||
'NetzbezugEnergieVariableID',
|
'NetzbezugEnergieVariableID',
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Manager;
|
||||||
|
use ManagerSDLRuntime;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
*/
|
||||||
|
final class ManagerSDLTest extends TestCase
|
||||||
|
{
|
||||||
|
private Manager $manager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
require_once __DIR__ . '/Support/ManagerSDLRuntime.php';
|
||||||
|
require_once __DIR__ . '/../Manager/module.php';
|
||||||
|
|
||||||
|
ManagerSDLRuntime::reset();
|
||||||
|
$this->manager = new Manager(42);
|
||||||
|
$this->manager->Create();
|
||||||
|
foreach ([101 => 1000.0, 102 => 5000.0, 103 => 200.0, 104 => 1500.0, 105 => 70.0] as $id => $value) {
|
||||||
|
ManagerSDLRuntime::variable($id, $value);
|
||||||
|
}
|
||||||
|
$this->properties([
|
||||||
|
'NetzleistungVariableID' => 101,
|
||||||
|
'AnlagenWechselrichter' => $this->json([
|
||||||
|
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102],
|
||||||
|
]),
|
||||||
|
'AnlagenBatterien' => $this->json([
|
||||||
|
['ID' => 'battery', 'LeistungVariableID' => 103],
|
||||||
|
]),
|
||||||
|
'SDLLeistungVariableID' => 104,
|
||||||
|
'SDLSOCVariableID' => 105,
|
||||||
|
]);
|
||||||
|
foreach ([
|
||||||
|
'SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie',
|
||||||
|
] as $ident) {
|
||||||
|
ManagerSDLRuntime::child(42, $ident);
|
||||||
|
}
|
||||||
|
ManagerSDLRuntime::child(42, 'SDLStatus', 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSDLByDefaultInactiveDoesNotRequireSources(): void
|
||||||
|
{
|
||||||
|
self::assertFalse($this->manager->testProperties['SDLAktiv']);
|
||||||
|
self::assertFalse($this->manager->testProperties['SDLEnergieflussAnzeigen']);
|
||||||
|
self::assertFalse($this->manager->testProperties['SDLDiagrammeAnzeigen']);
|
||||||
|
unset(ManagerSDLRuntime::$variables[104], ManagerSDLRuntime::$variables[105]);
|
||||||
|
|
||||||
|
$this->invoke('pruefeSDLKonfiguration');
|
||||||
|
$powers = $this->invoke('leseMessleistungen');
|
||||||
|
|
||||||
|
self::assertSame(0.0, $powers['SDL']);
|
||||||
|
self::assertSame(5800.0, $powers['Haus']);
|
||||||
|
self::assertNull($this->invoke('aktuelleSDLSOC'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider signedPowerProvider */
|
||||||
|
public function testSDLPowerIsExcludedFromHouseConsumption(float $raw, float $factor, float $sdl, float $house): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
|
||||||
|
ManagerSDLRuntime::variable(104, $raw);
|
||||||
|
$this->invoke('pruefeSDLKonfiguration');
|
||||||
|
|
||||||
|
self::assertSame([
|
||||||
|
'PV' => 5000.0, 'Haus' => $house, 'Netz' => 1000.0, 'Batterie' => 200.0, 'SDL' => $sdl,
|
||||||
|
], $this->invoke('leseMessleistungen'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function signedPowerProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'charging' => [1500.0, 1.0, 1500.0, 4300.0],
|
||||||
|
'discharging' => [-1500.0, 1.0, -1500.0, 7300.0],
|
||||||
|
'kilowatts' => [2.5, 1000.0, 2500.0, 3300.0],
|
||||||
|
'inverted source' => [1500.0, -1.0, -1500.0, 7300.0],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider duplicatePowerProvider */
|
||||||
|
public function testRejectsSDLPowerAlsoUsedByAnotherEnergySource(int $duplicateID): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLLeistungVariableID' => $duplicateID]);
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('nicht zugleich');
|
||||||
|
$this->invoke('pruefeSDLKonfiguration');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function duplicatePowerProvider(): array
|
||||||
|
{
|
||||||
|
return ['grid' => [101], 'PV' => [102], 'battery' => [103]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRejectsSOCUsingThePowerVariable(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 104]);
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('separate numerische Variable');
|
||||||
|
$this->invoke('pruefeSDLKonfiguration');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider invalidFactorProvider */
|
||||||
|
public function testRejectsInvalidSDLFactors(float $factor): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->invoke('pruefeSDLKonfiguration');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidFactorProvider(): array
|
||||||
|
{
|
||||||
|
return ['zero' => [0.0], 'infinite' => [INF], 'not a number' => [NAN]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMissingSDLPowerFailsWithoutReturningHouseConsumption(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true]);
|
||||||
|
unset(ManagerSDLRuntime::$variables[104]);
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('nicht verfuegbar');
|
||||||
|
$this->invoke('leseMessleistungen');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider invalidPowerProvider */
|
||||||
|
public function testNonfiniteOrNonnumericPowerDoesNotBecomeZero($value): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true]);
|
||||||
|
ManagerSDLRuntime::variable(104, $value);
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('keine endliche Zahl');
|
||||||
|
$this->invoke('leseMessleistungen');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidPowerProvider(): array
|
||||||
|
{
|
||||||
|
return ['infinite' => [INF], 'negative infinite' => [-INF], 'not a number' => [NAN], 'string' => ['1500']];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testScaledSDLPowerOverflowIsRejected(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => 2.0]);
|
||||||
|
ManagerSDLRuntime::variable(104, PHP_FLOAT_MAX);
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('skalierte SDL-Istleistung');
|
||||||
|
$this->invoke('leseMessleistungen');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSDLPowerMustBeANumericVariable(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true]);
|
||||||
|
ManagerSDLRuntime::variable(104, '1500', 3);
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->invoke('pruefeSDLKonfiguration');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider socProvider */
|
||||||
|
public function testSDLSOCBoundaries(float $value, ?float $expected): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true]);
|
||||||
|
ManagerSDLRuntime::variable(105, $value);
|
||||||
|
self::assertSame($expected, $this->invoke('aktuelleSDLSOC'));
|
||||||
|
if ($expected === null) {
|
||||||
|
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function socProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'empty' => [0.0, 0.0], 'full' => [100.0, 100.0], 'normal' => [37.5, 37.5],
|
||||||
|
'negative' => [-0.1, null], 'overfull' => [100.1, null], 'infinite' => [INF, null], 'NaN' => [NAN, null],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSDLSOCMayBeOmittedAndDisappearingSourceIsIgnored(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 0]);
|
||||||
|
$this->invoke('pruefeSDLKonfiguration');
|
||||||
|
self::assertNull($this->invoke('aktuelleSDLSOC'));
|
||||||
|
|
||||||
|
$this->properties(['SDLSOCVariableID' => 105]);
|
||||||
|
unset(ManagerSDLRuntime::$variables[105]);
|
||||||
|
self::assertNull($this->invoke('aktuelleSDLSOC'));
|
||||||
|
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPowerReadingPreservesSOCErrorUntilRecovery(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true]);
|
||||||
|
ManagerSDLRuntime::variable(105, 101.0);
|
||||||
|
$this->invoke('aktuelleSDLLeistung');
|
||||||
|
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
|
||||||
|
$this->invoke('aktuelleSDLLeistung');
|
||||||
|
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
|
||||||
|
ManagerSDLRuntime::variable(105, 47.5);
|
||||||
|
$this->invoke('aktuelleSDLLeistung');
|
||||||
|
self::assertSame('Aktiv', $this->manager->GetValue('SDLStatus'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSDLSOCDoesNotChangeWeightedNormalBatterySOC(): void
|
||||||
|
{
|
||||||
|
ManagerSDLRuntime::variable(106, 20.0);
|
||||||
|
ManagerSDLRuntime::variable(107, 80.0);
|
||||||
|
$this->properties([
|
||||||
|
'SDLAktiv' => true,
|
||||||
|
'AnlagenBatterien' => $this->json([
|
||||||
|
['ID' => 'small', 'SOCVariableID' => 106, 'Nutzkapazitaet_kWh' => 10.0],
|
||||||
|
['ID' => 'large', 'SOCVariableID' => 107, 'Nutzkapazitaet_kWh' => 30.0],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
|
||||||
|
self::assertSame(70.0, $this->invoke('aktuelleSDLSOC'));
|
||||||
|
ManagerSDLRuntime::variable(105, 0.0);
|
||||||
|
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCounterDeltasExcludeSDLChargingAndIncludeDischarging(): void
|
||||||
|
{
|
||||||
|
$this->configureCounters();
|
||||||
|
$baseline = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
|
||||||
|
self::assertCount(8, $baseline);
|
||||||
|
self::assertSame(array_fill_keys(array_keys($baseline), 0.0), $baseline);
|
||||||
|
$this->advanceCounters();
|
||||||
|
|
||||||
|
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(2.1, $deltas['Hausenergie'], 0.000001);
|
||||||
|
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
|
||||||
|
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
|
||||||
|
self::assertSame(0.2, $deltas['SDLEntladenEnergie']);
|
||||||
|
self::assertEqualsWithDelta(0.4, $deltas['BatterieLadenEnergie'], 0.000001);
|
||||||
|
self::assertEqualsWithDelta(0.1, $deltas['BatterieEntladenEnergie'], 0.000001);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMissingSDLIntervalDoesNotPolluteHouseEnergyOrTheNextInterval(): void
|
||||||
|
{
|
||||||
|
$this->configureCounters();
|
||||||
|
$this->invoke('leseEnergieZaehlerDeltas');
|
||||||
|
$this->advanceCounters();
|
||||||
|
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.0, 0.0, false]);
|
||||||
|
|
||||||
|
self::assertSame(0.0, $deltas['Hausenergie']);
|
||||||
|
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
|
||||||
|
self::assertEqualsWithDelta(1.0, $deltas['NetzbezugEnergie'], 0.000001);
|
||||||
|
ManagerSDLRuntime::variable(201, 102.5);
|
||||||
|
$next = $this->invoke('leseEnergieZaehlerDeltas', [0.1, 0.0, true]);
|
||||||
|
self::assertEqualsWithDelta(0.4, $next['Hausenergie'], 0.000001);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCounterResetSuppressesHouseEnergyForActiveSDL(): void
|
||||||
|
{
|
||||||
|
$this->configureCounters();
|
||||||
|
$this->invoke('leseEnergieZaehlerDeltas');
|
||||||
|
$this->advanceCounters();
|
||||||
|
ManagerSDLRuntime::variable(202, 0.0);
|
||||||
|
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
|
||||||
|
self::assertSame(0.0, $deltas['Hausenergie']);
|
||||||
|
self::assertSame(0.0, $deltas['NetzbezugEnergie']);
|
||||||
|
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithoutConfiguredCountersReturnsNull(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true]);
|
||||||
|
self::assertNull($this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testChangingSDLSourceResetsBaselinesButKeepsCollectedEnergy(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true]);
|
||||||
|
$this->invoke('synchronisiereSDLMesskonfiguration');
|
||||||
|
$this->manager->testAttributes['MessungLetzterZeitstempel'] = 123;
|
||||||
|
$this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung'] = 100;
|
||||||
|
$this->manager->testAttributes['MessungZaehlerstaende'] = '{"pv:pv":5.0}';
|
||||||
|
$this->manager->testAttributes['MessungEnergiePuffer'] = '{"Hausenergie":1.5}';
|
||||||
|
|
||||||
|
$this->invoke('synchronisiereSDLMesskonfiguration');
|
||||||
|
self::assertSame(123, $this->manager->testAttributes['MessungLetzterZeitstempel']);
|
||||||
|
$this->properties(['SDLLeistungsfaktor' => -1.0]);
|
||||||
|
$this->invoke('synchronisiereSDLMesskonfiguration');
|
||||||
|
|
||||||
|
self::assertSame(0, $this->manager->testAttributes['MessungLetzterZeitstempel']);
|
||||||
|
self::assertSame(0, $this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung']);
|
||||||
|
self::assertSame('{}', $this->manager->testAttributes['MessungZaehlerstaende']);
|
||||||
|
self::assertSame('{"Hausenergie":1.5}', $this->manager->testAttributes['MessungEnergiePuffer']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider chartProvider */
|
||||||
|
public function testChartMergePreservesCustomizationsAndOnlyRemovesSDL(bool $energy): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLDiagrammeAnzeigen' => true]);
|
||||||
|
$powerIdent = $energy ? 'SDLLadenEnergie' : 'SDLLeistungArchiv';
|
||||||
|
$custom = ['variableID' => 999, 'title' => 'Custom series', 'strokeColor' => '#ABCDEF'];
|
||||||
|
$sdl = ['variableID' => $this->manager->GetIDForIdent($powerIdent), 'title' => 'Own SDL name', 'visible' => false];
|
||||||
|
ManagerSDLRuntime::$media[301] = base64_encode($this->json([
|
||||||
|
'datasets' => [$custom, $sdl], 'customOption' => ['keep' => true],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
|
||||||
|
$chart = $this->chart();
|
||||||
|
self::assertSame($custom, $chart['datasets'][0]);
|
||||||
|
self::assertSame($sdl, $chart['datasets'][1]);
|
||||||
|
self::assertSame(['keep' => true], $chart['customOption']);
|
||||||
|
self::assertCount($energy ? 4 : 3, $chart['datasets']);
|
||||||
|
$soc = $chart['datasets'][count($chart['datasets']) - 1];
|
||||||
|
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $soc['variableID']);
|
||||||
|
self::assertSame('right', $soc['side']);
|
||||||
|
if ($energy) {
|
||||||
|
self::assertSame('bar', $chart['datasets'][2]['type']);
|
||||||
|
self::assertSame($this->manager->GetIDForIdent('SDLEntladenEnergie'), $chart['datasets'][2]['variableID']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$writes = ManagerSDLRuntime::$writes;
|
||||||
|
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
|
||||||
|
self::assertSame($writes, ManagerSDLRuntime::$writes);
|
||||||
|
|
||||||
|
$this->properties(['SDLDiagrammeAnzeigen' => false]);
|
||||||
|
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
|
||||||
|
self::assertSame([$custom], $this->chart()['datasets']);
|
||||||
|
self::assertSame(['keep' => true], $this->chart()['customOption']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function chartProvider(): array
|
||||||
|
{
|
||||||
|
return ['power chart' => [false], 'energy chart' => [true]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnergyFlowMergePreservesCustomNodesAndIsIdempotent(): void
|
||||||
|
{
|
||||||
|
$this->properties(['SDLAktiv' => true, 'SDLEnergieflussAnzeigen' => true]);
|
||||||
|
$custom = ['VariableID' => 999, 'Name' => 'Custom consumer', 'Type' => 'consumer', 'Invert' => true];
|
||||||
|
ManagerSDLRuntime::$properties[302]['Config'] = $this->json([$custom]);
|
||||||
|
|
||||||
|
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
|
||||||
|
$nodes = $this->flow();
|
||||||
|
self::assertSame($custom, $nodes[0]);
|
||||||
|
self::assertCount(2, $nodes);
|
||||||
|
self::assertSame('storage', $nodes[1]['Type']);
|
||||||
|
self::assertSame($this->manager->GetIDForIdent('EnergieflussSDL'), $nodes[1]['VariableID']);
|
||||||
|
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $nodes[1]['InfoVariableID']);
|
||||||
|
self::assertTrue(ManagerSDLRuntime::$hidden[$nodes[1]['VariableID']]);
|
||||||
|
|
||||||
|
$nodes[1]['Name'] = 'My SDL';
|
||||||
|
$nodes[1]['Invert'] = true;
|
||||||
|
ManagerSDLRuntime::$properties[302]['Config'] = $this->json($nodes);
|
||||||
|
$writes = ManagerSDLRuntime::$writes;
|
||||||
|
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
|
||||||
|
self::assertSame($nodes, $this->flow());
|
||||||
|
self::assertSame($writes, ManagerSDLRuntime::$writes);
|
||||||
|
|
||||||
|
$this->properties(['SDLSOCVariableID' => 0]);
|
||||||
|
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
|
||||||
|
self::assertSame(1, $this->flow()[1]['InfoVariableID']);
|
||||||
|
self::assertSame('My SDL', $this->flow()[1]['Name']);
|
||||||
|
|
||||||
|
$this->properties(['SDLAktiv' => false]);
|
||||||
|
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
|
||||||
|
self::assertSame([$custom], $this->flow());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureCounters(): void
|
||||||
|
{
|
||||||
|
$this->properties([
|
||||||
|
'SDLAktiv' => true,
|
||||||
|
'NetzbezugEnergieVariableID' => 202,
|
||||||
|
'NetzeinspeisungEnergieVariableID' => 203,
|
||||||
|
'AnlagenWechselrichter' => $this->json([
|
||||||
|
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102, 'ErzeugungsenergieVariableID' => 201],
|
||||||
|
]),
|
||||||
|
'AnlagenBatterien' => $this->json([
|
||||||
|
['ID' => 'battery', 'LeistungVariableID' => 103, 'LadeenergieVariableID' => 204, 'EntladeenergieVariableID' => 205],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
foreach ([201 => 100.0, 202 => 10.0, 203 => 5.0, 204 => 10.0, 205 => 8.0] as $id => $value) {
|
||||||
|
ManagerSDLRuntime::variable($id, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function advanceCounters(): void
|
||||||
|
{
|
||||||
|
foreach ([201 => 102.0, 202 => 11.0, 203 => 5.2, 204 => 10.4, 205 => 8.1] as $id => $value) {
|
||||||
|
ManagerSDLRuntime::variable($id, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function properties(array $properties): void
|
||||||
|
{
|
||||||
|
$this->manager->testProperties = array_replace($this->manager->testProperties, $properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function invoke(string $name, array $arguments = [])
|
||||||
|
{
|
||||||
|
$method = new ReflectionMethod(Manager::class, $name);
|
||||||
|
if (PHP_VERSION_ID < 80100) {
|
||||||
|
$method->setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $method->invokeArgs($this->manager, $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function json(array $value): string
|
||||||
|
{
|
||||||
|
return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function chart(): array
|
||||||
|
{
|
||||||
|
return json_decode(base64_decode(ManagerSDLRuntime::$media[301]), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function flow(): array
|
||||||
|
{
|
||||||
|
return json_decode(ManagerSDLRuntime::$properties[302]['Config'], true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/** Minimal Symcon runtime, loaded only inside isolated ManagerSDLTest processes. */
|
||||||
|
final class ManagerSDLRuntime
|
||||||
|
{
|
||||||
|
public static array $variables = [];
|
||||||
|
public static array $children = [];
|
||||||
|
public static array $properties = [];
|
||||||
|
public static array $media = [];
|
||||||
|
public static array $writes = [];
|
||||||
|
public static array $hidden = [];
|
||||||
|
private static int $nextID = 10000;
|
||||||
|
|
||||||
|
public static function reset(): void
|
||||||
|
{
|
||||||
|
self::$variables = [];
|
||||||
|
self::$children = [];
|
||||||
|
self::$properties = [];
|
||||||
|
self::$media = [];
|
||||||
|
self::$writes = [];
|
||||||
|
self::$hidden = [];
|
||||||
|
self::$nextID = 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function variable(int $id, $value, int $type = 2): int
|
||||||
|
{
|
||||||
|
self::$variables[$id] = ['VariableType' => $type, 'Value' => $value];
|
||||||
|
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function child(int $parent, string $ident, int $type = 2): int
|
||||||
|
{
|
||||||
|
if (!isset(self::$children[$parent][$ident])) {
|
||||||
|
$id = self::$nextID++;
|
||||||
|
self::$children[$parent][$ident] = $id;
|
||||||
|
self::variable($id, $type === 3 ? '' : 0.0, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$children[$parent][$ident];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IPSModule
|
||||||
|
{
|
||||||
|
public int $InstanceID;
|
||||||
|
public array $testProperties = [];
|
||||||
|
public array $testAttributes = [];
|
||||||
|
|
||||||
|
public function __construct(int $instanceID)
|
||||||
|
{
|
||||||
|
$this->InstanceID = $instanceID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Create(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __call(string $method, array $arguments)
|
||||||
|
{
|
||||||
|
if (preg_match('/^(Register|Read|Write)(Property|Attribute)(Boolean|Integer|Float|String)$/', $method, $parts)) {
|
||||||
|
$storage = $parts[2] === 'Property' ? 'testProperties' : 'testAttributes';
|
||||||
|
$ident = $arguments[0];
|
||||||
|
if ($parts[1] === 'Register') {
|
||||||
|
$this->{$storage}[$ident] = $this->{$storage}[$ident] ?? $arguments[1];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($parts[1] === 'Write') {
|
||||||
|
$this->{$storage}[$ident] = $arguments[1];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!array_key_exists($ident, $this->{$storage})) {
|
||||||
|
throw new RuntimeException('Unregistered test property or attribute: ' . $ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->{$storage}[$ident];
|
||||||
|
}
|
||||||
|
if (preg_match('/^RegisterVariable(Boolean|Integer|Float|String)$/', $method, $parts)) {
|
||||||
|
$types = ['Boolean' => 0, 'Integer' => 1, 'Float' => 2, 'String' => 3];
|
||||||
|
|
||||||
|
return ManagerSDLRuntime::child($this->InstanceID, $arguments[0], $types[$parts[1]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadMethodCallException('Unsupported Symcon test call: ' . $method);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function EnableAction(string $ident): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RegisterTimer(string $ident, int $interval, string $script): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetIDForIdent(string $ident): int
|
||||||
|
{
|
||||||
|
if (!isset(ManagerSDLRuntime::$children[$this->InstanceID][$ident])) {
|
||||||
|
throw new RuntimeException('Unknown test child: ' . $ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ManagerSDLRuntime::$children[$this->InstanceID][$ident];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetValue(string $ident, $value): void
|
||||||
|
{
|
||||||
|
ManagerSDLRuntime::$variables[$this->GetIDForIdent($ident)]['Value'] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetValue(string $ident)
|
||||||
|
{
|
||||||
|
return GetValue($this->GetIDForIdent($ident));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_VariableExists(int $variableID): bool
|
||||||
|
{
|
||||||
|
return isset(ManagerSDLRuntime::$variables[$variableID]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_GetVariable(int $variableID): array
|
||||||
|
{
|
||||||
|
if (!IPS_VariableExists($variableID)) {
|
||||||
|
throw new RuntimeException('Unknown test variable: ' . $variableID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ManagerSDLRuntime::$variables[$variableID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetValue(int $variableID)
|
||||||
|
{
|
||||||
|
return IPS_GetVariable($variableID)['Value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_GetObjectIDByIdent(string $ident, int $parentID)
|
||||||
|
{
|
||||||
|
return ManagerSDLRuntime::$children[$parentID][$ident] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_GetProperty(int $instanceID, string $property)
|
||||||
|
{
|
||||||
|
return ManagerSDLRuntime::$properties[$instanceID][$property];
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_SetProperty(int $instanceID, string $property, $value): void
|
||||||
|
{
|
||||||
|
ManagerSDLRuntime::$properties[$instanceID][$property] = $value;
|
||||||
|
ManagerSDLRuntime::$writes[] = ['property', $instanceID, $property];
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_ApplyChanges(int $instanceID): void
|
||||||
|
{
|
||||||
|
ManagerSDLRuntime::$writes[] = ['apply', $instanceID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_GetMediaContent(int $mediaID): string
|
||||||
|
{
|
||||||
|
return ManagerSDLRuntime::$media[$mediaID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_SetMediaContent(int $mediaID, string $content): void
|
||||||
|
{
|
||||||
|
ManagerSDLRuntime::$media[$mediaID] = $content;
|
||||||
|
ManagerSDLRuntime::$writes[] = ['media', $mediaID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPS_SetHidden(int $objectID, bool $hidden): void
|
||||||
|
{
|
||||||
|
ManagerSDLRuntime::$hidden[$objectID] = $hidden;
|
||||||
|
}
|
||||||
@@ -431,6 +431,97 @@ return static function (TestContext $test): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$test->runCase('SDL korrigiert Hauslast und ergaenzt bestehende Anzeigen idempotent', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($managerId): void {
|
||||||
|
$leistungID = $test->createVariable(2, 'SDL Istleistung', -1.2);
|
||||||
|
$socID = $test->createVariable(2, 'SDL SOC', 47.5);
|
||||||
|
$flussID = $test->objectByIdent('Energiefluss', $managerId);
|
||||||
|
$chartID = $test->objectByIdent('Leistungsdiagramm', $managerId);
|
||||||
|
$flussVorher = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
$chartVorher = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
try {
|
||||||
|
IPS_SetProperty($managerId, 'SDLAktiv', true);
|
||||||
|
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $leistungID);
|
||||||
|
IPS_SetProperty($managerId, 'SDLLeistungsfaktor', -1000.0);
|
||||||
|
IPS_SetProperty($managerId, 'SDLSOCVariableID', $socID);
|
||||||
|
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', true);
|
||||||
|
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
|
||||||
|
$test->assertSame(1200.0, GetValue($test->objectByIdent('SDLLeistungArchiv', $managerId)));
|
||||||
|
$test->assertSame(47.5, GetValue($test->objectByIdent('SDLSOCArchiv', $managerId)));
|
||||||
|
$test->assertSame(1.2, GetValue($test->objectByIdent('EnergieflussSDL', $managerId)));
|
||||||
|
$test->assertSame('Aktiv', GetValue($test->objectByIdent('SDLStatus', $managerId)));
|
||||||
|
$fluss = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
$test->assertSame($flussVorher, array_slice($fluss, 0, count($flussVorher)));
|
||||||
|
$test->assertSame(count($flussVorher) + 1, count($fluss));
|
||||||
|
$test->assertSame($test->objectByIdent('SDLSOCArchiv', $managerId), $fluss[count($fluss) - 1]['InfoVariableID']);
|
||||||
|
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
$test->assertSame($chartVorher['datasets'], array_slice($chart['datasets'], 0, count($chartVorher['datasets'])));
|
||||||
|
$test->assertSame('right', $chart['datasets'][count($chart['datasets']) - 1]['side']);
|
||||||
|
$test->assertSame('~Intensity.100', $chart['datasets'][count($chart['datasets']) - 1]['profile']);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
$test->assertSame($fluss, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
|
||||||
|
$test->assertSame($chart, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
foreach ([0 => ['~Power', 1200.0], 1 => ['Enelix.Power.kW', 1.2]] as $einheit => $erwartet) {
|
||||||
|
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', false);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', $einheit);
|
||||||
|
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', true);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
$flussID = $test->objectByIdent('Energiefluss', $managerId);
|
||||||
|
$sdlID = $test->objectByIdent('EnergieflussSDL', $managerId);
|
||||||
|
$test->assertSame($erwartet[0], IPS_GetVariable($sdlID)['VariableProfile']);
|
||||||
|
$test->assertSame($erwartet[1], GetValue($sdlID));
|
||||||
|
}
|
||||||
|
|
||||||
|
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
|
||||||
|
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
$test->assertSame($flussVorher, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
|
||||||
|
$test->assertSame($chartVorher, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
|
||||||
|
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
|
||||||
|
$test->assertEquals(1.9, GetValue($test->objectByIdent('EnergieflussBatterie', $managerId)), 0.000001);
|
||||||
|
|
||||||
|
IPS_SetProperty($managerId, 'SDLSOCVariableID', 0);
|
||||||
|
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
$test->assertSame(count($chartVorher['datasets']) + 1, count($chart['datasets']));
|
||||||
|
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', false);
|
||||||
|
$test->assertTrue(IPS_ApplyChanges($managerId));
|
||||||
|
$archivID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
|
||||||
|
foreach (['SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie'] as $ident) {
|
||||||
|
$test->assertSame(false, AC_GetLoggingStatus($archivID, $test->objectByIdent($ident, $managerId)));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', true);
|
||||||
|
IPS_SetProperty($managerId, 'SDLAktiv', false);
|
||||||
|
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
|
||||||
|
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
}
|
||||||
|
$test->assertSame(2700.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
|
||||||
|
});
|
||||||
|
$test->runCase('SDL weist fehlende und doppelte Leistungsquellen zurueck', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($managerId, $batterieLeistungId): void {
|
||||||
|
try {
|
||||||
|
IPS_SetProperty($managerId, 'SDLAktiv', true);
|
||||||
|
foreach ([0, $batterieLeistungId] as $quelle) {
|
||||||
|
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $quelle);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
$test->assertInstanceStatus($managerId, 202);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IPS_SetProperty($managerId, 'SDLAktiv', false);
|
||||||
|
IPS_SetProperty($managerId, 'SDLLeistungVariableID', 0);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$zustandswert = static function (array $daten, int $instanzId, string $kennung) {
|
$zustandswert = static function (array $daten, int $instanzId, string $kennung) {
|
||||||
foreach ($daten['Verbraucher'] ?? [] as $verbraucher) {
|
foreach ($daten['Verbraucher'] ?? [] as $verbraucher) {
|
||||||
if (($verbraucher['InstanzID'] ?? 0) !== $instanzId) {
|
if (($verbraucher['InstanzID'] ?? 0) !== $instanzId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user