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

This commit is contained in:
dh
2026-09-27 19:29:37 +00:00
parent 40f41a2fb8
commit 708a8ecea2
11 changed files with 1426 additions and 56 deletions
+126 -13
View File
@@ -321,6 +321,13 @@
"add": 0, "add": 0,
"edit": {"type": "SelectVariable"} "edit": {"type": "SelectVariable"}
}, },
{
"caption": "Messfaktor",
"name": "Istleistungsfaktor",
"width": "110px",
"add": 1.0,
"edit": {"type": "NumberSpinner", "digits": 4}
},
{ {
"caption": "PV-Begrenzung", "caption": "PV-Begrenzung",
"name": "BegrenzungVariableID", "name": "BegrenzungVariableID",
@@ -534,6 +541,27 @@
{"caption": "Hybrid", "value": "hybrid"} {"caption": "Hybrid", "value": "hybrid"}
] ]
} }
},
{
"caption": "Leistung",
"name": "LeistungVariableID",
"width": "140px",
"add": 0,
"edit": {"type": "SelectVariable"}
},
{
"caption": "Messfaktor",
"name": "Leistungsfaktor",
"width": "110px",
"add": 1.0,
"edit": {"type": "NumberSpinner", "digits": 4}
},
{
"caption": "Ladezustand",
"name": "SOCVariableID",
"width": "140px",
"add": 0,
"edit": {"type": "SelectVariable"}
} }
] ]
} }
@@ -591,24 +619,109 @@
} }
] ]
}, },
{
"type": "ExpansionPanel",
"caption": "Energieaufzeichnung",
"expanded": true,
"items": [
{
"type": "CheckBox",
"name": "EnergieaufzeichnungAktiv",
"caption": "Leistungs- und Energiewerte automatisch aufzeichnen"
},
{
"type": "Label",
"caption": "PV und Batterie werden aus der Anlagentopologie summiert. Der Hausverbrauch wird aus PV + Netz - Batterie berechnet. Netz: positiv Bezug; Batterie: positiv Laden."
},
{
"type": "CheckBox",
"name": "MesswerteAnzeigen",
"caption": "Einzelne Leistungs- und Energiewerte unter dem Manager anzeigen"
},
{
"type": "RowLayout",
"items": [
{
"type": "Select",
"name": "LeistungsaufzeichnungMinuten",
"caption": "Leistung verdichten",
"options": [
{"caption": "1 Minute", "value": 1},
{"caption": "5 Minuten", "value": 5},
{"caption": "1 Stunde", "value": 60}
]
},
{
"type": "NumberSpinner",
"name": "LeistungLoeschenMonate",
"caption": "Leistung loeschen nach",
"minimum": 0,
"suffix": " Monaten (0 = nie)"
}
]
},
{
"type": "RowLayout",
"items": [
{
"type": "Select",
"name": "EnergieaufzeichnungMinuten",
"caption": "Energie verdichten",
"options": [
{"caption": "1 Minute", "value": 1},
{"caption": "5 Minuten", "value": 5},
{"caption": "1 Stunde", "value": 60}
]
},
{
"type": "NumberSpinner",
"name": "EnergieVerdichtenMonate",
"caption": "Auf Tageswerte verdichten nach",
"minimum": 0,
"suffix": " Monaten"
},
{
"type": "NumberSpinner",
"name": "EnergieLoeschenMonate",
"caption": "Energie loeschen nach",
"minimum": 0,
"suffix": " Monaten (0 = nie)"
}
]
},
{
"type": "CheckBox",
"name": "EnergyPieAnzeigen",
"caption": "Energy Pie unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormEnergyPieAnzeigen', $EnergyPieAnzeigen);"
},
{
"type": "CheckBox",
"name": "EnergiediagrammeAnzeigen",
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormEnergiediagrammeAnzeigen', $EnergiediagrammeAnzeigen);"
},
{
"type": "CheckBox",
"name": "FunFactsAnzeigen",
"caption": "Energy Facts unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormFunFactsAnzeigen', $FunFactsAnzeigen);"
},
{
"type": "CheckBox",
"name": "EnergieflussAnzeigen",
"caption": "Energiefluss unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);"
}
]
},
{ {
"type": "ExpansionPanel", "type": "ExpansionPanel",
"caption": "Prognose / Forecast", "caption": "Prognose / Forecast",
"items": [ "items": [
{ {
"type": "SelectVariable", "type": "Label",
"name": "PrognosePVVariableID", "caption": "Forecast verwendet dieselben Messquellen aus der Anlagentopologie und den daraus berechneten Hausverbrauch."
"caption": "PV-Leistung"
},
{
"type": "SelectVariable",
"name": "PrognoseHausverbrauchVariableID",
"caption": "Hausverbrauch"
},
{
"type": "SelectVariable",
"name": "PrognoseSOCVariableID",
"caption": "Batterie-Ladezustand"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
+378 -27
View File
@@ -9,9 +9,13 @@ require_once __DIR__ . '/../libs/EinspeiseRegler.php';
require_once __DIR__ . '/../libs/Lizenzpruefung.php'; require_once __DIR__ . '/../libs/Lizenzpruefung.php';
require_once __DIR__ . '/../libs/Anlagentopologie.php'; 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/ManagerEnergieTrait.php';
use Belevo\EnelixEMS\Anlagentopologie; use Belevo\EnelixEMS\Anlagentopologie;
use Belevo\EnelixEMS\EinspeiseRegler; use Belevo\EnelixEMS\EinspeiseRegler;
use Belevo\EnelixEMS\EnergieMessung;
use Belevo\EnelixEMS\ManagerEnergieTrait;
use Belevo\EnelixEMS\Lizenzpruefung; use Belevo\EnelixEMS\Lizenzpruefung;
use Belevo\EnelixEMS\ManagerRegler; use Belevo\EnelixEMS\ManagerRegler;
use Belevo\EnelixEMS\ManagerSchnittstelle; use Belevo\EnelixEMS\ManagerSchnittstelle;
@@ -20,6 +24,8 @@ use Belevo\EnelixEMS\StoerungsSnapshot;
class Manager extends IPSModule implements ManagerSchnittstelle class Manager extends IPSModule implements ManagerSchnittstelle
{ {
use ManagerEnergieTrait;
private const STATUS_AKTIV = 102; private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201; private const STATUS_MESSWERT_UNGUELTIG = 201;
private const STATUS_KONFIGURATION_UNGUELTIG = 202; private const STATUS_KONFIGURATION_UNGUELTIG = 202;
@@ -32,6 +38,23 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private const LIZENZ_RETRY_SEKUNDEN = 3600; private const LIZENZ_RETRY_SEKUNDEN = 3600;
private const STOERUNGS_RETRY_MAX_SEKUNDEN = 900; private const STOERUNGS_RETRY_MAX_SEKUNDEN = 900;
private const VM_UPDATE = 10603; private const VM_UPDATE = 10603;
private const ARCHIVE_MODULE_ID = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
private const ENERGY_PIE_MODULE_ID = '{F73B8F8D-5E36-4B23-B379-4632A517A1D6}';
private const ENERGY_FLOW_MODULE_ID = '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}';
private const LEISTUNGS_VARIABLEN = [
'PVLeistungArchiv',
'HausverbrauchLeistungArchiv',
'NetzleistungArchiv',
'BatterieleistungArchiv',
];
private const ENERGIE_VARIABLEN = [
'PVEnergie',
'Hausenergie',
'NetzbezugEnergie',
'EinspeisungEnergie',
'BatterieLadenEnergie',
'BatterieEntladenEnergie',
];
private const VERBRAUCHER_LIZENZEN = [ private const VERBRAUCHER_LIZENZEN = [
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [ '{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
'Katalog' => 'boiler_multi', 'Katalog' => 'boiler_multi',
@@ -112,6 +135,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyString('AnlagenWechselrichter', '[]'); $this->RegisterPropertyString('AnlagenWechselrichter', '[]');
$this->RegisterPropertyString('AnlagenPVFlaechen', '[]'); $this->RegisterPropertyString('AnlagenPVFlaechen', '[]');
$this->RegisterPropertyString('AnlagenBatterien', '[]'); $this->RegisterPropertyString('AnlagenBatterien', '[]');
// Verdeckte Altproperties; Messquellen liegen neu ausschliesslich in der Anlagentopologie.
$this->RegisterPropertyInteger('PrognosePVVariableID', 0); $this->RegisterPropertyInteger('PrognosePVVariableID', 0);
$this->RegisterPropertyInteger('PrognoseHausverbrauchVariableID', 0); $this->RegisterPropertyInteger('PrognoseHausverbrauchVariableID', 0);
$this->RegisterPropertyInteger('PrognoseSOCVariableID', 0); $this->RegisterPropertyInteger('PrognoseSOCVariableID', 0);
@@ -127,6 +151,24 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('StoerungsSendeintervall', 300); $this->RegisterPropertyInteger('StoerungsSendeintervall', 300);
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
$this->RegisterPropertyBoolean('LoggingEin', false); $this->RegisterPropertyBoolean('LoggingEin', false);
$this->RegisterPropertyBoolean('EnergieaufzeichnungAktiv', true);
// Verdeckte Altproperties; bestehende Konfigurationen bleiben lesbar.
$this->RegisterPropertyInteger('MessungPVLeistungVariableID', 0);
$this->RegisterPropertyInteger('MessungHausverbrauchLeistungVariableID', 0);
$this->RegisterPropertyInteger('MessungBatterieleistungVariableID', 0);
$this->RegisterPropertyFloat('MessungPVLeistungsfaktor', 1.0);
$this->RegisterPropertyFloat('MessungHausverbrauchLeistungsfaktor', 1.0);
$this->RegisterPropertyFloat('MessungBatterieleistungsfaktor', 1.0);
$this->RegisterPropertyBoolean('MesswerteAnzeigen', false);
$this->RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1);
$this->RegisterPropertyInteger('LeistungLoeschenMonate', 12);
$this->RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5);
$this->RegisterPropertyInteger('EnergieVerdichtenMonate', 12);
$this->RegisterPropertyInteger('EnergieLoeschenMonate', 0);
$this->RegisterPropertyBoolean('EnergyPieAnzeigen', false);
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false);
$this->RegisterPropertyBoolean('FunFactsAnzeigen', false);
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10); $this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
$this->EnableAction('Aktiv'); $this->EnableAction('Aktiv');
@@ -150,6 +192,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeInteger('StoerungsLetzterErfolg', 0); $this->RegisterAttributeInteger('StoerungsLetzterErfolg', 0);
$this->RegisterAttributeInteger('StoerungsNaechsterVersuch', 0); $this->RegisterAttributeInteger('StoerungsNaechsterVersuch', 0);
$this->RegisterAttributeInteger('StoerungsFehlversuche', 0); $this->RegisterAttributeInteger('StoerungsFehlversuche', 0);
$this->RegisterAttributeInteger('MessungLetzterZeitstempel', 0);
$this->RegisterAttributeInteger('MessungLetzteLeistungsaufzeichnung', 0);
$this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0);
$this->RegisterAttributeString('MessungEnergiePuffer', '{}');
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren. // Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
$this->RegisterTimer('Regelzyklus', 0, ''); $this->RegisterTimer('Regelzyklus', 0, '');
@@ -169,6 +215,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'StoerungenSenden', true);" "IPS_RequestAction(\$_IPS['TARGET'], 'StoerungenSenden', true);"
); );
$this->RegisterTimer(
'MesswerteAufzeichnen',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'MesswerteAufzeichnen', true);"
);
} }
public function ApplyChanges(): void public function ApplyChanges(): void
@@ -180,6 +231,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
$this->konfiguriereEnergiearchiv();
$this->synchronisiereEnergievisualisierungen();
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('Regelzyklus', 0);
@@ -187,6 +240,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->SetTimerInterval('KeepAlive', 0); $this->SetTimerInterval('KeepAlive', 0);
$this->SetTimerInterval('PrognoseSenden', 0); $this->SetTimerInterval('PrognoseSenden', 0);
$this->SetTimerInterval('StoerungenSenden', 0); $this->SetTimerInterval('StoerungenSenden', 0);
$this->SetTimerInterval('MesswerteAufzeichnen', 0);
$this->protokolliere('Konfiguration', $fehler->getMessage()); $this->protokolliere('Konfiguration', $fehler->getMessage());
return; return;
} }
@@ -200,6 +254,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'StoerungenSenden', 'StoerungenSenden',
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 30000 : 0 $this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 30000 : 0
); );
$messungAktiv = $this->messungKonfiguriert();
$this->SetTimerInterval('MesswerteAufzeichnen', $messungAktiv ? 60000 : 0);
if ($messungAktiv) {
$this->zeichneMesswerteAuf();
}
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen')); $this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
$this->aktualisiereAnschlussstatus(); $this->aktualisiereAnschlussstatus();
$geraetezugangFehlt = ($this->hatAnlagentopologie() $geraetezugangFehlt = ($this->hatAnlagentopologie()
@@ -378,6 +437,26 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->sendeStoerungsSnapshot(false); $this->sendeStoerungsSnapshot(false);
return; return;
case 'MesswerteAufzeichnen':
$this->zeichneMesswerteAuf();
return;
case 'FormEnergyPieAnzeigen':
$this->synchronisiereEnergyPie((bool) $wert);
return;
case 'FormEnergiediagrammeAnzeigen':
$this->synchronisiereEnergiediagramme((bool) $wert);
return;
case 'FormFunFactsAnzeigen':
$this->synchronisiereFunFacts((bool) $wert);
return;
case 'FormEnergieflussAnzeigen':
$this->synchronisiereEnergiefluss((bool) $wert);
return;
case 'FormLizenzPruefen': case 'FormLizenzPruefen':
if (!is_string($wert)) { if (!is_string($wert)) {
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.'); throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
@@ -1430,6 +1509,32 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function aktualisiereVariablen(): void private function aktualisiereVariablen(): void
{ {
$leistungsvariablen = [
'PVLeistungArchiv' => 'PV-Leistung',
'HausverbrauchLeistungArchiv' => 'Hausverbrauch',
'NetzleistungArchiv' => 'Netzleistung Aufzeichnung',
'BatterieleistungArchiv' => 'Batterieleistung',
];
$energievariablen = [
'PVEnergie' => 'PV-Energie',
'Hausenergie' => 'Hausenergie',
'NetzbezugEnergie' => 'Netzbezug Energie',
'EinspeisungEnergie' => 'Einspeisung Energie',
'BatterieLadenEnergie' => 'Batterie geladen',
'BatterieEntladenEnergie' => 'Batterie entladen',
];
$position = 200;
foreach ($leistungsvariablen as $ident => $name) {
$this->RegisterVariableFloat($ident, $name, '~Power', $position);
IPS_SetHidden($this->GetIDForIdent($ident), !$this->ReadPropertyBoolean('MesswerteAnzeigen'));
$position += 10;
}
foreach ($energievariablen as $ident => $name) {
$this->RegisterVariableFloat($ident, $name, '~Electricity', $position);
IPS_SetHidden($this->GetIDForIdent($ident), !$this->ReadPropertyBoolean('MesswerteAnzeigen'));
$position += 10;
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) { if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'NetzleistungGueltig', 'NetzleistungGueltig',
@@ -1495,6 +1600,249 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
} }
private function messungKonfiguriert(): bool
{
return $this->ReadPropertyBoolean('EnergieaufzeichnungAktiv')
&& $this->messquellenKonfiguriert();
}
private function messquellenKonfiguriert(): bool
{
try {
return $this->istNumerischeVariable($this->ReadPropertyInteger('NetzleistungVariableID'))
&& $this->pvMessquellen() !== [];
} catch (Throwable $fehler) {
return false;
}
}
/** @return list<array{VariableID: int, Faktor: float}> */
private function pvMessquellen(): array
{
$wechselrichter = $this->leseAnlagenKonfigurationsliste('AnlagenWechselrichter');
$pvFlaechen = $this->leseAnlagenKonfigurationsliste('AnlagenPVFlaechen');
$verwendeteWechselrichter = [];
foreach ($pvFlaechen as $pvFlaeche) {
$id = trim((string) ($pvFlaeche['WechselrichterID'] ?? ''));
if ($id !== '') {
$verwendeteWechselrichter[$id] = true;
}
}
$quellen = [];
foreach ($wechselrichter as $index => $eintrag) {
$typ = (string) ($eintrag['Typ'] ?? '');
$id = trim((string) ($eintrag['ID'] ?? ''));
if ($verwendeteWechselrichter !== []) {
if (!isset($verwendeteWechselrichter[$id])) {
continue;
}
} elseif (!in_array($typ, ['pv', 'hybrid'], true)) {
continue;
}
$variableID = $eintrag['IstleistungVariableID'] ?? 0;
if ($variableID === 0) {
continue;
}
if (!is_int($variableID) || !$this->istNumerischeVariable($variableID)) {
throw new InvalidArgumentException(
'Wechselrichter ' . ($index + 1) . ': Istleistung muss eine numerische Variable sein.'
);
}
$quellen[] = [
'VariableID' => $variableID,
'Faktor' => $this->anlagenMessfaktor(
$eintrag['Istleistungsfaktor'] ?? 1.0,
'Wechselrichter ' . ($index + 1)
),
];
}
return $quellen;
}
/** @return list<array{VariableID: int, Faktor: float}> */
private function batterieMessquellen(): array
{
$quellen = [];
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenBatterien') as $index => $eintrag) {
$variableID = $eintrag['LeistungVariableID'] ?? 0;
if ($variableID === 0) {
continue;
}
if (!is_int($variableID) || !$this->istNumerischeVariable($variableID)) {
throw new InvalidArgumentException(
'Batterie ' . ($index + 1) . ': Leistung muss eine numerische Variable sein.'
);
}
$quellen[] = [
'VariableID' => $variableID,
'Faktor' => $this->anlagenMessfaktor(
$eintrag['Leistungsfaktor'] ?? 1.0,
'Batterie ' . ($index + 1)
),
];
}
return $quellen;
}
/** @return list<array{VariableID: int, Gewicht: float}> */
private function batterieSocQuellen(): array
{
$quellen = [];
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenBatterien') as $index => $eintrag) {
$variableID = $eintrag['SOCVariableID'] ?? 0;
if ($variableID === 0) {
continue;
}
if (!is_int($variableID) || !$this->istNumerischeVariable($variableID)) {
throw new InvalidArgumentException(
'Batterie ' . ($index + 1) . ': SOC muss eine numerische Variable sein.'
);
}
$kapazitaet = $eintrag['Nutzkapazitaet_kWh'] ?? 0.0;
$quellen[] = [
'VariableID' => $variableID,
'Gewicht' => is_numeric($kapazitaet) && (float) $kapazitaet > 0.0
? (float) $kapazitaet
: 1.0,
];
}
return $quellen;
}
/** @return list<array<string, mixed>> */
private function leseAnlagenKonfigurationsliste(string $property): array
{
$liste = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($liste) || !array_is_list($liste)) {
throw new InvalidArgumentException($property . ' muss eine JSON-Liste sein.');
}
return array_values(array_filter($liste, 'is_array'));
}
/** @param mixed $wert */
private function anlagenMessfaktor($wert, string $bezeichnung): float
{
if (!is_int($wert) && !is_float($wert)) {
throw new InvalidArgumentException($bezeichnung . ': Messfaktor muss numerisch sein.');
}
$faktor = (float) $wert;
if (!is_finite($faktor) || $faktor === 0.0) {
throw new InvalidArgumentException($bezeichnung . ': Messfaktor muss endlich und ungleich 0 sein.');
}
return $faktor;
}
/** @return array{PV: float, Haus: float, Netz: float, Batterie: float} */
private function leseMessleistungen(): array
{
$pv = 0.0;
foreach ($this->pvMessquellen() as $quelle) {
$pv += max(0.0, (float) GetValue($quelle['VariableID']) * $quelle['Faktor']);
}
$batterie = 0.0;
foreach ($this->batterieMessquellen() as $quelle) {
$batterie += (float) GetValue($quelle['VariableID']) * $quelle['Faktor'];
}
$netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID'))
* $this->ReadPropertyFloat('Netzleistungsfaktor');
return [
'PV' => $pv,
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie),
'Netz' => $netz,
'Batterie' => $batterie,
];
}
private function aktuelleBatterieSoc(): ?float
{
$quellen = $this->batterieSocQuellen();
if ($quellen === []) {
return $this->leseAnlagenKonfigurationsliste('AnlagenBatterien') === [] ? 0.0 : null;
}
$summe = 0.0;
$gewicht = 0.0;
foreach ($quellen as $quelle) {
$wert = (float) GetValue($quelle['VariableID']);
if (!is_finite($wert) || $wert < 0.0 || $wert > 100.0) {
return null;
}
$summe += $wert * $quelle['Gewicht'];
$gewicht += $quelle['Gewicht'];
}
return $gewicht > 0.0 ? $summe / $gewicht : null;
}
private function zeichneMesswerteAuf(): void
{
if (!$this->messungKonfiguriert()) {
return;
}
$jetzt = time();
$leistungen = $this->leseMessleistungen();
$letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel');
$puffer = $this->leseJsonAttribut('MessungEnergiePuffer');
if ($letzterZeitstempel > 0) {
$sekunden = min(300, max(0, $jetzt - $letzterZeitstempel));
foreach (EnergieMessung::energieDeltas($leistungen, $sekunden) as $ident => $delta) {
$puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta;
}
}
$this->WriteAttributeInteger('MessungLetzterZeitstempel', $jetzt);
$this->WriteAttributeString(
'MessungEnergiePuffer',
json_encode($puffer, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION)
);
$leistungsintervall = $this->ReadPropertyInteger('LeistungsaufzeichnungMinuten') * 60;
$letzteLeistung = $this->ReadAttributeInteger('MessungLetzteLeistungsaufzeichnung');
if ($letzteLeistung === 0 || $jetzt - $letzteLeistung >= $leistungsintervall) {
foreach ([
'PVLeistungArchiv' => 'PV',
'HausverbrauchLeistungArchiv' => 'Haus',
'NetzleistungArchiv' => 'Netz',
'BatterieleistungArchiv' => 'Batterie',
] as $ident => $quelle) {
$this->SetValue($ident, $leistungen[$quelle]);
}
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt);
}
$energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60;
$letzteEnergie = $this->ReadAttributeInteger('MessungLetzteEnergieaufzeichnung');
if ($letzteEnergie === 0) {
$this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt);
} elseif ($jetzt - $letzteEnergie >= $energieintervall) {
foreach (self::ENERGIE_VARIABLEN as $ident) {
$this->SetValue($ident, (float) $this->GetValue($ident) + (float) ($puffer[$ident] ?? 0.0));
}
$this->WriteAttributeString('MessungEnergiePuffer', '{}');
$this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt);
}
$funFactsID = @IPS_GetObjectIDByIdent('EnergieFunFacts', $this->InstanceID);
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
$this->aktualisiereFunFacts($funFactsID);
}
}
private function istNumerischeVariable(int $variableID): bool
{
if ($variableID <= 0 || !IPS_VariableExists($variableID)) {
return false;
}
$variable = IPS_GetVariable($variableID);
return in_array((int) ($variable['VariableType'] ?? -1), [1, 2], true);
}
/** @param bool|float|int|string $wert */ /** @param bool|float|int|string $wert */
private function setzeDiagnosewert(string $ident, $wert): void private function setzeDiagnosewert(string $ident, $wert): void
{ {
@@ -1929,6 +2277,22 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
} }
} }
foreach (['LeistungsaufzeichnungMinuten', 'EnergieaufzeichnungMinuten'] as $property) {
$wert = $this->ReadPropertyInteger($property);
if (!in_array($wert, [1, 5, 60], true)) {
throw new InvalidArgumentException($property . ' muss 1, 5 oder 60 Minuten betragen.');
}
}
foreach (['LeistungLoeschenMonate', 'EnergieVerdichtenMonate', 'EnergieLoeschenMonate'] as $property) {
if ($this->ReadPropertyInteger($property) < 0) {
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
}
}
$energieLoeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate');
$energieVerdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate');
if ($energieLoeschen > 0 && $energieLoeschen <= $energieVerdichten) {
throw new InvalidArgumentException('Energie darf erst nach der taeglichen Verdichtung geloescht werden.');
}
$intervall = $this->ReadPropertyInteger('PrognoseSendeintervall'); $intervall = $this->ReadPropertyInteger('PrognoseSendeintervall');
if ($intervall < 60 || $intervall > 3600) { if ($intervall < 60 || $intervall > 3600) {
throw new InvalidArgumentException('PrognoseSendeintervall muss zwischen 60 und 3600 Sekunden liegen.'); throw new InvalidArgumentException('PrognoseSendeintervall muss zwischen 60 und 3600 Sekunden liegen.');
@@ -1937,25 +2301,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if ($stoerungsIntervall < 60 || $stoerungsIntervall > 3600) { if ($stoerungsIntervall < 60 || $stoerungsIntervall > 3600) {
throw new InvalidArgumentException('StoerungsSendeintervall muss zwischen 60 und 3600 Sekunden liegen.'); throw new InvalidArgumentException('StoerungsSendeintervall muss zwischen 60 und 3600 Sekunden liegen.');
} }
$telemetrieIDs = [
$this->ReadPropertyInteger('PrognosePVVariableID'),
$this->ReadPropertyInteger('PrognoseHausverbrauchVariableID'),
$this->ReadPropertyInteger('PrognoseSOCVariableID'),
];
$konfigurierteIDs = array_values(array_filter($telemetrieIDs, static fn (int $id): bool => $id > 0));
if ($konfigurierteIDs !== [] && count($konfigurierteIDs) !== count($telemetrieIDs)) {
throw new InvalidArgumentException('Fuer die Prognose muessen PV, Hausverbrauch und SOC gemeinsam konfiguriert sein.');
}
foreach ($konfigurierteIDs as $variableID) {
if (!IPS_VariableExists($variableID)) {
throw new InvalidArgumentException('Eine konfigurierte Prognosevariable existiert nicht.');
}
}
if ($konfigurierteIDs !== []
&& !IPS_VariableExists($this->ReadPropertyInteger('NetzleistungVariableID'))
) {
throw new InvalidArgumentException('Die Netzleistungsvariable fuer die Prognose existiert nicht.');
}
if ($this->ReadPropertyFloat('Umschaltdifferenz') < 0 if ($this->ReadPropertyFloat('Umschaltdifferenz') < 0
|| $this->ReadPropertyFloat('Umschaltdifferenz') > 100 || $this->ReadPropertyFloat('Umschaltdifferenz') > 100
) { ) {
@@ -1981,6 +2326,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung'); $this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')); $this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
$this->leseAnlagentopologie(); $this->leseAnlagentopologie();
$this->pvMessquellen();
$this->batterieMessquellen();
$this->batterieSocQuellen();
foreach (['PrognoseAnschluss', 'SDLAnschluss'] as $property) { foreach (['PrognoseAnschluss', 'SDLAnschluss'] as $property) {
$wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR); $wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($wert)) { if (!is_array($wert)) {
@@ -2011,9 +2359,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function prognoseTelemetrieKonfiguriert(): bool private function prognoseTelemetrieKonfiguriert(): bool
{ {
return $this->ReadPropertyInteger('PrognosePVVariableID') > 0 return $this->messquellenKonfiguriert()
&& $this->ReadPropertyInteger('PrognoseHausverbrauchVariableID') > 0 && $this->aktuelleBatterieSoc() !== null;
&& $this->ReadPropertyInteger('PrognoseSOCVariableID') > 0;
} }
private function aktuelleNetzfahrplanVorgabe(): ?float private function aktuelleNetzfahrplanVorgabe(): ?float
@@ -2158,11 +2505,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.'); throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.');
} }
$leistungen = $this->leseMessleistungen();
$soc = $this->aktuelleBatterieSoc();
if ($soc === null) {
throw new RuntimeException('Der Batterie-Ladezustand ist nicht verfuegbar.');
}
$werte = [ $werte = [
'PV' => GetValue($this->ReadPropertyInteger('PrognosePVVariableID')), 'PV' => $leistungen['PV'],
'Hausverbrauch' => GetValue($this->ReadPropertyInteger('PrognoseHausverbrauchVariableID')), 'Hausverbrauch' => $leistungen['Haus'],
'Netzleistung' => GetValue($this->ReadPropertyInteger('NetzleistungVariableID')), 'Netzleistung' => $leistungen['Netz'],
'SOC' => GetValue($this->ReadPropertyInteger('PrognoseSOCVariableID')), 'SOC' => $soc,
]; ];
foreach ($werte as $name => $wert) { foreach ($werte as $name => $wert) {
if (!is_int($wert) && !is_float($wert)) { if (!is_int($wert) && !is_float($wert)) {
@@ -2173,7 +2525,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new RuntimeException('Der Prognosewert ' . $name . ' ist nicht endlich.'); throw new RuntimeException('Der Prognosewert ' . $name . ' ist nicht endlich.');
} }
} }
$werte['Netzleistung'] *= $this->ReadPropertyFloat('Netzleistungsfaktor');
if ($werte['PV'] < 0.0 || $werte['Hausverbrauch'] < 0.0 if ($werte['PV'] < 0.0 || $werte['Hausverbrauch'] < 0.0
|| $werte['SOC'] < 0.0 || $werte['SOC'] > 100.0 || $werte['SOC'] < 0.0 || $werte['SOC'] > 100.0
) { ) {
+58 -6
View File
@@ -8,13 +8,23 @@ kommen aus den Verbrauchermeldungen.
## Variablen ## Variablen
Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netzleistung`. Die drei Regelungsvariablen `Aktiv`, `Betriebsart` und `Netzleistung` sind immer sichtbar. Die vom Manager gefuehrten Mess- und Energievariablen werden immer angelegt und archiviert, koennen aber gemeinsam ausgeblendet werden.
| Ident | Typ / Zugriff | Beschreibung | | Ident | Typ / Zugriff | Beschreibung |
| --- | --- | --- | | --- | --- | --- |
| `Aktiv` | Boolean / bedienbar | Regelung ein/aus; Start `false`. | | `Aktiv` | Boolean / bedienbar | Regelung ein/aus; Start `false`. |
| `Betriebsart` | String / Anzeige | `Inaktiv`, `PV` oder `Peak`. | | `Betriebsart` | String / Anzeige | `Inaktiv`, `PV` oder `Peak`. |
| `Netzleistung` | Float / Anzeige | Aktuelle Netzleistung in W; positiv Bezug, negativ Einspeisung. | | `Netzleistung` | Float / Anzeige | Aktuelle Netzleistung in W; positiv Bezug, negativ Einspeisung. |
| `PVLeistungArchiv` | Float / Logging | Normalisierte PV-Leistung in W. |
| `HausverbrauchLeistungArchiv` | Float / Logging | Normalisierter Hausverbrauch in W. |
| `NetzleistungArchiv` | Float / Logging | Normalisierte Netzleistung in W; positiv Bezug, negativ Einspeisung. |
| `BatterieleistungArchiv` | Float / Logging | Batterieleistung in W; positiv Laden, negativ Entladen. |
| `PVEnergie` | Float / Zaehler | Integrierte PV-Erzeugung in kWh. |
| `Hausenergie` | Float / Zaehler | Integrierter Hausverbrauch in kWh. |
| `NetzbezugEnergie` | Float / Zaehler | Integrierter Netzbezug in kWh. |
| `EinspeisungEnergie` | Float / Zaehler | Integrierte Einspeisung in kWh. |
| `BatterieLadenEnergie` | Float / Zaehler | Integrierte Batterieladung in kWh. |
| `BatterieEntladenEnergie` | Float / Zaehler | Integrierte Batterieentladung in kWh. |
| `NetzleistungGueltig` | Boolean / Logging | Messquelle vorhanden und aktuell. | | `NetzleistungGueltig` | Boolean / Logging | Messquelle vorhanden und aktuell. |
| `WirksameLastspitzengrenze` | Float / Logging | Aktuelle feste oder monatliche Bezugsgrenze in W. | | `WirksameLastspitzengrenze` | Float / Logging | Aktuelle feste oder monatliche Bezugsgrenze in W. |
| `WirksameEinspeisegrenze` | Float / Logging | Anlagenweite Einspeisegrenze in W. | | `WirksameEinspeisegrenze` | Float / Logging | Anlagenweite Einspeisegrenze in W. |
@@ -54,12 +64,9 @@ Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netz
| `Einspeisegrenze` | Float | Maximale Einspeisung der Gesamtanlage in W; `0` bedeutet Nulleinspeisung. | | `Einspeisegrenze` | Float | Maximale Einspeisung der Gesamtanlage in W; `0` bedeutet Nulleinspeisung. |
| `Einspeisetoleranz` | Float | `100` W; Totband gegen pendelnde Wechselrichtervorgaben. | | `Einspeisetoleranz` | Float | `100` W; Totband gegen pendelnde Wechselrichtervorgaben. |
| `NetzfahrplanAktiv` | Boolean | `false`; lizenzierten, tarif- und prognosebasierten Netzzielwert verwenden. | | `NetzfahrplanAktiv` | Boolean | `false`; lizenzierten, tarif- und prognosebasierten Netzzielwert verwenden. |
| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Grenzen sowie optionalen Istleistungs- und Begrenzungsregistern. | | `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Grenzen, Istleistungsmessung und optionalem Begrenzungsregister. |
| `AnlagenPVFlaechen` | String/JSON | `[]`; PV-Flaechen mit DC-Leistung, Ausrichtung und Wechselrichter-ID. | | `AnlagenPVFlaechen` | String/JSON | `[]`; PV-Flaechen mit DC-Leistung, Ausrichtung und Wechselrichter-ID. |
| `AnlagenBatterien` | String/JSON | `[]`; Batteriespeicher mit Kapazitaeten, Leistungen, Kopplung und Wechselrichter-ID. | | `AnlagenBatterien` | String/JSON | `[]`; Batteriespeicher mit Kapazitaeten, Kopplung, Leistungs- und SOC-Messung. |
| `PrognosePVVariableID` | Integer | `0`; PV-Leistung in W fuer die Prognosetelemetrie. |
| `PrognoseHausverbrauchVariableID` | Integer | `0`; Hausverbrauch in W fuer die Prognosetelemetrie. |
| `PrognoseSOCVariableID` | Integer | `0`; Batterie-Ladezustand in Prozent. |
| `PrognoseSendeintervall` | Integer | `60` s; zulaessig sind 60 bis 3600 Sekunden. | | `PrognoseSendeintervall` | Integer | `60` s; 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. |
@@ -67,6 +74,17 @@ Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netz
| `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. |
| `LoggingEin` | Boolean | `false`; laufende Meldungen im Debug-Fenster ausgeben. | | `LoggingEin` | Boolean | `false`; laufende Meldungen im Debug-Fenster ausgeben. |
| `EnergieaufzeichnungAktiv` | Boolean | `true`; managergefuehrte Leistungs- und Energieaufzeichnung aktivieren. |
| `MesswerteAnzeigen` | Boolean | `false`; einzelne Manager-Messwerte in der Objektstruktur anzeigen. |
| `EnergyPieAnzeigen` | Boolean | `false`; Energy Pie unter dem Manager anlegen oder entfernen. |
| `EnergiediagrammeAnzeigen` | Boolean | `false`; native Leistungs- und Energiediagramme anlegen oder entfernen. |
| `FunFactsAnzeigen` | Boolean | `false`; responsive Energy Facts anlegen oder entfernen. |
| `EnergieflussAnzeigen` | Boolean | `false`; native Energy Distribution anlegen oder entfernen. |
| `LeistungsaufzeichnungMinuten` | Auswahl | `1`; Verdichtung auf 1 Minute, 5 Minuten oder 1 Stunde. |
| `LeistungLoeschenMonate` | Integer | `12`; Leistungswerte nach zwoelf Monaten loeschen, `0` deaktiviert die Loeschung. |
| `EnergieaufzeichnungMinuten` | Auswahl | `5`; Verdichtung auf 1 Minute, 5 Minuten oder 1 Stunde. |
| `EnergieVerdichtenMonate` | Integer | `12`; Energiezaehler nach zwoelf Monaten auf Tageswerte verdichten. |
| `EnergieLoeschenMonate` | Integer | `0`; Energiezaehler nicht loeschen. |
Es gibt keine Sollwertquellenauswahl und keine zweite Prioritätseinstellung im Es gibt keine Sollwertquellenauswahl und keine zweite Prioritätseinstellung im
Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt. Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt.
@@ -74,6 +92,40 @@ Die Monatsgrenzen werden in den Grundeinstellungen über eine Schaltfläche
ein- und ausgeblendet. Die automatische Verbrauchersuche kann dort erneut ein- und ausgeblendet. Die automatische Verbrauchersuche kann dort erneut
ausgeführt werden, ohne andere ungespeicherte Formulareingaben zu verlieren. ausgeführt werden, ohne andere ungespeicherte Formulareingaben zu verlieren.
## Energieaufzeichnung und Visualisierung
Sobald Netzleistung und mindestens eine PV-Istleistung in der Anlagentopologie
konfiguriert sind, tastet der Manager die Quellen minuetlich ab. PV-Leistung ist
die Summe der den PV-Flaechen zugeordneten Wechselrichter. Batterieleistung und
der kapazitaetsgewichtete SOC stammen aus den Batteriespeichern. Positive
Batterieleistung bedeutet Laden, negative Entladen. Der Hausverbrauch wird als
`PV + Netz - Batterie` bilanziert. Dieselben Werte versorgen ohne zweite
Eingabe auch die Prognosetelemetrie. Ausfallluecken ueber fuenf Minuten werden
nicht nachberechnet.
Batteriespeicher bleiben als physische Komponenten samt Messquellen in der
Anlagentopologie. Ein zusaetzliches Batterieverbrauchermodul beschreibt dagegen
Regelung, Leistungsangebot und Betriebszustand; beide Rollen sind bewusst
getrennt.
Die eigenen Variablen werden automatisch im ersten Archive Control aktiviert.
Leistungen verwenden die Standardaggregation, Energievariablen den Zaehlermodus.
Standardmaessig werden Leistungen auf eine Minute verdichtet und nach zwoelf
Monaten geloescht. Energie wird auf fuenf Minuten verdichtet, nach zwoelf
Monaten auf Tageswerte reduziert und nie geloescht. Aenderungen an diesen
Regeln werden idempotent ueber die Archiv-API gesetzt und reaggregiert.
Vier Schalter im Bereich `Energieaufzeichnung` verwalten die
Visualisierungen idempotent. Beim Einschalten werden die Objekte direkt unter
dem Manager angelegt oder aktualisiert. Beim Ausschalten werden ausschliesslich
die vom Manager anhand ihrer festen Kennung und ihres Typs erkannten Objekte
entfernt beziehungsweise die native Energy Distribution ausgeblendet:
- Energy Pie aus Enelix Utils mit den vier relevanten Energiezaehlern,
- zwei native IP-Symcon-Diagrammmedien fuer Leistungen und Energien,
- responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten,
- native Energy Distribution fuer PV, Netz, Haus und Batterie.
## 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
+10 -3
View File
@@ -288,6 +288,7 @@ function enelixDemoInstall(): array
'ACEinspeisegrenze_kW' => 0.0, 'ACEinspeisegrenze_kW' => 0.0,
'ACBezugsgrenze_kW' => 0.0, 'ACBezugsgrenze_kW' => 0.0,
'IstleistungVariableID' => $inverterActual, 'IstleistungVariableID' => $inverterActual,
'Istleistungsfaktor' => 1.0,
'BegrenzungVariableID' => $inverterLimit, 'BegrenzungVariableID' => $inverterLimit,
'Begrenzungsart' => 'watt', 'Begrenzungsart' => 'watt',
]]; ]];
@@ -314,13 +315,18 @@ function enelixDemoInstall(): array
), ),
'WechselrichterID' => 'demo-hybrid-1', 'WechselrichterID' => 'demo-hybrid-1',
'Kopplung' => 'hybrid', 'Kopplung' => 'hybrid',
'LeistungVariableID' => $batteryActual,
'Leistungsfaktor' => 1.0,
'SOCVariableID' => $batterySoc,
]]; ]];
$managerProperties = [ $managerProperties = [
'NetzleistungVariableID' => $grid, 'NetzleistungVariableID' => $grid,
'Netzleistungsfaktor' => 1.0, 'Netzleistungsfaktor' => 1.0,
'PrognosePVVariableID' => $pvDisplay, 'MesswerteAnzeigen' => true,
'PrognoseHausverbrauchVariableID' => $houseDisplay, 'EnergyPieAnzeigen' => true,
'PrognoseSOCVariableID' => $batterySoc, 'EnergiediagrammeAnzeigen' => true,
'FunFactsAnzeigen' => true,
'EnergieflussAnzeigen' => true,
'PrognoseSendeintervall' => 60, 'PrognoseSendeintervall' => 60,
'MesswertMaxAlter' => 10, 'MesswertMaxAlter' => 10,
'AutomatischeSuche' => false, 'AutomatischeSuche' => false,
@@ -366,6 +372,7 @@ function enelixDemoInstall(): array
} }
} }
IPS_RequestAction($manager, 'Aktiv', $managerReady); IPS_RequestAction($manager, 'Aktiv', $managerReady);
IPS_RequestAction($manager, 'MesswerteAufzeichnen', true);
$boiler1List = enelixDemoEnsureInstance( $boiler1List = enelixDemoEnsureInstance(
$dashboard, $dashboard,
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
final class EnergieMessung
{
private const WATTSEKUNDEN_PRO_KWH = 3600000.0;
public static function hausleistung(float $pv, float $netz, float $batterie): float
{
return max(
0.0,
self::endlich($pv) + self::endlich($netz) - self::endlich($batterie)
);
}
/**
* @param array{PV: float, Haus: float, Netz: float, Batterie: float} $leistungen
* @return array{
* PVEnergie: float,
* Hausenergie: float,
* NetzbezugEnergie: float,
* EinspeisungEnergie: float,
* BatterieLadenEnergie: float,
* BatterieEntladenEnergie: float
* }
*/
public static function energieDeltas(array $leistungen, int $sekunden): array
{
$faktor = max(0, $sekunden) / self::WATTSEKUNDEN_PRO_KWH;
$pv = self::endlich($leistungen['PV']);
$haus = self::endlich($leistungen['Haus']);
$netz = self::endlich($leistungen['Netz']);
$batterie = self::endlich($leistungen['Batterie']);
return [
'PVEnergie' => max(0.0, $pv) * $faktor,
'Hausenergie' => max(0.0, $haus) * $faktor,
'NetzbezugEnergie' => max(0.0, $netz) * $faktor,
'EinspeisungEnergie' => max(0.0, -$netz) * $faktor,
'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor,
'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor,
];
}
private static function endlich(float $wert): float
{
return is_finite($wert) ? $wert : 0.0;
}
}
+542
View File
@@ -0,0 +1,542 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
trait ManagerEnergieTrait
{
private function konfiguriereEnergiearchiv(): void
{
$archivID = $this->archivID();
if ($archivID === 0) {
$this->protokolliere('Energiearchiv', 'Kein Archiv Control vorhanden.');
return;
}
$aktiv = $this->ReadPropertyBoolean('EnergieaufzeichnungAktiv');
$geaendert = [];
$leistungstyp = $this->verdichtungstyp(
$this->ReadPropertyInteger('LeistungsaufzeichnungMinuten')
);
$energietyp = $this->verdichtungstyp(
$this->ReadPropertyInteger('EnergieaufzeichnungMinuten')
);
foreach (self::LEISTUNGS_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
AC_SetAggregationType($archivID, $variableID, 0);
AC_SetGraphStatus($archivID, $variableID, $aktiv);
$regeln = [-1 => $leistungstyp];
$loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate');
if ($loeschen > 0) {
$regeln[$loeschen] = 7;
}
if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) {
$geaendert[] = $variableID;
}
}
foreach (self::ENERGIE_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
AC_SetAggregationType($archivID, $variableID, 1);
AC_SetGraphStatus($archivID, $variableID, $aktiv);
$regeln = [-1 => $energietyp];
$verdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate');
if ($verdichten > 0) {
$regeln[$verdichten] = 3;
}
$loeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate');
if ($loeschen > 0) {
$regeln[$loeschen] = 7;
}
if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) {
$geaendert[] = $variableID;
}
}
IPS_ApplyChanges($archivID);
foreach (array_unique($geaendert) as $variableID) {
AC_ReAggregateVariable($archivID, $variableID);
}
}
/** @param array<int, int> $regeln */
private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool
{
ksort($regeln);
$aktuell = [];
$eintraege = AC_GetCompaction($archivID, $variableID);
if (is_array($eintraege)) {
foreach ($eintraege as $eintrag) {
$monat = (int) ($eintrag['MonthOffset'] ?? -999);
$typ = (int) ($eintrag['CompactionType'] ?? -1);
if ($monat >= -1 && $typ >= 0) {
$aktuell[$monat] = $typ;
}
}
}
ksort($aktuell);
if ($aktuell === $regeln) {
return false;
}
foreach (array_keys($aktuell) as $monat) {
AC_SetCompaction($archivID, $variableID, $monat, -1);
}
foreach ($regeln as $monat => $typ) {
AC_SetCompaction($archivID, $variableID, $monat, $typ);
}
return true;
}
private function verdichtungstyp(int $minuten): int
{
return [1 => 0, 5 => 1, 60 => 2][$minuten] ?? 0;
}
private function archivID(): int
{
$archive = IPS_GetInstanceListByModuleID(self::ARCHIVE_MODULE_ID);
return $archive === [] ? 0 : (int) $archive[0];
}
private function synchronisiereEnergievisualisierungen(): void
{
$this->synchronisiereEnergyPie($this->ReadPropertyBoolean('EnergyPieAnzeigen'));
$this->synchronisiereEnergiediagramme(
$this->ReadPropertyBoolean('EnergiediagrammeAnzeigen')
);
$this->synchronisiereFunFacts($this->ReadPropertyBoolean('FunFactsAnzeigen'));
$this->synchronisiereEnergiefluss($this->ReadPropertyBoolean('EnergieflussAnzeigen'));
}
private function synchronisiereEnergyPie(bool $anzeigen): void
{
if ($anzeigen) {
$this->erstelleEnergyPie();
return;
}
$this->entferneInstanzUnterobjekt('EnergyPie', self::ENERGY_PIE_MODULE_ID);
}
private function synchronisiereEnergiediagramme(bool $anzeigen): void
{
if ($anzeigen) {
$this->erstelleEnergiediagramme();
return;
}
$this->entferneDiagrammUnterobjekt('Leistungsdiagramm');
$this->entferneDiagrammUnterobjekt('Energiediagramm');
}
private function synchronisiereFunFacts(bool $anzeigen): void
{
if ($anzeigen) {
$this->erstelleFunFacts();
return;
}
$variableID = $this->findeUnterobjekt('EnergieFunFacts');
if ($variableID === 0) {
return;
}
if ((int) IPS_GetObject($variableID)['ObjectType'] !== 2) {
throw new RuntimeException('Unterobjekt EnergieFunFacts ist keine Variable.');
}
$this->UnregisterVariable('EnergieFunFacts');
}
private function synchronisiereEnergiefluss(bool $anzeigen): void
{
if ($anzeigen) {
$this->erstelleEnergiefluss();
return;
}
$instanzID = $this->findeUnterobjekt('Energiefluss');
if ($instanzID === 0) {
return;
}
$objekt = IPS_GetObject($instanzID);
$modulID = $objekt['ObjectType'] === 1
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
: '';
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
}
IPS_SetHidden($instanzID, true);
}
private function entferneInstanzUnterobjekt(string $ident, string $modulID): void
{
$instanzID = $this->findeUnterobjekt($ident);
if ($instanzID === 0) {
return;
}
$objekt = IPS_GetObject($instanzID);
$istModulID = $objekt['ObjectType'] === 1
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
: '';
if ($istModulID !== $modulID) {
throw new RuntimeException('Unterobjekt ' . $ident . ' hat einen unpassenden Typ.');
}
IPS_DeleteInstance($instanzID);
}
private function entferneDiagrammUnterobjekt(string $ident): void
{
$medienID = $this->findeUnterobjekt($ident);
if ($medienID === 0) {
return;
}
$objekt = IPS_GetObject($medienID);
if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) {
throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.');
}
IPS_DeleteMedia($medienID, true);
}
private function erstelleEnergyPie(): string
{
if (!in_array(self::ENERGY_PIE_MODULE_ID, IPS_GetModuleList(), true)) {
throw new RuntimeException(
'Das Modul Energy Pie aus Enelix Utils ist nicht installiert.'
);
}
$instanzID = $this->findeUnterobjekt('EnergyPie');
if ($instanzID === 0) {
$instanzID = IPS_CreateInstance(self::ENERGY_PIE_MODULE_ID);
IPS_SetParent($instanzID, $this->InstanceID);
IPS_SetIdent($instanzID, 'EnergyPie');
} else {
$objekt = IPS_GetObject($instanzID);
$modulID = $objekt['ObjectType'] === 1
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
: '';
if ($modulID !== self::ENERGY_PIE_MODULE_ID) {
throw new RuntimeException('Unterobjekt EnergyPie hat einen unpassenden Typ.');
}
}
IPS_SetName($instanzID, 'Energy Pie');
IPS_SetPosition($instanzID, 500);
IPS_SetHidden($instanzID, false);
foreach ([
'ProduktionZaehlerID' => $this->GetIDForIdent('PVEnergie'),
'EinspeisungZaehlerID' => $this->GetIDForIdent('EinspeisungEnergie'),
'NetzbezugZaehlerID' => $this->GetIDForIdent('NetzbezugEnergie'),
'VerbrauchZaehlerID' => $this->GetIDForIdent('Hausenergie'),
'Zaehlerfaktor' => 1.0,
] as $property => $wert) {
IPS_SetProperty($instanzID, $property, $wert);
}
IPS_ApplyChanges($instanzID);
return 'Energy Pie wurde erstellt oder aktualisiert.';
}
private function erstelleEnergiefluss(): string
{
if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) {
throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.');
}
$instanzID = $this->findeUnterobjekt('Energiefluss');
if ($instanzID === 0) {
$instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID);
IPS_SetParent($instanzID, $this->InstanceID);
IPS_SetIdent($instanzID, 'Energiefluss');
} else {
$objekt = IPS_GetObject($instanzID);
$modulID = $objekt['ObjectType'] === 1
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
: '';
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
}
}
IPS_SetName($instanzID, 'Energiefluss');
IPS_SetPosition($instanzID, 540);
IPS_SetHidden($instanzID, false);
$knoten = [
$this->energieflussKnoten('PVLeistungArchiv', 'PV-Anlage', 'solar'),
$this->energieflussKnoten('NetzleistungArchiv', 'Stromnetz', 'grid'),
$this->energieflussKnoten('HausverbrauchLeistungArchiv', 'Hausverbrauch', 'consumer'),
$this->energieflussKnoten('BatterieleistungArchiv', 'Batteriespeicher', 'battery'),
];
IPS_SetProperty($instanzID, 'Calculation', true);
IPS_SetProperty(
$instanzID,
'Config',
json_encode($knoten, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
);
IPS_SetProperty($instanzID, 'EnableLimits', false);
IPS_SetProperty($instanzID, 'Restrictions', '[]');
IPS_ApplyChanges($instanzID);
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
}
/** @return array<string, mixed> */
private function energieflussKnoten(string $ident, string $name, string $typ): array
{
return [
'VariableConfigType' => 0,
'VariableID' => $this->GetIDForIdent($ident),
'SecondaryVariableID' => 1,
'Name' => $name,
'Type' => $typ,
'Grouped' => false,
'CalculationType' => 0,
'InfoVariableID' => 1,
'Invert' => false,
];
}
private function erstelleEnergiediagramme(): string
{
$leistung = [
$this->chartDatensatz('PVLeistungArchiv', 'PV', '#F4C542', '~Power'),
$this->chartDatensatz('HausverbrauchLeistungArchiv', 'Haus', '#22A06B', '~Power'),
$this->chartDatensatz('NetzleistungArchiv', 'Netz', '#E95D5D', '~Power'),
$this->chartDatensatz('BatterieleistungArchiv', 'Batterie', '#8B5CF6', '~Power'),
];
$energie = [
$this->chartDatensatz('PVEnergie', 'PV', '#F4C542', '~Electricity'),
$this->chartDatensatz('Hausenergie', 'Haus', '#22A06B', '~Electricity'),
$this->chartDatensatz('NetzbezugEnergie', 'Netzbezug', '#E95D5D', '~Electricity'),
$this->chartDatensatz('EinspeisungEnergie', 'Einspeisung', '#2F80ED', '~Electricity'),
$this->chartDatensatz('BatterieLadenEnergie', 'Batterie laden', '#8B5CF6', '~Electricity'),
$this->chartDatensatz('BatterieEntladenEnergie', 'Batterie entladen', '#F59E0B', '~Electricity'),
];
$this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
$this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.';
}
/** @param list<array<string, mixed>> $datensaetze */
private function erstelleDiagramm(
string $ident,
string $name,
array $datensaetze,
int $position
): int {
$medienID = $this->findeUnterobjekt($ident);
if ($medienID === 0) {
$medienID = IPS_CreateMedia(4);
IPS_SetParent($medienID, $this->InstanceID);
IPS_SetIdent($medienID, $ident);
}
$objekt = IPS_GetObject($medienID);
if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) {
throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.');
}
IPS_SetName($medienID, $name);
IPS_SetPosition($medienID, $position);
IPS_SetHidden($medienID, false);
IPS_SetMediaFile($medienID, 'media/' . $medienID . '.chart', false);
IPS_SetMediaContent(
$medienID,
base64_encode(json_encode(['datasets' => $datensaetze], JSON_THROW_ON_ERROR))
);
return $medienID;
}
/** @return array<string, mixed> */
private function chartDatensatz(
string $ident,
string $titel,
string $farbe,
string $profil
): array {
return [
'variableID' => $this->GetIDForIdent($ident),
'fillColor' => 'clear',
'strokeColor' => $farbe,
'timeOffset' => 0,
'visible' => true,
'title' => $titel,
'type' => 'line',
'side' => 'left',
'profile' => $profil,
];
}
private function findeUnterobjekt(string $ident): int
{
$objektID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
return is_int($objektID) && $objektID > 0 ? $objektID : 0;
}
private function erstelleFunFacts(): string
{
$this->RegisterVariableString('EnergieFunFacts', 'Energy Facts', '~HTMLBox', 530);
$variableID = $this->GetIDForIdent('EnergieFunFacts');
IPS_SetHidden($variableID, false);
$this->aktualisiereFunFacts($variableID);
return 'Energy Facts wurden erstellt oder aktualisiert.';
}
private function aktualisiereFunFacts(int $variableID): void
{
$archivID = $this->archivID();
if ($archivID === 0) {
SetValue($variableID, '<div style="padding:16px">Archiv nicht verfuegbar.</div>');
return;
}
$heute = strtotime('today');
$jahr = mktime(0, 0, 0, 1, 1, (int) date('Y'));
$jetzt = time();
$pvID = $this->GetIDForIdent('PVEnergie');
$hausID = $this->GetIDForIdent('Hausenergie');
$bezugID = $this->GetIDForIdent('NetzbezugEnergie');
$exportID = $this->GetIDForIdent('EinspeisungEnergie');
$pvHeute = $this->energieSumme($archivID, $pvID, $heute, $jetzt);
$pvJahr = $this->energieSumme($archivID, $pvID, $jahr, $jetzt);
$pvTotal = max(0.0, (float) GetValue($pvID));
[$besterTag, $besterTagZeit] = $this->maximalerTageswert($archivID, $pvID);
[$maxPV, $maxPVZeit] = $this->maximaleLeistung(
$archivID,
$this->GetIDForIdent('PVLeistungArchiv')
);
[$maxHausTag, $maxHausTagZeit] = $this->maximalerTageswert($archivID, $hausID);
[$maxHaus, $maxHausZeit] = $this->maximaleLeistung(
$archivID,
$this->GetIDForIdent('HausverbrauchLeistungArchiv')
);
[$maxBezug, $maxBezugZeit] = $this->maximalerTageswert($archivID, $bezugID);
[$maxExport, $maxExportZeit] = $this->maximalerTageswert($archivID, $exportID);
$werte = [
'{{PV_HEUTE}}' => $this->formatiereEnergie($pvHeute),
'{{PV_JAHR}}' => $this->formatiereEnergie($pvJahr),
'{{PV_TOTAL}}' => $this->formatiereEnergie($pvTotal),
'{{BESTER_TAG}}' => $this->formatiereEnergie($besterTag),
'{{BESTER_TAG_DATUM}}' => $this->formatiereDatum($besterTagZeit),
'{{MAX_PV}}' => $this->formatiereLeistung($maxPV),
'{{MAX_PV_DATUM}}' => $this->formatiereDatum($maxPVZeit),
'{{MAX_HAUS_TAG}}' => $this->formatiereEnergie($maxHausTag),
'{{MAX_HAUS_TAG_DATUM}}' => $this->formatiereDatum($maxHausTagZeit),
'{{MAX_HAUS}}' => $this->formatiereLeistung($maxHaus),
'{{MAX_HAUS_DATUM}}' => $this->formatiereDatum($maxHausZeit),
'{{MAX_BEZUG}}' => $this->formatiereEnergie($maxBezug),
'{{MAX_BEZUG_DATUM}}' => $this->formatiereDatum($maxBezugZeit),
'{{MAX_EXPORT}}' => $this->formatiereEnergie($maxExport),
'{{MAX_EXPORT_DATUM}}' => $this->formatiereDatum($maxExportZeit),
'{{HEIZOEL}}' => number_format($pvTotal / 10.0, 0, ',', "'") . ' Liter',
'{{FLUGKM}}' => number_format($pvTotal * 2.4, 0, ',', "'") . ' km',
];
SetValue($variableID, strtr($this->funFactsHtml(), $werte));
}
private function energieSumme(int $archivID, int $variableID, int $start, int $ende): float
{
$werte = @AC_GetAggregatedValues($archivID, $variableID, 1, $start, $ende, 0);
if (!is_array($werte)) {
return 0.0;
}
return array_sum(array_map(
static fn (array $wert): float => max(0.0, (float) ($wert['Avg'] ?? 0.0)),
$werte
));
}
/** @return array{0: float, 1: int} */
private function maximalerTageswert(int $archivID, int $variableID): array
{
$werte = @AC_GetAggregatedValues($archivID, $variableID, 1, 0, time(), 0);
$maximum = 0.0;
$zeit = 0;
if (is_array($werte)) {
foreach ($werte as $wert) {
$kandidat = max(0.0, (float) ($wert['Avg'] ?? 0.0));
if ($kandidat > $maximum) {
$maximum = $kandidat;
$zeit = (int) ($wert['TimeStamp'] ?? 0);
}
}
}
return [$maximum, $zeit];
}
/** @return array{0: float, 1: int} */
private function maximaleLeistung(int $archivID, int $variableID): array
{
$werte = @AC_GetAggregatedValues($archivID, $variableID, 1, 0, time(), 0);
$maximum = 0.0;
$zeit = 0;
if (is_array($werte)) {
foreach ($werte as $wert) {
$kandidat = max(0.0, (float) ($wert['Max'] ?? 0.0));
if ($kandidat > $maximum) {
$maximum = $kandidat;
$zeit = (int) ($wert['MaxTime'] ?? $wert['TimeStamp'] ?? 0);
}
}
}
return [$maximum, $zeit];
}
private function formatiereEnergie(float $wert): string
{
if ($wert >= 1000.0) {
return number_format($wert / 1000.0, 2, ',', "'") . ' MWh';
}
return number_format($wert, 1, ',', "'") . ' kWh';
}
private function formatiereLeistung(float $wert): string
{
if ($wert >= 10000.0) {
return number_format($wert / 1000.0, 1, ',', "'") . ' kW';
}
return number_format($wert, 0, ',', "'") . ' W';
}
private function formatiereDatum(int $zeit): string
{
return $zeit > 0 ? date('d.m.Y', $zeit) : 'Noch keine Daten';
}
private function funFactsHtml(): string
{
return <<<'HTML'
<style>
.enx-facts{box-sizing:border-box;font-family:system-ui,-apple-system,sans-serif;color:#182230;padding:16px;background:#f4f7f6}
.enx-facts *{box-sizing:border-box}.enx-facts-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
.enx-fact-panel{min-width:0;background:#fff;border:1px solid #dfe7e4;border-radius:8px;overflow:hidden;box-shadow:0 4px 18px rgba(17,44,36,.08)}
.enx-fact-head{padding:15px 18px 13px;border-bottom:3px solid #22a06b;display:flex;align-items:center;justify-content:space-between}
.enx-fact-head b{font-size:18px}.enx-fact-head span{font-size:11px;letter-spacing:.08em;color:#60736c}
.enx-fact-body{padding:5px 18px 10px}.enx-fact-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:11px 0;border-bottom:1px solid #edf1ef}
.enx-fact-row:last-child{border-bottom:0}.enx-fact-label{min-width:0;color:#52635d;font-size:13px}.enx-fact-value{text-align:right;font-size:15px;font-weight:700;white-space:nowrap}
.enx-fact-date{display:block;color:#81908b;font-size:11px;font-weight:500;margin-top:2px}.enx-fact-panel.grid .enx-fact-head{border-color:#2f80ed}
.enx-fact-panel.impact .enx-fact-head{border-color:#f4c542}.enx-impact{padding:18px}.enx-impact-value{font-size:30px;font-weight:750;line-height:1.15}
.enx-impact-label{font-size:12px;color:#60736c;margin:5px 0 20px}.enx-impact-note{font-size:11px;color:#81908b;line-height:1.45}
@media(max-width:820px){.enx-facts-grid{grid-template-columns:1fr}.enx-facts{padding:10px}}
</style>
<div class="enx-facts"><div class="enx-facts-grid">
<section class="enx-fact-panel"><div class="enx-fact-head"><b>Solar</b><span>ERZEUGUNG</span></div><div class="enx-fact-body">
<div class="enx-fact-row"><span class="enx-fact-label">Heute</span><span class="enx-fact-value">{{PV_HEUTE}}</span></div>
<div class="enx-fact-row"><span class="enx-fact-label">Dieses Jahr</span><span class="enx-fact-value">{{PV_JAHR}}</span></div>
<div class="enx-fact-row"><span class="enx-fact-label">Total</span><span class="enx-fact-value">{{PV_TOTAL}}</span></div>
<div class="enx-fact-row"><span class="enx-fact-label">Bester Tag</span><span class="enx-fact-value">{{BESTER_TAG}}<small class="enx-fact-date">{{BESTER_TAG_DATUM}}</small></span></div>
<div class="enx-fact-row"><span class="enx-fact-label">Max. Leistung</span><span class="enx-fact-value">{{MAX_PV}}<small class="enx-fact-date">{{MAX_PV_DATUM}}</small></span></div>
</div></section>
<section class="enx-fact-panel grid"><div class="enx-fact-head"><b>Netz &amp; Haus</b><span>SPITZENWERTE</span></div><div class="enx-fact-body">
<div class="enx-fact-row"><span class="enx-fact-label">Max. Tagesverbrauch</span><span class="enx-fact-value">{{MAX_HAUS_TAG}}<small class="enx-fact-date">{{MAX_HAUS_TAG_DATUM}}</small></span></div>
<div class="enx-fact-row"><span class="enx-fact-label">Max. Hauslast</span><span class="enx-fact-value">{{MAX_HAUS}}<small class="enx-fact-date">{{MAX_HAUS_DATUM}}</small></span></div>
<div class="enx-fact-row"><span class="enx-fact-label">Max. Tagesbezug</span><span class="enx-fact-value">{{MAX_BEZUG}}<small class="enx-fact-date">{{MAX_BEZUG_DATUM}}</small></span></div>
<div class="enx-fact-row"><span class="enx-fact-label">Max. Einspeisung</span><span class="enx-fact-value">{{MAX_EXPORT}}<small class="enx-fact-date">{{MAX_EXPORT_DATUM}}</small></span></div>
</div></section>
<section class="enx-fact-panel impact"><div class="enx-fact-head"><b>Wirkung</b><span>VERGLEICHE</span></div><div class="enx-impact">
<div class="enx-impact-value">{{HEIZOEL}}</div><div class="enx-impact-label">rechnerisch kompensiertes Heizoel</div>
<div class="enx-impact-value">{{FLUGKM}}</div><div class="enx-impact-label">rechnerisch kompensierte Flugkilometer</div>
<div class="enx-impact-note">Vergleichswerte auf Basis von 10 kWh pro Liter Heizoel und 2,4 Flugkilometern pro erzeugter kWh.</div>
</div></section>
</div></div>
HTML;
}
}
+6 -3
View File
@@ -119,9 +119,12 @@ final class DemoanlageBootstrapTest extends TestCase
self::assertStringContainsString("'AnlagenWechselrichter'", $source); self::assertStringContainsString("'AnlagenWechselrichter'", $source);
self::assertStringContainsString("'AnlagenPVFlaechen'", $source); self::assertStringContainsString("'AnlagenPVFlaechen'", $source);
self::assertStringContainsString("'AnlagenBatterien'", $source); self::assertStringContainsString("'AnlagenBatterien'", $source);
self::assertStringContainsString("'PrognosePVVariableID' => \$pvDisplay", $source); self::assertStringContainsString("'EnergyPieAnzeigen' => true", $source);
self::assertStringContainsString("'PrognoseHausverbrauchVariableID' => \$houseDisplay", $source); self::assertStringContainsString("'EnergiediagrammeAnzeigen' => true", $source);
self::assertStringContainsString("'PrognoseSOCVariableID' => \$batterySoc", $source); self::assertStringContainsString("'FunFactsAnzeigen' => true", $source);
self::assertStringContainsString("'EnergieflussAnzeigen' => true", $source);
self::assertStringContainsString("'LeistungVariableID' => \$batteryActual", $source);
self::assertStringContainsString("'SOCVariableID' => \$batterySoc", $source);
self::assertStringContainsString("'PrognoseSendeintervall' => 60", $source); self::assertStringContainsString("'PrognoseSendeintervall' => 60", $source);
self::assertStringContainsString("'IstleistungVariableID' => \$inverterActual", $source); self::assertStringContainsString("'IstleistungVariableID' => \$inverterActual", $source);
self::assertStringContainsString("'BegrenzungVariableID' => \$inverterLimit", $source); self::assertStringContainsString("'BegrenzungVariableID' => \$inverterLimit", $source);
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use Belevo\EnelixEMS\EnergieMessung;
use PHPUnit\Framework\TestCase;
final class EnergieMessungTest extends TestCase
{
public function testIntegriertVierLeistungenMitVorzeichenrichtung(): void
{
$werte = EnergieMessung::energieDeltas([
'PV' => 7200.0,
'Haus' => 3600.0,
'Netz' => -1800.0,
'Batterie' => 900.0,
], 300);
self::assertEqualsWithDelta(0.6, $werte['PVEnergie'], 0.000001);
self::assertEqualsWithDelta(0.3, $werte['Hausenergie'], 0.000001);
self::assertSame(0.0, $werte['NetzbezugEnergie']);
self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001);
self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001);
self::assertSame(0.0, $werte['BatterieEntladenEnergie']);
}
public function testTrenntBezugUndBatterieentladung(): void
{
$werte = EnergieMessung::energieDeltas([
'PV' => -10.0,
'Haus' => -10.0,
'Netz' => 1000.0,
'Batterie' => -2000.0,
], 3600);
self::assertSame(0.0, $werte['PVEnergie']);
self::assertSame(0.0, $werte['Hausenergie']);
self::assertSame(1.0, $werte['NetzbezugEnergie']);
self::assertSame(0.0, $werte['EinspeisungEnergie']);
self::assertSame(0.0, $werte['BatterieLadenEnergie']);
self::assertSame(2.0, $werte['BatterieEntladenEnergie']);
}
public function testBerechnetHausverbrauchAusDerAnlagenbilanz(): void
{
self::assertSame(2700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0));
self::assertSame(3500.0, EnergieMessung::hausleistung(2000.0, 500.0, -1000.0));
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
}
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
{
$werte = EnergieMessung::energieDeltas([
'PV' => INF,
'Haus' => NAN,
'Netz' => 1000.0,
'Batterie' => -1000.0,
], -1);
foreach ($werte as $wert) {
self::assertSame(0.0, $wert);
}
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use PHPUnit\Framework\TestCase;
final class ManagerEnergieModulstrukturTest extends TestCase
{
public function testManagerKonfiguriertMessquellenUndArchivregeln(): void
{
$modul = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
$trait = (string) file_get_contents(__DIR__ . '/../libs/ManagerEnergieTrait.php');
$formular = (string) file_get_contents(__DIR__ . '/../Manager/form.json');
foreach ([
"RegisterPropertyBoolean('EnergyPieAnzeigen', false)",
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
"RegisterPropertyBoolean('FunFactsAnzeigen', false)",
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
"RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)",
"RegisterPropertyInteger('LeistungLoeschenMonate', 12)",
"RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)",
"RegisterPropertyInteger('EnergieVerdichtenMonate', 12)",
"RegisterPropertyInteger('EnergieLoeschenMonate', 0)",
] as $erwartet) {
self::assertStringContainsString($erwartet, $modul);
}
foreach ([
'AC_SetLoggingStatus',
'AC_SetAggregationType',
'AC_SetCompaction',
'AC_ReAggregateVariable',
'IPS_CreateMedia(4)',
'IPS_SetMediaContent',
'ENERGY_PIE_MODULE_ID',
'ENERGY_FLOW_MODULE_ID',
'energieflussKnoten',
'IPS_DeleteInstance',
'IPS_DeleteMedia',
] as $erwartet) {
self::assertStringContainsString($erwartet, $modul . $trait);
}
foreach ([
'Energieaufzeichnung',
'FormEnergyPieAnzeigen',
'FormEnergiediagrammeAnzeigen',
'FormFunFactsAnzeigen',
'FormEnergieflussAnzeigen',
'MesswerteAnzeigen',
'IstleistungVariableID',
'LeistungVariableID',
'SOCVariableID',
] as $erwartet) {
self::assertStringContainsString($erwartet, $formular);
}
}
public function testManagerFuehrtAlleMessvariablenUndRichtungszaehler(): void
{
$modul = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
foreach ([
'PVLeistungArchiv',
'HausverbrauchLeistungArchiv',
'NetzleistungArchiv',
'BatterieleistungArchiv',
'PVEnergie',
'Hausenergie',
'NetzbezugEnergie',
'EinspeisungEnergie',
'BatterieLadenEnergie',
'BatterieEntladenEnergie',
] as $ident) {
self::assertStringContainsString("'" . $ident . "'", $modul);
}
self::assertStringContainsString("RegisterTimer(
'MesswerteAufzeichnen'", $modul);
self::assertStringContainsString('EnergieMessung::energieDeltas', $modul);
}
public function testFunFactsSindResponsivUndErklaerenUmrechnungen(): void
{
$trait = (string) file_get_contents(__DIR__ . '/../libs/ManagerEnergieTrait.php');
self::assertStringContainsString('grid-template-columns:repeat(3,minmax(0,1fr))', $trait);
self::assertStringContainsString('@media(max-width:820px)', $trait);
self::assertStringContainsString('10 kWh pro Liter Heizoel', $trait);
self::assertStringContainsString('2,4 Flugkilometern', $trait);
self::assertStringContainsString('AC_GetAggregatedValues', $trait);
}
}
+8 -3
View File
@@ -66,9 +66,14 @@ final class ManagerModulstrukturTest extends TestCase
'AnlagenWechselrichter', 'AnlagenWechselrichter',
'AnlagenPVFlaechen', 'AnlagenPVFlaechen',
'AnlagenBatterien', 'AnlagenBatterien',
'PrognosePVVariableID', 'Istleistungsfaktor',
'PrognoseHausverbrauchVariableID', 'LeistungVariableID',
'PrognoseSOCVariableID', 'Leistungsfaktor',
'SOCVariableID',
'EnergyPieAnzeigen',
'EnergiediagrammeAnzeigen',
'FunFactsAnzeigen',
'EnergieflussAnzeigen',
'PrognoseSendeintervall', 'PrognoseSendeintervall',
'StoerueberwachungAktiv', 'StoerueberwachungAktiv',
'StoerungsSendeintervall', 'StoerungsSendeintervall',
+87 -1
View File
@@ -5,7 +5,10 @@ declare(strict_types=1);
use Belevo\EnelixEMS\SymconTest\TestContext; use Belevo\EnelixEMS\SymconTest\TestContext;
return static function (TestContext $test): void { return static function (TestContext $test): void {
$netzleistungId = $test->createVariable(2, 'Netzleistung', 0.0); $netzleistungId = $test->createVariable(2, 'Netzleistung', -800.0);
$pvLeistungId = $test->createVariable(2, 'PV-Leistung', 4200.0);
$batterieLeistungId = $test->createVariable(2, 'Batterieleistung', 700.0);
$batterieSocId = $test->createVariable(2, 'Batterie-Ladezustand', 62.0);
$einKontaktId = $test->createActionVariable('Kontakt Verbraucher 1-Stufig'); $einKontaktId = $test->createActionVariable('Kontakt Verbraucher 1-Stufig');
$einStufigId = $test->createInstance( $einStufigId = $test->createInstance(
'{15879A4E-D0C2-4495-83DE-46E1E462591E}', '{15879A4E-D0C2-4495-83DE-46E1E462591E}',
@@ -79,6 +82,8 @@ return static function (TestContext $test): void {
'ACNennleistung_kW' => 10.0, 'ACNennleistung_kW' => 10.0,
'ACEinspeisegrenze_kW' => 8.0, 'ACEinspeisegrenze_kW' => 8.0,
'ACBezugsgrenze_kW' => 0.0, 'ACBezugsgrenze_kW' => 0.0,
'IstleistungVariableID' => $pvLeistungId,
'Istleistungsfaktor' => 1.0,
]]; ]];
$pvKonfiguration = [[ $pvKonfiguration = [[
'ID' => 'pv-1', 'ID' => 'pv-1',
@@ -100,6 +105,9 @@ return static function (TestContext $test): void {
'MaxEntladeleistung_kW' => 5.0, 'MaxEntladeleistung_kW' => 5.0,
'WechselrichterID' => 'hybrid-1', 'WechselrichterID' => 'hybrid-1',
'Kopplung' => 'hybrid', 'Kopplung' => 'hybrid',
'LeistungVariableID' => $batterieLeistungId,
'Leistungsfaktor' => 1.0,
'SOCVariableID' => $batterieSocId,
]]; ]];
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE')); $lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
$managerId = $test->createInstance( $managerId = $test->createInstance(
@@ -107,6 +115,10 @@ return static function (TestContext $test): void {
'Manager', 'Manager',
[ [
'NetzleistungVariableID' => $netzleistungId, 'NetzleistungVariableID' => $netzleistungId,
'EnergyPieAnzeigen' => true,
'EnergiediagrammeAnzeigen' => true,
'FunFactsAnzeigen' => true,
'EnergieflussAnzeigen' => true,
'Lizenzcode' => $lizenzcode, 'Lizenzcode' => $lizenzcode,
'AutomatischeSuche' => false, 'AutomatischeSuche' => false,
'VerbraucherZuordnung' => json_encode($zuordnung, JSON_THROW_ON_ERROR), 'VerbraucherZuordnung' => json_encode($zuordnung, JSON_THROW_ON_ERROR),
@@ -214,6 +226,80 @@ return static function (TestContext $test): void {
$test->assertSame(['pv-1'], $topologie['sharedAcLimits'][0]['pvArrayIds'] ?? null); $test->assertSame(['pv-1'], $topologie['sharedAcLimits'][0]['pvArrayIds'] ?? null);
$test->assertSame(['bat-1'], $topologie['sharedAcLimits'][0]['batteryIds'] ?? null); $test->assertSame(['bat-1'], $topologie['sharedAcLimits'][0]['batteryIds'] ?? null);
}); });
$test->runCase('Energiewerte, Archivregeln und Unterobjekte werden erzeugt', static function (
TestContext $test
) use ($managerId): void {
IPS_RequestAction($managerId, 'MesswerteAufzeichnen', true);
$test->assertSame(
4200.0,
GetValue($test->objectByIdent('PVLeistungArchiv', $managerId))
);
$test->assertSame(
2700.0,
GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId))
);
$test->assertSame(
-800.0,
GetValue($test->objectByIdent('NetzleistungArchiv', $managerId))
);
$test->assertSame(
700.0,
GetValue($test->objectByIdent('BatterieleistungArchiv', $managerId))
);
$archive = IPS_GetInstanceListByModuleID(
'{43192F0B-135B-4CE7-A0A7-1475603F3060}'
);
$test->assertTrue($archive !== []);
$archivID = (int) $archive[0];
$leistungID = $test->objectByIdent('PVLeistungArchiv', $managerId);
$energieID = $test->objectByIdent('PVEnergie', $managerId);
$test->assertSame(true, AC_GetLoggingStatus($archivID, $leistungID));
$test->assertSame(0, AC_GetAggregationType($archivID, $leistungID));
$test->assertSame(1, AC_GetAggregationType($archivID, $energieID));
foreach (['Leistungsdiagramm', 'Energiediagramm'] as $ident) {
$medienID = $test->objectByIdent($ident, $managerId);
$test->assertSame(4, (int) IPS_GetMedia($medienID)['MediaType']);
}
$html = (string) GetValue($test->objectByIdent('EnergieFunFacts', $managerId));
$test->assertTrue(str_contains($html, 'Solar'));
$test->assertTrue(str_contains($html, 'Netz &amp; Haus'));
$energieflussID = $test->objectByIdent('Energiefluss', $managerId);
$test->assertSame(
'{BA90FDB3-C203-8F26-B4D6-F3DB99905724}',
IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID']
);
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false);
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', true);
$test->assertSame(false, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
foreach ([
'EnergyPieAnzeigen',
'EnergiediagrammeAnzeigen',
'FunFactsAnzeigen',
'EnergieflussAnzeigen',
] as $property) {
IPS_SetProperty($managerId, $property, false);
}
IPS_ApplyChanges($managerId);
foreach ([
'EnergyPie',
'Leistungsdiagramm',
'Energiediagramm',
'EnergieFunFacts',
] as $ident) {
$test->assertSame(
false,
@IPS_GetObjectIDByIdent($ident, $managerId),
$ident . ' wurde beim Ausschalten nicht entfernt.'
);
}
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
});
$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) {