This commit is contained in:
+84
-38
@@ -129,6 +129,7 @@
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"name": "PeakShavingTitel",
|
||||
"caption": "Peak Shaving am Netzanschlusspunkt"
|
||||
},
|
||||
{
|
||||
@@ -144,19 +145,6 @@
|
||||
"minimum": 0.0,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "Einspeisetoleranz",
|
||||
"caption": "Regeltoleranz",
|
||||
"suffix": " W",
|
||||
"minimum": 0.0,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "NetzfahrplanAktiv",
|
||||
"caption": "Intelligenten Netzfahrplan zur Vermeidung von Abregelung verwenden"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "AutomatischeSuche",
|
||||
@@ -241,6 +229,37 @@
|
||||
"caption": "Anlagentopologie",
|
||||
"expanded": true,
|
||||
"items": [
|
||||
{
|
||||
"type": "RowLayout",
|
||||
"items": [
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "NetzbezugEnergieVariableID",
|
||||
"caption": "Netzbezugszaehler"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "NetzbezugEnergiefaktor",
|
||||
"caption": "Faktor",
|
||||
"digits": 6
|
||||
},
|
||||
{
|
||||
"type": "SelectVariable",
|
||||
"name": "NetzeinspeisungEnergieVariableID",
|
||||
"caption": "Einspeisezaehler"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "NetzeinspeisungEnergiefaktor",
|
||||
"caption": "Faktor",
|
||||
"digits": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Energiezaehler werden in kWh erwartet; ueber den Faktor koennen beispielsweise Wh (0,001) umgerechnet werden."
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "AnlagenWechselrichter",
|
||||
@@ -290,30 +309,6 @@
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Einspeisegrenze",
|
||||
"name": "ACEinspeisegrenze_kW",
|
||||
"width": "150px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kW",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Bezugsgrenze",
|
||||
"name": "ACBezugsgrenze_kW",
|
||||
"width": "140px",
|
||||
"add": 0.0,
|
||||
"edit": {
|
||||
"type": "NumberSpinner",
|
||||
"suffix": " kW",
|
||||
"minimum": 0.0,
|
||||
"digits": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"caption": "Istleistung",
|
||||
"name": "IstleistungVariableID",
|
||||
@@ -328,6 +323,20 @@
|
||||
"add": 1.0,
|
||||
"edit": {"type": "NumberSpinner", "digits": 4}
|
||||
},
|
||||
{
|
||||
"caption": "Erzeugungsenergie",
|
||||
"name": "ErzeugungsenergieVariableID",
|
||||
"width": "160px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Energiefaktor",
|
||||
"name": "Erzeugungsenergiefaktor",
|
||||
"width": "110px",
|
||||
"add": 1.0,
|
||||
"edit": {"type": "NumberSpinner", "digits": 6}
|
||||
},
|
||||
{
|
||||
"caption": "PV-Begrenzung",
|
||||
"name": "BegrenzungVariableID",
|
||||
@@ -562,6 +571,27 @@
|
||||
"width": "140px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Ladeenergie",
|
||||
"name": "LadeenergieVariableID",
|
||||
"width": "140px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Entladeenergie",
|
||||
"name": "EntladeenergieVariableID",
|
||||
"width": "150px",
|
||||
"add": 0,
|
||||
"edit": {"type": "SelectVariable"}
|
||||
},
|
||||
{
|
||||
"caption": "Energiefaktor",
|
||||
"name": "Energiefaktor",
|
||||
"width": "110px",
|
||||
"add": 1.0,
|
||||
"edit": {"type": "NumberSpinner", "digits": 6}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -719,14 +749,30 @@
|
||||
"type": "ExpansionPanel",
|
||||
"caption": "Prognose / Forecast",
|
||||
"items": [
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "PrognoseAktiv",
|
||||
"caption": "Prognosen aktivieren",
|
||||
"onChange": "IPS_RequestAction($id, 'FormPrognoseAktiv', $PrognoseAktiv);"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"name": "PrognoseLizenzInformation",
|
||||
"caption": "Prognoselizenz wird geprueft."
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Forecast verwendet dieselben Messquellen aus der Anlagentopologie und den daraus berechneten Hausverbrauch."
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "NetzfahrplanAktiv",
|
||||
"caption": "Intelligenten Netzfahrplan zur Vermeidung von PV-Abregelung verwenden"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "PrognoseSendeintervall",
|
||||
"caption": "Sendeintervall",
|
||||
"caption": "Aktuelle Messwerte an den Prognosedienst senden alle",
|
||||
"minimum": 60,
|
||||
"maximum": 3600,
|
||||
"suffix": " Sekunden"
|
||||
|
||||
+332
-40
@@ -130,11 +130,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterPropertyFloat('Umschaltdifferenz', 5.0);
|
||||
$this->RegisterPropertyBoolean('EinspeisebegrenzungAktiv', false);
|
||||
$this->RegisterPropertyFloat('Einspeisegrenze', 0.0);
|
||||
$this->RegisterPropertyFloat('Einspeisetoleranz', 100.0);
|
||||
$this->RegisterPropertyBoolean('PrognoseAktiv', false);
|
||||
$this->RegisterPropertyBoolean('NetzfahrplanAktiv', false);
|
||||
$this->RegisterPropertyString('AnlagenWechselrichter', '[]');
|
||||
$this->RegisterPropertyString('AnlagenPVFlaechen', '[]');
|
||||
$this->RegisterPropertyString('AnlagenBatterien', '[]');
|
||||
$this->RegisterPropertyInteger('NetzbezugEnergieVariableID', 0);
|
||||
$this->RegisterPropertyFloat('NetzbezugEnergiefaktor', 1.0);
|
||||
$this->RegisterPropertyInteger('NetzeinspeisungEnergieVariableID', 0);
|
||||
$this->RegisterPropertyFloat('NetzeinspeisungEnergiefaktor', 1.0);
|
||||
// Verdeckte Altproperties; Messquellen liegen neu ausschliesslich in der Anlagentopologie.
|
||||
$this->RegisterPropertyInteger('PrognosePVVariableID', 0);
|
||||
$this->RegisterPropertyInteger('PrognoseHausverbrauchVariableID', 0);
|
||||
@@ -196,6 +200,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterAttributeInteger('MessungLetzteLeistungsaufzeichnung', 0);
|
||||
$this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0);
|
||||
$this->RegisterAttributeString('MessungEnergiePuffer', '{}');
|
||||
$this->RegisterAttributeString('MessungZaehlerstaende', '{}');
|
||||
|
||||
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
|
||||
$this->RegisterTimer('Regelzyklus', 0, '');
|
||||
@@ -261,8 +266,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
|
||||
$this->aktualisiereAnschlussstatus();
|
||||
$geraetezugangFehlt = ($this->hatAnlagentopologie()
|
||||
|| $this->prognoseTelemetrieKonfiguriert()
|
||||
$geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv')
|
||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
||||
if (!$this->aktualisiereLizenzfreigabe(
|
||||
@@ -306,6 +310,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$automatisch = $this->ReadPropertyBoolean('AutomatischeSuche');
|
||||
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
|
||||
$peakShavingLizenziert = $this->peakShavingLizenziert();
|
||||
$prognoseLizenziert = $this->prognoseLizenziert();
|
||||
$netzfahrplanLizenziert = $this->berechtigungLizenziert(
|
||||
Lizenzpruefung::NETZFAHRPLAN
|
||||
);
|
||||
@@ -352,19 +357,32 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
'options',
|
||||
$lastspitzenOptionen
|
||||
);
|
||||
foreach (['EinspeisebegrenzungAktiv', 'Einspeisegrenze', 'Einspeisetoleranz'] as $feld) {
|
||||
foreach (['PeakShavingTitel', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze'] as $feld) {
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
$feld,
|
||||
'enabled',
|
||||
'visible',
|
||||
$peakShavingLizenziert
|
||||
);
|
||||
}
|
||||
$prognoseAktiv = $this->ReadPropertyBoolean('PrognoseAktiv');
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
'PrognoseAktiv',
|
||||
'enabled',
|
||||
$prognoseLizenziert || $prognoseAktiv
|
||||
);
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
'PrognoseLizenzInformation',
|
||||
'caption',
|
||||
$this->prognoseLizenzStatustext($prognoseAktiv, $prognoseLizenziert)
|
||||
);
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
'NetzfahrplanAktiv',
|
||||
'enabled',
|
||||
$peakShavingLizenziert && $netzfahrplanLizenziert
|
||||
$prognoseAktiv && $peakShavingLizenziert && $netzfahrplanLizenziert
|
||||
);
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
@@ -441,6 +459,27 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->zeichneMesswerteAuf();
|
||||
return;
|
||||
|
||||
case 'FormPrognoseAktiv':
|
||||
$aktiv = (bool) $wert;
|
||||
$lizenziert = $this->prognoseLizenziert();
|
||||
$netzfahrplanLizenziert = $this->berechtigungLizenziert(
|
||||
Lizenzpruefung::NETZFAHRPLAN
|
||||
);
|
||||
$this->UpdateFormField(
|
||||
'PrognoseLizenzInformation',
|
||||
'caption',
|
||||
$this->prognoseLizenzStatustext($aktiv, $lizenziert)
|
||||
);
|
||||
$this->UpdateFormField(
|
||||
'NetzfahrplanAktiv',
|
||||
'enabled',
|
||||
$aktiv && $netzfahrplanLizenziert && $this->peakShavingLizenziert()
|
||||
);
|
||||
if (!$aktiv) {
|
||||
$this->UpdateFormField('NetzfahrplanAktiv', 'value', false);
|
||||
}
|
||||
return;
|
||||
|
||||
case 'FormEnergyPieAnzeigen':
|
||||
$this->synchronisiereEnergyPie((bool) $wert);
|
||||
return;
|
||||
@@ -463,7 +502,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
$this->aktualisiereLizenzfreigabe($wert, true);
|
||||
$this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert));
|
||||
$this->aktualisiereLastspitzenformular();
|
||||
$this->aktualisiereLastspitzenformular($wert);
|
||||
$this->aktualisierePrognoseformular($wert);
|
||||
return;
|
||||
|
||||
case 'FormAutomatischeSuche':
|
||||
@@ -737,7 +777,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$ergebnis = EinspeiseRegler::verteile(
|
||||
$netzleistung,
|
||||
$this->ReadPropertyFloat('Einspeisegrenze'),
|
||||
$this->ReadPropertyFloat('Einspeisetoleranz'),
|
||||
$wechselrichter,
|
||||
$letzteGrenzen
|
||||
);
|
||||
@@ -1713,6 +1752,105 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $quellen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<array{Schluessel: string, VariableID: int, Faktor: float}>>|null
|
||||
*/
|
||||
private function energieZaehlerquellen(): ?array
|
||||
{
|
||||
$wechselrichter = $this->leseAnlagenKonfigurationsliste('AnlagenWechselrichter');
|
||||
$pvFlaechen = $this->leseAnlagenKonfigurationsliste('AnlagenPVFlaechen');
|
||||
$batterien = $this->leseAnlagenKonfigurationsliste('AnlagenBatterien');
|
||||
$pvWechselrichter = [];
|
||||
foreach ($pvFlaechen as $pvFlaeche) {
|
||||
$id = trim((string) ($pvFlaeche['WechselrichterID'] ?? ''));
|
||||
if ($id !== '') {
|
||||
$pvWechselrichter[$id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$hatZaehler = $this->ReadPropertyInteger('NetzbezugEnergieVariableID') > 0
|
||||
|| $this->ReadPropertyInteger('NetzeinspeisungEnergieVariableID') > 0;
|
||||
foreach ($wechselrichter as $eintrag) {
|
||||
$hatZaehler = $hatZaehler || (int) ($eintrag['ErzeugungsenergieVariableID'] ?? 0) > 0;
|
||||
}
|
||||
foreach ($batterien as $eintrag) {
|
||||
$hatZaehler = $hatZaehler
|
||||
|| (int) ($eintrag['LadeenergieVariableID'] ?? 0) > 0
|
||||
|| (int) ($eintrag['EntladeenergieVariableID'] ?? 0) > 0;
|
||||
}
|
||||
if (!$hatZaehler) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$quellen = [
|
||||
'PVEnergie' => [],
|
||||
'NetzbezugEnergie' => [],
|
||||
'EinspeisungEnergie' => [],
|
||||
'BatterieLadenEnergie' => [],
|
||||
'BatterieEntladenEnergie' => [],
|
||||
];
|
||||
$netz = [
|
||||
'NetzbezugEnergie' => [
|
||||
$this->ReadPropertyInteger('NetzbezugEnergieVariableID'),
|
||||
$this->ReadPropertyFloat('NetzbezugEnergiefaktor'),
|
||||
'Netzbezug',
|
||||
],
|
||||
'EinspeisungEnergie' => [
|
||||
$this->ReadPropertyInteger('NetzeinspeisungEnergieVariableID'),
|
||||
$this->ReadPropertyFloat('NetzeinspeisungEnergiefaktor'),
|
||||
'Netzeinspeisung',
|
||||
],
|
||||
];
|
||||
foreach ($netz as $ident => [$variableID, $faktor, $name]) {
|
||||
$quellen[$ident][] = $this->energieZaehlerquelle(
|
||||
$name,
|
||||
$variableID,
|
||||
$faktor,
|
||||
strtolower($ident)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($wechselrichter as $index => $eintrag) {
|
||||
$typ = (string) ($eintrag['Typ'] ?? '');
|
||||
$id = trim((string) ($eintrag['ID'] ?? ''));
|
||||
if ($pvWechselrichter !== []) {
|
||||
if (!isset($pvWechselrichter[$id])) {
|
||||
continue;
|
||||
}
|
||||
} elseif (!in_array($typ, ['pv', 'hybrid'], true)) {
|
||||
continue;
|
||||
}
|
||||
$quellen['PVEnergie'][] = $this->energieZaehlerquelle(
|
||||
'Wechselrichter ' . ($index + 1) . ': Erzeugungsenergie',
|
||||
(int) ($eintrag['ErzeugungsenergieVariableID'] ?? 0),
|
||||
$eintrag['Erzeugungsenergiefaktor'] ?? 1.0,
|
||||
'pv:' . $id
|
||||
);
|
||||
}
|
||||
if ($quellen['PVEnergie'] === []) {
|
||||
throw new InvalidArgumentException('Mindestens ein PV-Erzeugungszaehler ist erforderlich.');
|
||||
}
|
||||
|
||||
foreach ($batterien as $index => $eintrag) {
|
||||
$id = trim((string) ($eintrag['ID'] ?? ''));
|
||||
$faktor = $eintrag['Energiefaktor'] ?? 1.0;
|
||||
$quellen['BatterieLadenEnergie'][] = $this->energieZaehlerquelle(
|
||||
'Batterie ' . ($index + 1) . ': Ladeenergie',
|
||||
(int) ($eintrag['LadeenergieVariableID'] ?? 0),
|
||||
$faktor,
|
||||
'batterie-laden:' . $id
|
||||
);
|
||||
$quellen['BatterieEntladenEnergie'][] = $this->energieZaehlerquelle(
|
||||
'Batterie ' . ($index + 1) . ': Entladeenergie',
|
||||
(int) ($eintrag['EntladeenergieVariableID'] ?? 0),
|
||||
$faktor,
|
||||
'batterie-entladen:' . $id
|
||||
);
|
||||
}
|
||||
|
||||
return $quellen;
|
||||
}
|
||||
|
||||
/** @return list<array<string, mixed>> */
|
||||
private function leseAnlagenKonfigurationsliste(string $property): array
|
||||
{
|
||||
@@ -1738,6 +1876,37 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $faktor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $faktor
|
||||
* @return array{Schluessel: string, VariableID: int, Faktor: float}
|
||||
*/
|
||||
private function energieZaehlerquelle(
|
||||
string $bezeichnung,
|
||||
int $variableID,
|
||||
$faktor,
|
||||
string $schluessel
|
||||
): array {
|
||||
if (!$this->istNumerischeVariable($variableID)) {
|
||||
throw new InvalidArgumentException(
|
||||
$bezeichnung . ' muss eine numerische Variable sein.'
|
||||
);
|
||||
}
|
||||
if ((!is_int($faktor) && !is_float($faktor))
|
||||
|| !is_finite((float) $faktor)
|
||||
|| (float) $faktor <= 0.0
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
$bezeichnung . ': Energiefaktor muss groesser als 0 sein.'
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'Schluessel' => $schluessel,
|
||||
'VariableID' => $variableID,
|
||||
'Faktor' => (float) $faktor,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{PV: float, Haus: float, Netz: float, Batterie: float} */
|
||||
private function leseMessleistungen(): array
|
||||
{
|
||||
@@ -1780,6 +1949,58 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $gewicht > 0.0 ? $summe / $gewicht : null;
|
||||
}
|
||||
|
||||
/** @return array<string, float>|null */
|
||||
private function leseEnergieZaehlerDeltas(): ?array
|
||||
{
|
||||
$quellen = $this->energieZaehlerquellen();
|
||||
if ($quellen === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$alt = $this->leseJsonAttribut('MessungZaehlerstaende');
|
||||
$neu = [];
|
||||
$deltas = [];
|
||||
$vollstaendigeBasis = true;
|
||||
foreach ($quellen as $ident => $identQuellen) {
|
||||
$deltas[$ident] = 0.0;
|
||||
foreach ($identQuellen as $quelle) {
|
||||
$schluessel = $quelle['Schluessel'];
|
||||
$stand = (float) GetValue($quelle['VariableID']) * $quelle['Faktor'];
|
||||
if (!is_finite($stand) || $stand < 0.0) {
|
||||
throw new RuntimeException(
|
||||
'Energiezaehler ' . $schluessel . ' liefert keinen gueltigen Stand.'
|
||||
);
|
||||
}
|
||||
$neu[$schluessel] = $stand;
|
||||
if (!isset($alt[$schluessel]) || !is_numeric($alt[$schluessel])) {
|
||||
$vollstaendigeBasis = false;
|
||||
continue;
|
||||
}
|
||||
$delta = $stand - (float) $alt[$schluessel];
|
||||
if ($delta >= 0.0) {
|
||||
$deltas[$ident] += $delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->WriteAttributeString(
|
||||
'MessungZaehlerstaende',
|
||||
json_encode($neu, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION)
|
||||
);
|
||||
if (!$vollstaendigeBasis) {
|
||||
return array_fill_keys(self::ENERGIE_VARIABLEN, 0.0);
|
||||
}
|
||||
|
||||
$deltas['Hausenergie'] = EnergieMessung::hausenergie(
|
||||
$deltas['PVEnergie'],
|
||||
$deltas['NetzbezugEnergie'],
|
||||
$deltas['EinspeisungEnergie'],
|
||||
$deltas['BatterieLadenEnergie'],
|
||||
$deltas['BatterieEntladenEnergie']
|
||||
);
|
||||
|
||||
return $deltas;
|
||||
}
|
||||
|
||||
private function zeichneMesswerteAuf(): void
|
||||
{
|
||||
if (!$this->messungKonfiguriert()) {
|
||||
@@ -1789,11 +2010,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$leistungen = $this->leseMessleistungen();
|
||||
$letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel');
|
||||
$puffer = $this->leseJsonAttribut('MessungEnergiePuffer');
|
||||
if ($letzterZeitstempel > 0) {
|
||||
$energieDeltas = $this->leseEnergieZaehlerDeltas();
|
||||
if ($energieDeltas === null && $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;
|
||||
$energieDeltas = EnergieMessung::energieDeltas($leistungen, $sekunden);
|
||||
}
|
||||
foreach ($energieDeltas ?? [] as $ident => $delta) {
|
||||
$puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta;
|
||||
}
|
||||
$this->WriteAttributeInteger('MessungLetzterZeitstempel', $jetzt);
|
||||
$this->WriteAttributeString(
|
||||
@@ -1899,8 +2122,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
&& $this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
|
||||
$gueltig = $leaseGueltig && $berechtigungenPassen;
|
||||
$berechtigungenVeraltet = $basisGueltig && !$berechtigungenPassen;
|
||||
$geraetezugangFehlt = ($this->hatAnlagentopologie()
|
||||
|| $this->prognoseTelemetrieKonfiguriert()
|
||||
$geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv')
|
||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
||||
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|
||||
@@ -2022,15 +2244,36 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $this->ReadPropertyBoolean('EinspeisebegrenzungAktiv') ? 1 : 0;
|
||||
}
|
||||
|
||||
private function peakShavingLizenziert(): bool
|
||||
private function peakShavingLizenziert(?string $lizenzcode = null): bool
|
||||
{
|
||||
return $this->berechtigungLizenziert(Lizenzpruefung::MANAGER_PEAK);
|
||||
return $this->berechtigungLizenziert(Lizenzpruefung::MANAGER_PEAK, $lizenzcode);
|
||||
}
|
||||
|
||||
private function berechtigungLizenziert(string $kennung): bool
|
||||
private function prognoseLizenziert(?string $lizenzcode = null): bool
|
||||
{
|
||||
return $this->berechtigungLizenziert(Lizenzpruefung::NETZFAHRPLAN, $lizenzcode)
|
||||
|| ($this->berechtigungLizenziert(Lizenzpruefung::PROGNOSE_PV, $lizenzcode)
|
||||
&& $this->berechtigungLizenziert(
|
||||
Lizenzpruefung::PROGNOSE_VERBRAUCH,
|
||||
$lizenzcode
|
||||
));
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $lease */
|
||||
private function prognoseBerechtigungInLease(array $lease): bool
|
||||
{
|
||||
return Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::NETZFAHRPLAN) > 0
|
||||
|| (Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::PROGNOSE_PV) > 0
|
||||
&& Lizenzpruefung::berechtigungsMenge(
|
||||
$lease,
|
||||
Lizenzpruefung::PROGNOSE_VERBRAUCH
|
||||
) > 0);
|
||||
}
|
||||
|
||||
private function berechtigungLizenziert(string $kennung, ?string $lizenzcode = null): bool
|
||||
{
|
||||
$lizenzcode = Lizenzpruefung::normalisiereCode(
|
||||
$this->ReadPropertyString('Lizenzcode')
|
||||
$lizenzcode ?? $this->ReadPropertyString('Lizenzcode')
|
||||
);
|
||||
if (!Lizenzpruefung::istLizenzcodeGueltig($lizenzcode)) {
|
||||
return false;
|
||||
@@ -2068,6 +2311,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('PrognoseAktiv')
|
||||
&& !$this->prognoseBerechtigungInLease($lease)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('NetzfahrplanAktiv')
|
||||
&& Lizenzpruefung::berechtigungsMenge(
|
||||
$lease,
|
||||
@@ -2108,9 +2356,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $optionen;
|
||||
}
|
||||
|
||||
private function aktualisiereLastspitzenformular(): void
|
||||
private function aktualisiereLastspitzenformular(?string $lizenzcode = null): void
|
||||
{
|
||||
$lizenziert = $this->peakShavingLizenziert();
|
||||
$lizenziert = $this->peakShavingLizenziert($lizenzcode);
|
||||
$modus = $lizenziert ? $this->ReadPropertyInteger('Lastspitzenmodus') : 0;
|
||||
$this->UpdateFormField(
|
||||
'Lastspitzenmodus',
|
||||
@@ -2118,6 +2366,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
json_encode($this->lastspitzenOptionen($lizenziert), JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$this->UpdateFormField('Lastspitzenmodus', 'value', $modus);
|
||||
foreach (['PeakShavingTitel', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze'] as $feld) {
|
||||
$this->UpdateFormField($feld, 'visible', $lizenziert);
|
||||
}
|
||||
$this->UpdateFormField(
|
||||
'Lastspitzengrenze',
|
||||
'visible',
|
||||
@@ -2134,6 +2385,43 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
}
|
||||
|
||||
private function aktualisierePrognoseformular(?string $lizenzcode = null): void
|
||||
{
|
||||
$aktiv = $this->ReadPropertyBoolean('PrognoseAktiv');
|
||||
$lizenziert = $this->prognoseLizenziert($lizenzcode);
|
||||
$netzfahrplanLizenziert = $this->berechtigungLizenziert(
|
||||
Lizenzpruefung::NETZFAHRPLAN,
|
||||
$lizenzcode
|
||||
);
|
||||
$this->UpdateFormField('PrognoseAktiv', 'enabled', $lizenziert || $aktiv);
|
||||
$this->UpdateFormField(
|
||||
'PrognoseLizenzInformation',
|
||||
'caption',
|
||||
$this->prognoseLizenzStatustext($aktiv, $lizenziert)
|
||||
);
|
||||
$this->UpdateFormField(
|
||||
'NetzfahrplanAktiv',
|
||||
'enabled',
|
||||
$aktiv && $netzfahrplanLizenziert
|
||||
&& $this->peakShavingLizenziert($lizenzcode)
|
||||
);
|
||||
}
|
||||
|
||||
private function prognoseLizenzStatustext(bool $aktiv, bool $lizenziert): string
|
||||
{
|
||||
$managerID = $this->ReadAttributeString('LizenzInstallationID');
|
||||
if (!$lizenziert) {
|
||||
return 'Prognosen sind deaktiviert. Fuer die Manager-ID '
|
||||
. $managerID
|
||||
. ' fehlen forecast_pv plus forecast_load oder grid_schedule.';
|
||||
}
|
||||
|
||||
return ($aktiv ? 'Prognosen sind aktiviert. ' : 'Prognosen sind deaktiviert. ')
|
||||
. 'Die passende Lizenz fuer die Manager-ID '
|
||||
. $managerID
|
||||
. ' ist vorhanden.';
|
||||
}
|
||||
|
||||
private function lizenzStatustext(?string $lizenzcode = null): string
|
||||
{
|
||||
$code = Lizenzpruefung::normalisiereCode(
|
||||
@@ -2184,8 +2472,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$nutzlast = json_encode([
|
||||
'code' => $lizenzcode,
|
||||
'installationId' => $installationID,
|
||||
'issueDeviceToken' => ($this->hatAnlagentopologie()
|
||||
|| $this->prognoseTelemetrieKonfiguriert()
|
||||
'issueDeviceToken' => ($this->ReadPropertyBoolean('PrognoseAktiv')
|
||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '',
|
||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
||||
@@ -2256,10 +2543,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
private function aktualisiereAnschlussstatus(): void
|
||||
{
|
||||
foreach ([
|
||||
'PrognoseAnschluss' => 'Prognosestatus',
|
||||
'SDLAnschluss' => 'SDLStatus',
|
||||
] as $property => $variable) {
|
||||
$this->setzeDiagnosewert(
|
||||
'Prognosestatus',
|
||||
$this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet'
|
||||
);
|
||||
foreach (['SDLAnschluss' => 'SDLStatus'] as $property => $variable) {
|
||||
$konfiguration = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
|
||||
$aktiv = is_array($konfiguration) && ($konfiguration['Aktiv'] ?? false) === true;
|
||||
$this->setzeDiagnosewert($variable, $aktiv ? 'Fehler' : 'NichtVerwendet');
|
||||
@@ -2306,16 +2594,22 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
) {
|
||||
throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 liegen.');
|
||||
}
|
||||
if ($this->ReadPropertyFloat('Einspeisegrenze') < 0.0
|
||||
|| $this->ReadPropertyFloat('Einspeisetoleranz') < 0.0
|
||||
) {
|
||||
throw new InvalidArgumentException('Einspeisegrenze und Einspeisetoleranz duerfen nicht negativ sein.');
|
||||
if ($this->ReadPropertyFloat('Einspeisegrenze') < 0.0) {
|
||||
throw new InvalidArgumentException('Einspeisegrenze darf nicht negativ sein.');
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('EinspeisebegrenzungAktiv')) {
|
||||
$this->regelbareWechselrichter();
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('PrognoseAktiv')
|
||||
&& !$this->prognoseTelemetrieKonfiguriert()
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
'Aktive Prognosen benoetigen Netzleistung, PV-Leistung und Batterie-Ladezustand.'
|
||||
);
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('NetzfahrplanAktiv')
|
||||
&& (!$this->ReadPropertyBoolean('EinspeisebegrenzungAktiv')
|
||||
&& (!$this->ReadPropertyBoolean('PrognoseAktiv')
|
||||
|| !$this->ReadPropertyBoolean('EinspeisebegrenzungAktiv')
|
||||
|| !$this->prognoseTelemetrieKonfiguriert())
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
@@ -2329,6 +2623,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->pvMessquellen();
|
||||
$this->batterieMessquellen();
|
||||
$this->batterieSocQuellen();
|
||||
$this->energieZaehlerquellen();
|
||||
foreach (['PrognoseAnschluss', 'SDLAnschluss'] as $property) {
|
||||
$wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($wert)) {
|
||||
@@ -2348,18 +2643,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
);
|
||||
}
|
||||
|
||||
private function hatAnlagentopologie(): bool
|
||||
{
|
||||
$topologie = $this->leseAnlagentopologie();
|
||||
|
||||
return $topologie['inverters'] !== []
|
||||
|| $topologie['pvArrays'] !== []
|
||||
|| $topologie['batteries'] !== [];
|
||||
}
|
||||
|
||||
private function prognoseTelemetrieKonfiguriert(): bool
|
||||
{
|
||||
return $this->messquellenKonfiguriert()
|
||||
return $this->ReadPropertyBoolean('PrognoseAktiv')
|
||||
&& $this->messquellenKonfiguriert()
|
||||
&& $this->aktuelleBatterieSoc() !== null;
|
||||
}
|
||||
|
||||
@@ -2407,6 +2694,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
private function synchronisiereAnlagentopologie(): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('PrognoseAktiv')) {
|
||||
$this->setzeDiagnosewert('Prognosestatus', 'NichtVerwendet');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$topologie = $this->leseAnlagentopologie();
|
||||
if ($topologie['inverters'] === []
|
||||
|
||||
@@ -62,12 +62,15 @@ Die drei Regelungsvariablen `Aktiv`, `Betriebsart` und `Netzleistung` sind immer
|
||||
| `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
|
||||
| `EinspeisebegrenzungAktiv` | Boolean | `false`; gemeinsame Exportgrenze am Netzanschlusspunkt aktivieren. |
|
||||
| `Einspeisegrenze` | Float | Maximale Einspeisung der Gesamtanlage in W; `0` bedeutet Nulleinspeisung. |
|
||||
| `Einspeisetoleranz` | Float | `100` W; Totband gegen pendelnde Wechselrichtervorgaben. |
|
||||
| `PrognoseAktiv` | Boolean | `false`; Prognosetelemetrie und Topologiesynchronisation mit `forecast_pv` plus `forecast_load` oder `grid_schedule` aktivieren. |
|
||||
| `NetzfahrplanAktiv` | Boolean | `false`; lizenzierten, tarif- und prognosebasierten Netzzielwert verwenden. |
|
||||
| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Grenzen, Istleistungsmessung und optionalem Begrenzungsregister. |
|
||||
| `NetzbezugEnergieVariableID` | Integer | Optionaler kumulativer Netzbezugszaehler. |
|
||||
| `NetzeinspeisungEnergieVariableID` | Integer | Optionaler kumulativer Einspeisezaehler. |
|
||||
| `NetzbezugEnergiefaktor`, `NetzeinspeisungEnergiefaktor` | Float | Umrechnung der Netzenergiezaehler nach kWh. |
|
||||
| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Nennleistung, Leistungs- und Erzeugungsenergiemessung sowie optionalem Begrenzungsregister. |
|
||||
| `AnlagenPVFlaechen` | String/JSON | `[]`; PV-Flaechen mit DC-Leistung, Ausrichtung und Wechselrichter-ID. |
|
||||
| `AnlagenBatterien` | String/JSON | `[]`; Batteriespeicher mit Kapazitaeten, Kopplung, Leistungs- und SOC-Messung. |
|
||||
| `PrognoseSendeintervall` | Integer | `60` s; zulaessig sind 60 bis 3600 Sekunden. |
|
||||
| `AnlagenBatterien` | String/JSON | `[]`; Batteriespeicher mit Kapazitaeten, Kopplung, Leistung, SOC sowie Lade- und Entladeenergiezaehlern. |
|
||||
| `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. |
|
||||
| `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. |
|
||||
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
|
||||
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. |
|
||||
@@ -99,9 +102,14 @@ 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.
|
||||
`PV + Netz - Batterie` bilanziert. Sind Netzbezugs-, Einspeise-, PV-Erzeugungs-
|
||||
sowie gegebenenfalls Batterie-Lade- und Entladezaehler vollstaendig hinterlegt,
|
||||
verwendet der Manager deren Deltas fuer die Energieaufzeichnung. Die Hausenergie
|
||||
wird dann als `PV + Netzbezug + Batterieentladung - Einspeisung - Batterieladung`
|
||||
berechnet. Ohne vollstaendigen Zaehlerdatensatz integriert der Manager weiterhin
|
||||
die Leistungswerte. Dieselben Quellen versorgen ohne zweite Eingabe auch die
|
||||
Prognosetelemetrie. Ausfallluecken ueber fuenf Minuten werden bei der
|
||||
Leistungsintegration nicht nachberechnet.
|
||||
|
||||
Batteriespeicher bleiben als physische Komponenten samt Messquellen in der
|
||||
Anlagentopologie. Ein zusaetzliches Batterieverbrauchermodul beschreibt dagegen
|
||||
@@ -136,9 +144,10 @@ absolute Leistung in Watt ausgegeben werden.
|
||||
|
||||
Der Manager berechnet eine einzige Grenze fuer die Gesamtanlage. Beim Abregeln
|
||||
verteilt er sie nach der tatsaechlichen Erzeugung, beim Freigeben nach der
|
||||
AC-Nennleistung auf alle angebotenen Wechselrichter. Das Totband verhindert
|
||||
Pendeln am Grenzwert. Beim Abschalten des Managers oder der
|
||||
Funktion werden zuvor gesetzte Grenzen kontrolliert auf die jeweilige
|
||||
AC-Nennleistung auf alle angebotenen Wechselrichter. Jede gemessene Abweichung
|
||||
wird stufenlos nachgefuehrt; ein zusaetzliches Toleranzfenster wird nicht
|
||||
verwendet. Beim Abschalten des Managers oder der Funktion werden zuvor gesetzte
|
||||
Grenzen kontrolliert auf die jeweilige
|
||||
Nennleistung zurueckgesetzt. Wechselrichter ohne beide Register werden nicht
|
||||
geregelt; sobald eines der beiden Register gesetzt ist, muessen beide gueltig
|
||||
sein und das Begrenzungsregister eine IP-Symcon-Aktion besitzen.
|
||||
@@ -157,8 +166,10 @@ davon aktiv.
|
||||
|
||||
Der Manager ist die fuehrende Quelle fuer die technischen Stammdaten. Die
|
||||
Konfiguration trennt Wechselrichter, PV-Flaechen und Batteriespeicher. AC-Leistung
|
||||
wird am Wechselrichter, DC-Leistung an der PV-Flaeche gepflegt. Modulanzahl und
|
||||
Modulleistung sind optional und muessen gemeinsam gesetzt werden.
|
||||
wird am Wechselrichter, DC-Leistung an der PV-Flaeche gepflegt. Individuelle
|
||||
AC-Einspeise- und Bezugsgrenzen werden nicht erfasst, weil die Begrenzung
|
||||
anlagenweit am Netzanschlusspunkt erfolgt. Modulanzahl und Modulleistung sind
|
||||
optional und muessen gemeinsam gesetzt werden.
|
||||
|
||||
PV-Flaechen und Batterien referenzieren ihren Wechselrichter ueber dessen stabile
|
||||
ID. Bei `hybrid` koennen beide denselben Wechselrichter verwenden. Der Export
|
||||
@@ -167,9 +178,10 @@ AC-Grenze aus, damit PV und Batterie die Nennleistung nicht unabhaengig doppelt
|
||||
beanspruchen.
|
||||
|
||||
Der JSON-Export enthaelt die Vertragsversion `1.0`, die Lizenz-Installations-ID,
|
||||
die drei Komponentenlisten, gemeinsame AC-Grenzen und aggregierte Summen fuer
|
||||
die schrittweise Prognoseanbindung. Tarif- und Variantenparameter bleiben im
|
||||
Prognoseportal. Eine nichtleere Topologie wird beim Speichern automatisch ueber
|
||||
die drei Komponentenlisten, gemeinsame AC-Nennleistungen und aggregierte Summen
|
||||
fuer die schrittweise Prognoseanbindung. Interne Symcon-Variablen-IDs werden
|
||||
nicht exportiert. Tarif- und Variantenparameter bleiben im Prognoseportal. Bei
|
||||
aktivierter Prognose wird eine nichtleere Topologie beim Speichern automatisch ueber
|
||||
einen separaten, widerrufbaren Installationszugang synchronisiert. Der
|
||||
Lizenzserver liefert diesen Zugang nur auf ausdrueckliche Geraeteanforderung;
|
||||
der Manager entfernt ihn vor dem Speichern aus der Lease und haelt ihn in einem
|
||||
@@ -177,9 +189,12 @@ internen Attribut. Der Lizenzcode wird nicht als API-Token verwendet. Ein
|
||||
Synchronisationsfehler erscheint im `Prognosestatus`, blockiert die lokale
|
||||
EMS-Regelung aber nicht.
|
||||
|
||||
Sind PV-Leistung, Hausverbrauch und Batterie-SOC gemeinsam konfiguriert, sendet
|
||||
der Manager diese Werte zusammen mit der faktorbereinigten Netzleistung und
|
||||
einem UTC-Zeitstempel im eingestellten Intervall. Das Minimum von 60 Sekunden
|
||||
Der Forecast-Schalter zeigt direkt an, ob fuer die Manager-ID eine passende
|
||||
Kombination `forecast_pv` plus `forecast_load` oder `grid_schedule` vorhanden ist. Nach der Aktivierung sendet der
|
||||
Manager PV-Leistung, berechneten Hausverbrauch, Netzleistung und Batterie-SOC
|
||||
mit UTC-Zeitstempel im eingestellten Sendeintervall. Dieses Intervall bestimmt
|
||||
also, wie oft aktuelle Messwerte zum Prognosedienst hochgeladen werden; es ist
|
||||
kein Regelintervall. Das Minimum von 60 Sekunden
|
||||
passt zum Rate-Limit des Lizenzportals. Die Uebertragung nutzt denselben
|
||||
widerrufbaren Installationszugang wie die Topologie; Lizenzcode und Geraet Token
|
||||
werden weder als Telemetriefelder noch im Debug-Log ausgegeben. Bei HTTP 401
|
||||
|
||||
@@ -285,8 +285,6 @@ function enelixDemoInstall(): array
|
||||
'Name' => 'Demo-Hybridwechselrichter',
|
||||
'Typ' => 'hybrid',
|
||||
'ACNennleistung_kW' => $pvNennleistungKw,
|
||||
'ACEinspeisegrenze_kW' => 0.0,
|
||||
'ACBezugsgrenze_kW' => 0.0,
|
||||
'IstleistungVariableID' => $inverterActual,
|
||||
'Istleistungsfaktor' => 1.0,
|
||||
'BegrenzungVariableID' => $inverterLimit,
|
||||
@@ -338,7 +336,7 @@ function enelixDemoInstall(): array
|
||||
'Umschaltdifferenz' => 0.0,
|
||||
'EinspeisebegrenzungAktiv' => true,
|
||||
'Einspeisegrenze' => 3000.0,
|
||||
'Einspeisetoleranz' => 100.0,
|
||||
'PrognoseAktiv' => false,
|
||||
'NetzfahrplanAktiv' => false,
|
||||
'AnlagenWechselrichter' => json_encode(
|
||||
$wechselrichterKonfiguration,
|
||||
|
||||
@@ -47,16 +47,6 @@ final class Anlagentopologie
|
||||
'name' => self::name($eintrag['Name'] ?? null, $id),
|
||||
'type' => $typ,
|
||||
'acRatedKw' => $acRatedKw,
|
||||
'acExportLimitKw' => self::optionaleGrenze(
|
||||
$eintrag['ACEinspeisegrenze_kW'] ?? null,
|
||||
$acRatedKw,
|
||||
$pfad . ': AC-Einspeisegrenze'
|
||||
),
|
||||
'acImportLimitKw' => self::optionaleGrenze(
|
||||
$eintrag['ACBezugsgrenze_kW'] ?? null,
|
||||
$acRatedKw,
|
||||
$pfad . ': AC-Bezugsgrenze'
|
||||
),
|
||||
];
|
||||
$inverters[] = $inverter;
|
||||
$inverterById[$id] = $inverter;
|
||||
@@ -304,18 +294,6 @@ final class Anlagentopologie
|
||||
return (float) $wert;
|
||||
}
|
||||
|
||||
private static function optionaleGrenze($wert, float $nennleistung, string $feld): ?float
|
||||
{
|
||||
$grenze = self::optionalePositiveZahl($wert, $feld);
|
||||
if ($grenze !== null && $grenze > $nennleistung) {
|
||||
throw new InvalidArgumentException(
|
||||
$feld . ' darf die AC-Nennleistung nicht uebersteigen.'
|
||||
);
|
||||
}
|
||||
|
||||
return $grenze;
|
||||
}
|
||||
|
||||
private static function optionalerText($wert): ?string
|
||||
{
|
||||
if ($wert === null || $wert === '') {
|
||||
|
||||
@@ -26,16 +26,12 @@ final class EinspeiseRegler
|
||||
public static function verteile(
|
||||
float $netzleistung,
|
||||
float $einspeisegrenze,
|
||||
float $toleranz,
|
||||
array $wechselrichter,
|
||||
array $letzteGrenzen = []
|
||||
): array {
|
||||
if ($einspeisegrenze < 0.0) {
|
||||
throw new InvalidArgumentException('Einspeisegrenze darf nicht negativ sein.');
|
||||
}
|
||||
if ($toleranz < 0.0) {
|
||||
throw new InvalidArgumentException('Einspeisetoleranz darf nicht negativ sein.');
|
||||
}
|
||||
if ($wechselrichter === []) {
|
||||
throw new InvalidArgumentException('Mindestens ein regelbarer Wechselrichter ist erforderlich.');
|
||||
}
|
||||
@@ -81,12 +77,11 @@ final class EinspeiseRegler
|
||||
$netzabweichung = $netzleistung + $einspeisegrenze;
|
||||
$zielGesamt = $bisherigeGrenzeGesamt;
|
||||
$nachIstleistungVerteilen = false;
|
||||
if ($netzabweichung < -$toleranz) {
|
||||
if ($netzabweichung < 0.0) {
|
||||
$nachIstleistungVerteilen = $istleistungGesamt > 0.0;
|
||||
$wirksameAbweichung = $netzabweichung + $toleranz;
|
||||
$zielGesamt = min($bisherigeGrenzeGesamt, $istleistungGesamt) + $wirksameAbweichung;
|
||||
} elseif ($netzabweichung > $toleranz) {
|
||||
$zielGesamt = $bisherigeGrenzeGesamt + $netzabweichung - $toleranz;
|
||||
$zielGesamt = min($bisherigeGrenzeGesamt, $istleistungGesamt) + $netzabweichung;
|
||||
} elseif ($netzabweichung > 0.0) {
|
||||
$zielGesamt = $bisherigeGrenzeGesamt + $netzabweichung;
|
||||
}
|
||||
$zielGesamt = max(0.0, min($zielGesamt, $nennleistungGesamt));
|
||||
|
||||
|
||||
@@ -16,6 +16,23 @@ final class EnergieMessung
|
||||
);
|
||||
}
|
||||
|
||||
public static function hausenergie(
|
||||
float $pv,
|
||||
float $netzbezug,
|
||||
float $netzeinspeisung,
|
||||
float $batterieLaden,
|
||||
float $batterieEntladen
|
||||
): float {
|
||||
return max(
|
||||
0.0,
|
||||
self::endlich($pv)
|
||||
+ self::endlich($netzbezug)
|
||||
+ self::endlich($batterieEntladen)
|
||||
- self::endlich($netzeinspeisung)
|
||||
- self::endlich($batterieLaden)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{PV: float, Haus: float, Netz: float, Batterie: float} $leistungen
|
||||
* @return array{
|
||||
|
||||
@@ -18,8 +18,6 @@ final class AnlagentopologieTest extends TestCase
|
||||
'Name' => 'Hybrid Ost',
|
||||
'Typ' => 'hybrid',
|
||||
'ACNennleistung_kW' => 10.0,
|
||||
'ACEinspeisegrenze_kW' => 8.0,
|
||||
'ACBezugsgrenze_kW' => 0.0,
|
||||
]], JSON_THROW_ON_ERROR),
|
||||
json_encode([
|
||||
[
|
||||
@@ -62,6 +60,8 @@ final class AnlagentopologieTest extends TestCase
|
||||
self::assertSame(12.8, $topologie['summary']['pvDcKwp']);
|
||||
self::assertSame(10.0, $topologie['summary']['pvAcKw']);
|
||||
self::assertSame(12.8, $topologie['summary']['usableBatteryKwh']);
|
||||
self::assertArrayNotHasKey('acExportLimitKw', $topologie['inverters'][0]);
|
||||
self::assertArrayNotHasKey('acImportLimitKw', $topologie['inverters'][0]);
|
||||
self::assertSame([[
|
||||
'inverterId' => 'hybrid-1',
|
||||
'acRatedKw' => 10.0,
|
||||
|
||||
@@ -131,8 +131,9 @@ final class DemoanlageBootstrapTest extends TestCase
|
||||
self::assertStringContainsString("'Begrenzungsart' => 'watt'", $source);
|
||||
self::assertStringContainsString("'EinspeisebegrenzungAktiv' => true", $source);
|
||||
self::assertStringContainsString("'Einspeisegrenze' => 3000.0", $source);
|
||||
self::assertStringContainsString("'Einspeisetoleranz' => 100.0", $source);
|
||||
self::assertStringContainsString("'PrognoseAktiv' => false", $source);
|
||||
self::assertStringContainsString("'NetzfahrplanAktiv' => false", $source);
|
||||
self::assertStringNotContainsString("'Einspeisetoleranz'", $source);
|
||||
self::assertStringContainsString("'demo-hybrid-1'", $source);
|
||||
self::assertStringContainsString("'demo-pv-1'", $source);
|
||||
self::assertStringContainsString("'demo-battery-1'", $source);
|
||||
|
||||
@@ -15,17 +15,16 @@ final class EinspeiseReglerTest extends TestCase
|
||||
$ergebnis = EinspeiseRegler::verteile(
|
||||
-15000.0,
|
||||
10000.0,
|
||||
100.0,
|
||||
[
|
||||
['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 10000],
|
||||
['ID' => 'wr-2', 'Nennleistung_W' => 5000, 'Istleistung_W' => 5000],
|
||||
]
|
||||
);
|
||||
|
||||
self::assertSame(10100.0, $ergebnis['ZielGesamtleistung_W']);
|
||||
self::assertSame(4900.0, $ergebnis['Abregelbedarf_W']);
|
||||
self::assertSame(6733.333, $ergebnis['Grenzen_W']['wr-1']);
|
||||
self::assertSame(3366.667, $ergebnis['Grenzen_W']['wr-2']);
|
||||
self::assertSame(10000.0, $ergebnis['ZielGesamtleistung_W']);
|
||||
self::assertSame(5000.0, $ergebnis['Abregelbedarf_W']);
|
||||
self::assertSame(6666.667, $ergebnis['Grenzen_W']['wr-1']);
|
||||
self::assertSame(3333.333, $ergebnis['Grenzen_W']['wr-2']);
|
||||
}
|
||||
|
||||
public function testAbregelungFolgtDerTatsaechlichenErzeugung(): void
|
||||
@@ -33,7 +32,6 @@ final class EinspeiseReglerTest extends TestCase
|
||||
$ergebnis = EinspeiseRegler::verteile(
|
||||
-10000.0,
|
||||
5000.0,
|
||||
0.0,
|
||||
[
|
||||
['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 10000],
|
||||
['ID' => 'wr-2', 'Nennleistung_W' => 10000, 'Istleistung_W' => 0],
|
||||
@@ -49,7 +47,6 @@ final class EinspeiseReglerTest extends TestCase
|
||||
$ergebnis = EinspeiseRegler::verteile(
|
||||
-6000.0,
|
||||
10000.0,
|
||||
100.0,
|
||||
[
|
||||
['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 4000],
|
||||
['ID' => 'wr-2', 'Nennleistung_W' => 10000, 'Istleistung_W' => 2000],
|
||||
@@ -57,22 +54,21 @@ final class EinspeiseReglerTest extends TestCase
|
||||
['wr-1' => 5000, 'wr-2' => 5000]
|
||||
);
|
||||
|
||||
self::assertSame(13900.0, $ergebnis['ZielGesamtleistung_W']);
|
||||
self::assertSame(6950.0, $ergebnis['Grenzen_W']['wr-1']);
|
||||
self::assertSame(6950.0, $ergebnis['Grenzen_W']['wr-2']);
|
||||
self::assertSame(14000.0, $ergebnis['ZielGesamtleistung_W']);
|
||||
self::assertSame(7000.0, $ergebnis['Grenzen_W']['wr-1']);
|
||||
self::assertSame(7000.0, $ergebnis['Grenzen_W']['wr-2']);
|
||||
}
|
||||
|
||||
public function testToleranzVerhindertPendelnAmGrenzwert(): void
|
||||
public function testRegelungFolgtAuchKleinenAbweichungenStufenlos(): void
|
||||
{
|
||||
$ergebnis = EinspeiseRegler::verteile(
|
||||
-10050.0,
|
||||
10000.0,
|
||||
100.0,
|
||||
[['ID' => 'wr-1', 'Nennleistung_W' => 10000, 'Istleistung_W' => 9000]],
|
||||
['wr-1' => 8000]
|
||||
);
|
||||
|
||||
self::assertSame(8000.0, $ergebnis['ZielGesamtleistung_W']);
|
||||
self::assertSame(7950.0, $ergebnis['ZielGesamtleistung_W']);
|
||||
}
|
||||
|
||||
public function testStellwerteWerdenInProzentOderWattAusgegeben(): void
|
||||
|
||||
@@ -50,6 +50,12 @@ final class EnergieMessungTest extends TestCase
|
||||
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
|
||||
}
|
||||
|
||||
public function testBerechnetHausenergieAusDenZaehlerdeltas(): void
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
|
||||
{
|
||||
$werte = EnergieMessung::energieDeltas([
|
||||
|
||||
@@ -52,6 +52,11 @@ final class ManagerEnergieModulstrukturTest extends TestCase
|
||||
'IstleistungVariableID',
|
||||
'LeistungVariableID',
|
||||
'SOCVariableID',
|
||||
'NetzbezugEnergieVariableID',
|
||||
'NetzeinspeisungEnergieVariableID',
|
||||
'ErzeugungsenergieVariableID',
|
||||
'LadeenergieVariableID',
|
||||
'EntladeenergieVariableID',
|
||||
] as $erwartet) {
|
||||
self::assertStringContainsString($erwartet, $formular);
|
||||
}
|
||||
@@ -78,6 +83,8 @@ final class ManagerEnergieModulstrukturTest extends TestCase
|
||||
self::assertStringContainsString("RegisterTimer(
|
||||
'MesswerteAufzeichnen'", $modul);
|
||||
self::assertStringContainsString('EnergieMessung::energieDeltas', $modul);
|
||||
self::assertStringContainsString('EnergieMessung::hausenergie', $modul);
|
||||
self::assertStringContainsString("RegisterAttributeString('MessungZaehlerstaende'", $modul);
|
||||
}
|
||||
|
||||
public function testFunFactsSindResponsivUndErklaerenUmrechnungen(): void
|
||||
|
||||
@@ -50,9 +50,14 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
'Umschaltdifferenz',
|
||||
'EinspeisebegrenzungAktiv',
|
||||
'Einspeisegrenze',
|
||||
'Einspeisetoleranz',
|
||||
'PrognoseAktiv',
|
||||
'PrognoseLizenzInformation',
|
||||
'NetzfahrplanAktiv',
|
||||
'NetzbezugEnergieVariableID',
|
||||
'NetzeinspeisungEnergieVariableID',
|
||||
'IstleistungVariableID',
|
||||
'ErzeugungsenergieVariableID',
|
||||
'Erzeugungsenergiefaktor',
|
||||
'BegrenzungVariableID',
|
||||
'Begrenzungsart',
|
||||
'DiagnosevariablenAnzeigen',
|
||||
@@ -70,6 +75,9 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
'LeistungVariableID',
|
||||
'Leistungsfaktor',
|
||||
'SOCVariableID',
|
||||
'LadeenergieVariableID',
|
||||
'EntladeenergieVariableID',
|
||||
'Energiefaktor',
|
||||
'EnergyPieAnzeigen',
|
||||
'EnergiediagrammeAnzeigen',
|
||||
'FunFactsAnzeigen',
|
||||
@@ -81,7 +89,8 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
self::assertStringContainsString($property, $json);
|
||||
}
|
||||
foreach (['Regelintervall', 'Vorgabeintervall', 'UmschaltschwelleUeberschuss',
|
||||
'UmschaltschwelleLastspitze', 'Optionale obere Anschluesse', 'LizenzAnschluss'] as $entfernt
|
||||
'UmschaltschwelleLastspitze', 'Optionale obere Anschluesse', 'LizenzAnschluss',
|
||||
'Einspeisetoleranz', 'ACEinspeisegrenze_kW', 'ACBezugsgrenze_kW'] as $entfernt
|
||||
) {
|
||||
self::assertStringNotContainsString($entfernt, $json);
|
||||
}
|
||||
@@ -294,8 +303,16 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
$inhalt
|
||||
);
|
||||
self::assertStringContainsString('Lizenzpruefung::NETZFAHRPLAN', $inhalt);
|
||||
self::assertStringContainsString('Lizenzpruefung::PROGNOSE_PV', $inhalt);
|
||||
self::assertStringContainsString('Lizenzpruefung::PROGNOSE_VERBRAUCH', $inhalt);
|
||||
self::assertStringContainsString('prognoseLizenzStatustext(', $inhalt);
|
||||
self::assertStringContainsString(
|
||||
"foreach (['PeakShavingTitel', 'EinspeisebegrenzungAktiv', 'Einspeisegrenze']",
|
||||
$inhalt
|
||||
);
|
||||
self::assertStringContainsString('aktuelleNetzfahrplanVorgabe()', $inhalt);
|
||||
self::assertStringContainsString('gibEinspeisebegrenzungFrei()', $inhalt);
|
||||
self::assertStringNotContainsString("RegisterPropertyFloat('Einspeisetoleranz'", $inhalt);
|
||||
}
|
||||
|
||||
public function testManagerVerwendetDieNeuenZeitdefaults(): void
|
||||
|
||||
@@ -80,8 +80,6 @@ return static function (TestContext $test): void {
|
||||
'Name' => 'Hybridwechselrichter',
|
||||
'Typ' => 'hybrid',
|
||||
'ACNennleistung_kW' => 10.0,
|
||||
'ACEinspeisegrenze_kW' => 8.0,
|
||||
'ACBezugsgrenze_kW' => 0.0,
|
||||
'IstleistungVariableID' => $pvLeistungId,
|
||||
'Istleistungsfaktor' => 1.0,
|
||||
]];
|
||||
|
||||
Reference in New Issue
Block a user