This commit is contained in:
@@ -127,6 +127,36 @@
|
|||||||
],
|
],
|
||||||
"values": []
|
"values": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "Label",
|
||||||
|
"caption": "Peak Shaving am Netzanschlusspunkt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "EinspeisebegrenzungAktiv",
|
||||||
|
"caption": "Anlagenweite Einspeisebegrenzung aktivieren"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Einspeisegrenze",
|
||||||
|
"caption": "Maximale Einspeisung der Gesamtanlage",
|
||||||
|
"suffix": " W",
|
||||||
|
"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",
|
"type": "CheckBox",
|
||||||
"name": "AutomatischeSuche",
|
"name": "AutomatischeSuche",
|
||||||
@@ -283,6 +313,33 @@
|
|||||||
"minimum": 0.0,
|
"minimum": 0.0,
|
||||||
"digits": 3
|
"digits": 3
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "Istleistung",
|
||||||
|
"name": "IstleistungVariableID",
|
||||||
|
"width": "140px",
|
||||||
|
"add": 0,
|
||||||
|
"edit": {"type": "SelectVariable"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "PV-Begrenzung",
|
||||||
|
"name": "BegrenzungVariableID",
|
||||||
|
"width": "150px",
|
||||||
|
"add": 0,
|
||||||
|
"edit": {"type": "SelectVariable"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "Stellwert",
|
||||||
|
"name": "Begrenzungsart",
|
||||||
|
"width": "120px",
|
||||||
|
"add": "percent",
|
||||||
|
"edit": {
|
||||||
|
"type": "Select",
|
||||||
|
"options": [
|
||||||
|
{"caption": "Prozent", "value": "percent"},
|
||||||
|
{"caption": "Watt", "value": "watt"}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+394
-13
@@ -5,10 +5,12 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../libs/ManagerSchnittstelle.php';
|
require_once __DIR__ . '/../libs/ManagerSchnittstelle.php';
|
||||||
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
|
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
|
||||||
require_once __DIR__ . '/../libs/ManagerRegler.php';
|
require_once __DIR__ . '/../libs/ManagerRegler.php';
|
||||||
|
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';
|
||||||
|
|
||||||
use Belevo\EnelixEMS\Anlagentopologie;
|
use Belevo\EnelixEMS\Anlagentopologie;
|
||||||
|
use Belevo\EnelixEMS\EinspeiseRegler;
|
||||||
use Belevo\EnelixEMS\Lizenzpruefung;
|
use Belevo\EnelixEMS\Lizenzpruefung;
|
||||||
use Belevo\EnelixEMS\ManagerRegler;
|
use Belevo\EnelixEMS\ManagerRegler;
|
||||||
use Belevo\EnelixEMS\ManagerSchnittstelle;
|
use Belevo\EnelixEMS\ManagerSchnittstelle;
|
||||||
@@ -23,6 +25,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
private const LIZENZ_ENDPOINT = 'https://license.enelix.ch/api/v1/licenses/activate';
|
private const LIZENZ_ENDPOINT = 'https://license.enelix.ch/api/v1/licenses/activate';
|
||||||
private const PROGNOSE_TOPOLOGIE_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/topology';
|
private const PROGNOSE_TOPOLOGIE_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/topology';
|
||||||
private const PROGNOSE_TELEMETRIE_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry';
|
private const PROGNOSE_TELEMETRIE_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry';
|
||||||
|
private const PROGNOSE_FAHRPLAN_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/schedule';
|
||||||
private const LIZENZ_RETRY_SEKUNDEN = 3600;
|
private const LIZENZ_RETRY_SEKUNDEN = 3600;
|
||||||
private const VM_UPDATE = 10603;
|
private const VM_UPDATE = 10603;
|
||||||
private const VERBRAUCHER_LIZENZEN = [
|
private const VERBRAUCHER_LIZENZEN = [
|
||||||
@@ -62,6 +65,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
private const DIAGNOSE_VARIABLEN = [
|
private const DIAGNOSE_VARIABLEN = [
|
||||||
'NetzleistungGueltig',
|
'NetzleistungGueltig',
|
||||||
'WirksameLastspitzengrenze',
|
'WirksameLastspitzengrenze',
|
||||||
|
'WirksameEinspeisegrenze',
|
||||||
|
'Abregelbedarf',
|
||||||
|
'Wechselrichterstatus',
|
||||||
'Verteilbudget',
|
'Verteilbudget',
|
||||||
'VerbraucherAnzahl',
|
'VerbraucherAnzahl',
|
||||||
'VerbraucherVerfuegbar',
|
'VerbraucherVerfuegbar',
|
||||||
@@ -95,6 +101,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$this->RegisterPropertyString('Monatsgrenzen', $this->standardMonatsgrenzen());
|
$this->RegisterPropertyString('Monatsgrenzen', $this->standardMonatsgrenzen());
|
||||||
$this->RegisterPropertyFloat('SollwertSolarladen', 0.0);
|
$this->RegisterPropertyFloat('SollwertSolarladen', 0.0);
|
||||||
$this->RegisterPropertyFloat('Umschaltdifferenz', 5.0);
|
$this->RegisterPropertyFloat('Umschaltdifferenz', 5.0);
|
||||||
|
$this->RegisterPropertyBoolean('EinspeisebegrenzungAktiv', false);
|
||||||
|
$this->RegisterPropertyFloat('Einspeisegrenze', 0.0);
|
||||||
|
$this->RegisterPropertyFloat('Einspeisetoleranz', 100.0);
|
||||||
|
$this->RegisterPropertyBoolean('NetzfahrplanAktiv', false);
|
||||||
$this->RegisterPropertyString('AnlagenWechselrichter', '[]');
|
$this->RegisterPropertyString('AnlagenWechselrichter', '[]');
|
||||||
$this->RegisterPropertyString('AnlagenPVFlaechen', '[]');
|
$this->RegisterPropertyString('AnlagenPVFlaechen', '[]');
|
||||||
$this->RegisterPropertyString('AnlagenBatterien', '[]');
|
$this->RegisterPropertyString('AnlagenBatterien', '[]');
|
||||||
@@ -118,6 +128,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
|
|
||||||
$this->RegisterAttributeString('VerbraucherCache', '{}');
|
$this->RegisterAttributeString('VerbraucherCache', '{}');
|
||||||
$this->RegisterAttributeString('LetzteSollwerte', '{}');
|
$this->RegisterAttributeString('LetzteSollwerte', '{}');
|
||||||
|
$this->RegisterAttributeString('LetztePvGrenzen', '{}');
|
||||||
|
$this->RegisterAttributeString('Netzfahrplan', '{}');
|
||||||
$this->RegisterAttributeInteger('RegistrierteNetzleistungVariableID', 0);
|
$this->RegisterAttributeInteger('RegistrierteNetzleistungVariableID', 0);
|
||||||
$this->RegisterAttributeString('LizenzInstallationID', '');
|
$this->RegisterAttributeString('LizenzInstallationID', '');
|
||||||
$this->RegisterAttributeString('LizenzLease', '{}');
|
$this->RegisterAttributeString('LizenzLease', '{}');
|
||||||
@@ -208,6 +220,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$automatisch = $this->ReadPropertyBoolean('AutomatischeSuche');
|
$automatisch = $this->ReadPropertyBoolean('AutomatischeSuche');
|
||||||
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
|
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
|
||||||
$peakShavingLizenziert = $this->peakShavingLizenziert();
|
$peakShavingLizenziert = $this->peakShavingLizenziert();
|
||||||
|
$netzfahrplanLizenziert = $this->berechtigungLizenziert(
|
||||||
|
Lizenzpruefung::NETZFAHRPLAN
|
||||||
|
);
|
||||||
$monatsgrenzenSichtbar = $peakShavingLizenziert
|
$monatsgrenzenSichtbar = $peakShavingLizenziert
|
||||||
&& $lastspitzenmodus === 2
|
&& $lastspitzenmodus === 2
|
||||||
&& $this->GetBuffer('MonatsgrenzenSichtbar') === '1';
|
&& $this->GetBuffer('MonatsgrenzenSichtbar') === '1';
|
||||||
@@ -251,6 +266,20 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'options',
|
'options',
|
||||||
$lastspitzenOptionen
|
$lastspitzenOptionen
|
||||||
);
|
);
|
||||||
|
foreach (['EinspeisebegrenzungAktiv', 'Einspeisegrenze', 'Einspeisetoleranz'] as $feld) {
|
||||||
|
$this->setzeFormularParameter(
|
||||||
|
$formular['elements'],
|
||||||
|
$feld,
|
||||||
|
'enabled',
|
||||||
|
$peakShavingLizenziert
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->setzeFormularParameter(
|
||||||
|
$formular['elements'],
|
||||||
|
'NetzfahrplanAktiv',
|
||||||
|
'enabled',
|
||||||
|
$peakShavingLizenziert && $netzfahrplanLizenziert
|
||||||
|
);
|
||||||
$this->setzeFormularParameter(
|
$this->setzeFormularParameter(
|
||||||
$formular['elements'],
|
$formular['elements'],
|
||||||
'Lastspitzengrenze',
|
'Lastspitzengrenze',
|
||||||
@@ -458,6 +487,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
|
|
||||||
$aktiv = (bool) $this->GetValue('Aktiv');
|
$aktiv = (bool) $this->GetValue('Aktiv');
|
||||||
if (!$aktiv || !$netzleistungGueltig) {
|
if (!$aktiv || !$netzleistungGueltig) {
|
||||||
|
if (!$aktiv) {
|
||||||
|
try {
|
||||||
|
$this->gibEinspeisebegrenzungFrei();
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->setzeDiagnosewert(
|
||||||
|
'Wechselrichterstatus',
|
||||||
|
'Ruecksetzen fehlgeschlagen: ' . substr($fehler->getMessage(), 0, 500)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
|
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
|
||||||
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||||
$this->setzeNichtGeregelteVerbraucher(
|
$this->setzeNichtGeregelteVerbraucher(
|
||||||
@@ -499,6 +538,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$synchronisiert
|
$synchronisiert
|
||||||
);
|
);
|
||||||
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
|
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
|
||||||
|
$this->regleEinspeisung($netzleistung, $stoerungen, $erneuern);
|
||||||
if (!$synchronisiert) {
|
if (!$synchronisiert) {
|
||||||
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||||
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
||||||
@@ -516,9 +556,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fahrplanVorgabe = $this->aktuelleNetzfahrplanVorgabe();
|
||||||
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
|
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
|
||||||
? (float) $lastspitzengrenze
|
? (float) $lastspitzengrenze
|
||||||
: $this->ReadPropertyFloat('SollwertSolarladen');
|
: ($fahrplanVorgabe ?? $this->ReadPropertyFloat('SollwertSolarladen'));
|
||||||
$ergebnis = ManagerRegler::verteile(
|
$ergebnis = ManagerRegler::verteile(
|
||||||
$netzleistung,
|
$netzleistung,
|
||||||
$zielnetzleistung,
|
$zielnetzleistung,
|
||||||
@@ -556,6 +597,174 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$this->SetStatus(self::STATUS_AKTIV);
|
$this->SetStatus(self::STATUS_AKTIV);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $stoerungen
|
||||||
|
*/
|
||||||
|
private function regleEinspeisung(
|
||||||
|
float $netzleistung,
|
||||||
|
array &$stoerungen,
|
||||||
|
bool $erneuern
|
||||||
|
): void {
|
||||||
|
if (!$this->ReadPropertyBoolean('EinspeisebegrenzungAktiv')) {
|
||||||
|
try {
|
||||||
|
$this->gibEinspeisebegrenzungFrei();
|
||||||
|
$this->setzeDiagnosewert('Wechselrichterstatus', 'Nicht aktiv');
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$stoerungen[] = 'Einspeisebegrenzung zuruecksetzen: ' . $fehler->getMessage();
|
||||||
|
$this->setzeDiagnosewert(
|
||||||
|
'Wechselrichterstatus',
|
||||||
|
'Ruecksetzen fehlgeschlagen: ' . substr($fehler->getMessage(), 0, 500)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->setzeDiagnosewert('WirksameEinspeisegrenze', 0.0);
|
||||||
|
$this->setzeDiagnosewert('Abregelbedarf', 0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$wechselrichter = $this->regelbareWechselrichter();
|
||||||
|
$letzteGrenzen = $this->leseJsonAttribut('LetztePvGrenzen');
|
||||||
|
$ergebnis = EinspeiseRegler::verteile(
|
||||||
|
$netzleistung,
|
||||||
|
$this->ReadPropertyFloat('Einspeisegrenze'),
|
||||||
|
$this->ReadPropertyFloat('Einspeisetoleranz'),
|
||||||
|
$wechselrichter,
|
||||||
|
$letzteGrenzen
|
||||||
|
);
|
||||||
|
$konfiguration = [];
|
||||||
|
foreach ($wechselrichter as $eintrag) {
|
||||||
|
$konfiguration[$eintrag['ID']] = $eintrag;
|
||||||
|
}
|
||||||
|
foreach ($ergebnis['Grenzen_W'] as $id => $grenzeW) {
|
||||||
|
if (!$erneuern
|
||||||
|
&& isset($letzteGrenzen[$id])
|
||||||
|
&& abs((float) $letzteGrenzen[$id] - $grenzeW) < 0.5
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$eintrag = $konfiguration[$id];
|
||||||
|
$stellwert = EinspeiseRegler::stellwert(
|
||||||
|
$grenzeW,
|
||||||
|
(float) $eintrag['Nennleistung_W'],
|
||||||
|
$eintrag['Begrenzungsart']
|
||||||
|
);
|
||||||
|
RequestAction($eintrag['BegrenzungVariableID'], $stellwert);
|
||||||
|
}
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'LetztePvGrenzen',
|
||||||
|
json_encode($ergebnis['Grenzen_W'], JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
$this->setzeDiagnosewert(
|
||||||
|
'WirksameEinspeisegrenze',
|
||||||
|
$this->ReadPropertyFloat('Einspeisegrenze')
|
||||||
|
);
|
||||||
|
$this->setzeDiagnosewert('Abregelbedarf', $ergebnis['Abregelbedarf_W']);
|
||||||
|
$this->setzeDiagnosewert(
|
||||||
|
'Wechselrichterstatus',
|
||||||
|
json_encode($ergebnis, JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$stoerungen[] = 'Einspeisebegrenzung: ' . $fehler->getMessage();
|
||||||
|
$this->setzeDiagnosewert(
|
||||||
|
'Wechselrichterstatus',
|
||||||
|
'Fehler: ' . substr($fehler->getMessage(), 0, 500)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function gibEinspeisebegrenzungFrei(): void
|
||||||
|
{
|
||||||
|
if ($this->leseJsonAttribut('LetztePvGrenzen') === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($this->regelbareWechselrichter() as $eintrag) {
|
||||||
|
RequestAction(
|
||||||
|
$eintrag['BegrenzungVariableID'],
|
||||||
|
EinspeiseRegler::stellwert(
|
||||||
|
(float) $eintrag['Nennleistung_W'],
|
||||||
|
(float) $eintrag['Nennleistung_W'],
|
||||||
|
$eintrag['Begrenzungsart']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->WriteAttributeString('LetztePvGrenzen', '{}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{
|
||||||
|
* ID: string,
|
||||||
|
* Nennleistung_W: float,
|
||||||
|
* Istleistung_W: float,
|
||||||
|
* BegrenzungVariableID: int,
|
||||||
|
* Begrenzungsart: string
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
private function regelbareWechselrichter(): array
|
||||||
|
{
|
||||||
|
$liste = json_decode(
|
||||||
|
$this->ReadPropertyString('AnlagenWechselrichter'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
if (!is_array($liste)) {
|
||||||
|
throw new InvalidArgumentException('AnlagenWechselrichter muss eine JSON-Liste sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ergebnis = [];
|
||||||
|
foreach ($liste as $index => $eintrag) {
|
||||||
|
if (!is_array($eintrag)
|
||||||
|
|| !in_array($eintrag['Typ'] ?? null, ['pv', 'hybrid'], true)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$istID = $eintrag['IstleistungVariableID'] ?? 0;
|
||||||
|
$begrenzungID = $eintrag['BegrenzungVariableID'] ?? 0;
|
||||||
|
if ($istID === 0 && $begrenzungID === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!is_int($istID) || !is_int($begrenzungID)
|
||||||
|
|| $istID <= 0 || $begrenzungID <= 0
|
||||||
|
|| !IPS_VariableExists($istID) || !IPS_VariableExists($begrenzungID)
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Wechselrichter ' . ($index + 1) . ': Istleistung und Begrenzungsregister fehlen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$begrenzungsvariable = IPS_GetVariable($begrenzungID);
|
||||||
|
if ((int) ($begrenzungsvariable['VariableAction'] ?? 0) <= 0
|
||||||
|
&& (int) ($begrenzungsvariable['VariableCustomAction'] ?? 0) <= 0
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Wechselrichter ' . ($index + 1) . ': Begrenzungsregister hat keine Aktion.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$istleistung = GetValue($istID);
|
||||||
|
$nennleistung = $eintrag['ACNennleistung_kW'] ?? null;
|
||||||
|
$art = $eintrag['Begrenzungsart'] ?? 'percent';
|
||||||
|
if ((!is_int($istleistung) && !is_float($istleistung))
|
||||||
|
|| (!is_int($nennleistung) && !is_float($nennleistung))
|
||||||
|
|| !in_array($art, ['percent', 'watt'], true)
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Wechselrichter ' . ($index + 1) . ': Regelparameter sind ungueltig.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$ergebnis[] = [
|
||||||
|
'ID' => (string) ($eintrag['ID'] ?? ''),
|
||||||
|
'Nennleistung_W' => (float) $nennleistung * 1000.0,
|
||||||
|
'Istleistung_W' => max(0.0, (float) $istleistung),
|
||||||
|
'BegrenzungVariableID' => $begrenzungID,
|
||||||
|
'Begrenzungsart' => $art,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($ergebnis === []) {
|
||||||
|
throw new InvalidArgumentException('Kein PV-Wechselrichter mit Stellregister konfiguriert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ergebnis;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array{0: float, 1: bool} */
|
/** @return array{0: float, 1: bool} */
|
||||||
private function leseNetzleistung(): array
|
private function leseNetzleistung(): array
|
||||||
{
|
{
|
||||||
@@ -1034,6 +1243,19 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'',
|
'',
|
||||||
50
|
50
|
||||||
);
|
);
|
||||||
|
$this->RegisterVariableFloat(
|
||||||
|
'WirksameEinspeisegrenze',
|
||||||
|
'Wirksame Einspeisegrenze',
|
||||||
|
'',
|
||||||
|
52
|
||||||
|
);
|
||||||
|
$this->RegisterVariableFloat('Abregelbedarf', 'Abregelbedarf', '', 54);
|
||||||
|
$this->RegisterVariableString(
|
||||||
|
'Wechselrichterstatus',
|
||||||
|
'Wechselrichterstatus',
|
||||||
|
'',
|
||||||
|
56
|
||||||
|
);
|
||||||
$this->RegisterVariableFloat('Verteilbudget', 'Verteilbudget', '', 60);
|
$this->RegisterVariableFloat('Verteilbudget', 'Verteilbudget', '', 60);
|
||||||
$this->RegisterVariableInteger('VerbraucherAnzahl', 'Verbraucher Anzahl', '', 70);
|
$this->RegisterVariableInteger('VerbraucherAnzahl', 'Verbraucher Anzahl', '', 70);
|
||||||
$this->RegisterVariableInteger(
|
$this->RegisterVariableInteger(
|
||||||
@@ -1114,20 +1336,23 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
$lease = $this->leseLizenzLease();
|
$lease = $this->leseLizenzLease();
|
||||||
$codePasst = $gespeicherterHash !== '' && hash_equals($gespeicherterHash, $codeHash);
|
$codePasst = $gespeicherterHash !== '' && hash_equals($gespeicherterHash, $codeHash);
|
||||||
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
|
$lastspitzenmodus = $this->lizenzPeakModus();
|
||||||
$basisGueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
|
$basisGueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
|
||||||
$lease,
|
$lease,
|
||||||
$installationID,
|
$installationID,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
$gueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
|
$leaseGueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
|
||||||
$lease,
|
$lease,
|
||||||
$installationID,
|
$installationID,
|
||||||
$lastspitzenmodus
|
$lastspitzenmodus
|
||||||
);
|
);
|
||||||
$berechtigungenVeraltet = $basisGueltig
|
$berechtigungenPassen = $leaseGueltig
|
||||||
&& !$this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
|
&& $this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
|
||||||
$geraetezugangFehlt = $this->hatAnlagentopologie()
|
$gueltig = $leaseGueltig && $berechtigungenPassen;
|
||||||
|
$berechtigungenVeraltet = $basisGueltig && !$berechtigungenPassen;
|
||||||
|
$geraetezugangFehlt = ($this->hatAnlagentopologie()
|
||||||
|
|| $this->prognoseTelemetrieKonfiguriert())
|
||||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
||||||
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|
||||||
|| $geraetezugangFehlt
|
|| $geraetezugangFehlt
|
||||||
@@ -1183,8 +1408,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
Lizenzpruefung::pruefeLease(
|
Lizenzpruefung::pruefeLease(
|
||||||
$neueLease,
|
$neueLease,
|
||||||
$installationID,
|
$installationID,
|
||||||
$this->ReadPropertyInteger('Lastspitzenmodus')
|
$this->lizenzPeakModus()
|
||||||
);
|
);
|
||||||
|
if (!$this->lizenzBerechtigungenPassen(
|
||||||
|
$neueLease,
|
||||||
|
$this->lizenzPeakModus()
|
||||||
|
)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Die Lizenz deckt die konfigurierten Funktionen oder Geraetemengen nicht ab.'
|
||||||
|
);
|
||||||
|
}
|
||||||
if ($geraetezugangFehlt) {
|
if ($geraetezugangFehlt) {
|
||||||
$this->WriteAttributeString('PrognoseInstallationsToken', $geraeteToken);
|
$this->WriteAttributeString('PrognoseInstallationsToken', $geraeteToken);
|
||||||
}
|
}
|
||||||
@@ -1230,7 +1463,22 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function lizenzPeakModus(): int
|
||||||
|
{
|
||||||
|
$modus = $this->ReadPropertyInteger('Lastspitzenmodus');
|
||||||
|
if ($modus !== 0) {
|
||||||
|
return $modus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->ReadPropertyBoolean('EinspeisebegrenzungAktiv') ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
private function peakShavingLizenziert(): bool
|
private function peakShavingLizenziert(): bool
|
||||||
|
{
|
||||||
|
return $this->berechtigungLizenziert(Lizenzpruefung::MANAGER_PEAK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function berechtigungLizenziert(string $kennung): bool
|
||||||
{
|
{
|
||||||
$lizenzcode = Lizenzpruefung::normalisiereCode(
|
$lizenzcode = Lizenzpruefung::normalisiereCode(
|
||||||
$this->ReadPropertyString('Lizenzcode')
|
$this->ReadPropertyString('Lizenzcode')
|
||||||
@@ -1255,10 +1503,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Lizenzpruefung::berechtigungsMenge(
|
return Lizenzpruefung::berechtigungsMenge($lease, $kennung) > 0;
|
||||||
$lease,
|
|
||||||
Lizenzpruefung::MANAGER_PEAK
|
|
||||||
) > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1266,7 +1511,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
*/
|
*/
|
||||||
private function lizenzBerechtigungenPassen(array $lease, int $lastspitzenmodus): bool
|
private function lizenzBerechtigungenPassen(array $lease, int $lastspitzenmodus): bool
|
||||||
{
|
{
|
||||||
if ($lastspitzenmodus !== 0
|
if (($lastspitzenmodus !== 0 || $this->ReadPropertyBoolean('EinspeisebegrenzungAktiv'))
|
||||||
&& Lizenzpruefung::berechtigungsMenge(
|
&& Lizenzpruefung::berechtigungsMenge(
|
||||||
$lease,
|
$lease,
|
||||||
Lizenzpruefung::MANAGER_PEAK
|
Lizenzpruefung::MANAGER_PEAK
|
||||||
@@ -1274,6 +1519,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if ($this->ReadPropertyBoolean('NetzfahrplanAktiv')
|
||||||
|
&& Lizenzpruefung::berechtigungsMenge(
|
||||||
|
$lease,
|
||||||
|
Lizenzpruefung::NETZFAHRPLAN
|
||||||
|
) < 1
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[$mengen] = $this->verbraucherLizenzbelegung(
|
[$mengen] = $this->verbraucherLizenzbelegung(
|
||||||
@@ -1356,7 +1609,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
Lizenzpruefung::pruefeLease(
|
Lizenzpruefung::pruefeLease(
|
||||||
$lease,
|
$lease,
|
||||||
$this->ReadAttributeString('LizenzInstallationID'),
|
$this->ReadAttributeString('LizenzInstallationID'),
|
||||||
$this->ReadPropertyInteger('Lastspitzenmodus')
|
$this->lizenzPeakModus()
|
||||||
);
|
);
|
||||||
$this->aktiveVerbraucherIDs();
|
$this->aktiveVerbraucherIDs();
|
||||||
$status = ($lease['development'] ?? false) === true
|
$status = ($lease['development'] ?? false) === true
|
||||||
@@ -1498,6 +1751,22 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
) {
|
) {
|
||||||
throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 liegen.');
|
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->ReadPropertyBoolean('EinspeisebegrenzungAktiv')) {
|
||||||
|
$this->regelbareWechselrichter();
|
||||||
|
}
|
||||||
|
if ($this->ReadPropertyBoolean('NetzfahrplanAktiv')
|
||||||
|
&& (!$this->ReadPropertyBoolean('EinspeisebegrenzungAktiv')
|
||||||
|
|| !$this->prognoseTelemetrieKonfiguriert())
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Der intelligente Netzfahrplan benoetigt Einspeisebegrenzung und Prognosetelemetrie.'
|
||||||
|
);
|
||||||
|
}
|
||||||
$this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
|
$this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
|
||||||
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
|
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
|
||||||
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
|
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
|
||||||
@@ -1537,6 +1806,48 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
&& $this->ReadPropertyInteger('PrognoseSOCVariableID') > 0;
|
&& $this->ReadPropertyInteger('PrognoseSOCVariableID') > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function aktuelleNetzfahrplanVorgabe(): ?float
|
||||||
|
{
|
||||||
|
if (!$this->ReadPropertyBoolean('NetzfahrplanAktiv')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$fahrplan = $this->leseJsonAttribut('Netzfahrplan');
|
||||||
|
if (($fahrplan['status'] ?? null) !== 'available'
|
||||||
|
|| !is_array($fahrplan['points'] ?? null)
|
||||||
|
|| !is_string($fahrplan['validUntil'] ?? null)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$gueltigBis = strtotime($fahrplan['validUntil']);
|
||||||
|
if ($gueltigBis === false || $gueltigBis <= time()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jetzt = time();
|
||||||
|
$naechsterAbstand = PHP_INT_MAX;
|
||||||
|
$vorgabe = null;
|
||||||
|
foreach ($fahrplan['points'] as $punkt) {
|
||||||
|
if (!is_array($punkt)
|
||||||
|
|| !is_string($punkt['time'] ?? null)
|
||||||
|
|| (!is_int($punkt['gridTargetW'] ?? null)
|
||||||
|
&& !is_float($punkt['gridTargetW'] ?? null))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$zeitpunkt = strtotime($punkt['time']);
|
||||||
|
if ($zeitpunkt === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$abstand = abs($zeitpunkt - $jetzt);
|
||||||
|
if ($abstand < $naechsterAbstand) {
|
||||||
|
$naechsterAbstand = $abstand;
|
||||||
|
$vorgabe = (float) $punkt['gridTargetW'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $naechsterAbstand <= 1800 ? $vorgabe : null;
|
||||||
|
}
|
||||||
|
|
||||||
private function synchronisiereAnlagentopologie(): void
|
private function synchronisiereAnlagentopologie(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -1667,6 +1978,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'values' => $werte,
|
'values' => $werte,
|
||||||
];
|
];
|
||||||
$this->uebertragePrognoseTelemetrie($installationID, $nutzlast, $token);
|
$this->uebertragePrognoseTelemetrie($installationID, $nutzlast, $token);
|
||||||
|
if ($this->ReadPropertyBoolean('NetzfahrplanAktiv')) {
|
||||||
|
$this->aktualisiereNetzfahrplan($installationID, $token);
|
||||||
|
}
|
||||||
$this->setzeDiagnosewert('Prognosestatus', 'Verbunden');
|
$this->setzeDiagnosewert('Prognosestatus', 'Verbunden');
|
||||||
} catch (Throwable $fehler) {
|
} catch (Throwable $fehler) {
|
||||||
$meldung = substr($fehler->getMessage(), 0, 500);
|
$meldung = substr($fehler->getMessage(), 0, 500);
|
||||||
@@ -1675,6 +1989,73 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function aktualisiereNetzfahrplan(string $installationID, string $token): void
|
||||||
|
{
|
||||||
|
$url = sprintf(
|
||||||
|
self::PROGNOSE_FAHRPLAN_ENDPOINT,
|
||||||
|
rawurlencode($installationID)
|
||||||
|
) . '?' . http_build_query([
|
||||||
|
'exportLimitW' => $this->ReadPropertyFloat('Einspeisegrenze'),
|
||||||
|
]);
|
||||||
|
$curl = curl_init($url);
|
||||||
|
if ($curl === false) {
|
||||||
|
throw new RuntimeException('Der Netzfahrplan konnte nicht vorbereitet werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: application/json',
|
||||||
|
'Authorization: Bearer ' . $token,
|
||||||
|
],
|
||||||
|
CURLOPT_USERAGENT => 'Enelix-EMS-Manager/0.2',
|
||||||
|
]);
|
||||||
|
$antwort = curl_exec($curl);
|
||||||
|
if ($antwort === false) {
|
||||||
|
throw new RuntimeException('Netzfahrplan nicht erreichbar: ' . curl_error($curl));
|
||||||
|
}
|
||||||
|
if (strlen($antwort) > 524288) {
|
||||||
|
throw new RuntimeException('Die Antwort des Netzfahrplans ist zu gross.');
|
||||||
|
}
|
||||||
|
$httpStatus = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
} finally {
|
||||||
|
curl_close($curl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpStatus === 401) {
|
||||||
|
$this->WriteAttributeString('PrognoseInstallationsToken', '');
|
||||||
|
}
|
||||||
|
if ($httpStatus !== 200) {
|
||||||
|
$meldung = 'Unbekannter Fehler';
|
||||||
|
try {
|
||||||
|
$fehlerdaten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (is_array($fehlerdaten) && is_string($fehlerdaten['error'] ?? null)) {
|
||||||
|
$meldung = $fehlerdaten['error'];
|
||||||
|
}
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
// Der HTTP-Status bleibt die fuehrende Fehlerinformation.
|
||||||
|
}
|
||||||
|
throw new RuntimeException(
|
||||||
|
'Netzfahrplan HTTP ' . $httpStatus . ': ' . $meldung
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fahrplan = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($fahrplan)
|
||||||
|
|| ($fahrplan['schemaVersion'] ?? null) !== 1
|
||||||
|
|| !is_array($fahrplan['points'] ?? null)
|
||||||
|
) {
|
||||||
|
throw new RuntimeException('Der Netzfahrplan hat ein ungueltiges Format.');
|
||||||
|
}
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'Netzfahrplan',
|
||||||
|
json_encode($fahrplan, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $nutzlast */
|
/** @param array<string, mixed> $nutzlast */
|
||||||
private function uebertragePrognoseTelemetrie(
|
private function uebertragePrognoseTelemetrie(
|
||||||
string $installationID,
|
string $installationID,
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netz
|
|||||||
| `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. |
|
||||||
| `NetzleistungGueltig` | Boolean / Logging | Messquelle vorhanden und aktuell. |
|
| `NetzleistungGueltig` | Boolean / Logging | Messquelle vorhanden und aktuell. |
|
||||||
| `WirksameLastspitzengrenze` | Float / Logging | Aktuelle feste oder monatliche Grenze in W. |
|
| `WirksameLastspitzengrenze` | Float / Logging | Aktuelle feste oder monatliche Bezugsgrenze in W. |
|
||||||
|
| `WirksameEinspeisegrenze` | Float / Logging | Anlagenweite Einspeisegrenze in W. |
|
||||||
|
| `Abregelbedarf` | Float / Logging | Aktuell erforderliche PV-Leistungsreduktion in W. |
|
||||||
|
| `Wechselrichterstatus` | String/JSON / Logging | Verteilte Grenzen und Zustand der PV-Regelung. |
|
||||||
| `Verteilbudget` | Float / Logging | Aktuell verfügbares Budget in W. |
|
| `Verteilbudget` | Float / Logging | Aktuell verfügbares Budget in W. |
|
||||||
| `VerbraucherAnzahl` | Integer / Logging | Anzahl zugeordneter Verbraucher. |
|
| `VerbraucherAnzahl` | Integer / Logging | Anzahl zugeordneter Verbraucher. |
|
||||||
| `VerbraucherVerfuegbar` | Integer / Logging | Anzahl aktuell verfügbarer Verbraucher. |
|
| `VerbraucherVerfuegbar` | Integer / Logging | Anzahl aktuell verfügbarer Verbraucher. |
|
||||||
@@ -47,7 +50,11 @@ Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netz
|
|||||||
| `Monatsgrenzen` | String/JSON | Editierbare Liste mit zwölf Monatswerten in W. |
|
| `Monatsgrenzen` | String/JSON | Editierbare Liste mit zwölf Monatswerten in W. |
|
||||||
| `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. |
|
| `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. |
|
||||||
| `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
|
| `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
|
||||||
| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ und AC-Grenzen. |
|
| `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. |
|
||||||
|
| `NetzfahrplanAktiv` | Boolean | `false`; lizenzierten, tarif- und prognosebasierten Netzzielwert verwenden. |
|
||||||
|
| `AnlagenWechselrichter` | String/JSON | `[]`; Wechselrichter mit Typ, AC-Grenzen sowie optionalen Istleistungs- und Begrenzungsregistern. |
|
||||||
| `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, Leistungen, Kopplung und Wechselrichter-ID. |
|
||||||
| `PrognosePVVariableID` | Integer | `0`; PV-Leistung in W fuer die Prognosetelemetrie. |
|
| `PrognosePVVariableID` | Integer | `0`; PV-Leistung in W fuer die Prognosetelemetrie. |
|
||||||
@@ -67,6 +74,33 @@ 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.
|
||||||
|
|
||||||
|
## Anlagenweite Einspeisebegrenzung
|
||||||
|
|
||||||
|
Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der
|
||||||
|
Messung am Netzanschlusspunkt. Fuer jeden regelbaren PV- oder Hybridwechselrichter
|
||||||
|
werden die Istleistungsvariable und eine bedienbare Begrenzungsvariable
|
||||||
|
konfiguriert. Der Stellwert kann als Prozent der AC-Nennleistung oder als
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
Der intelligente Netzfahrplan verwendet die PV-, Verbrauchs- und
|
||||||
|
Netzleistungsprognose sowie Tarife und Batteriespeicher. Liegt im
|
||||||
|
Prognosezeitraum mehr PV-Ertrag als zulaessige Einspeisung vor, wird dieses
|
||||||
|
Potenzial als flexibler Verbrauch beziehungsweise Speicherladung eingeplant.
|
||||||
|
Damit werden Verbraucher in ertragreiche Zeitfenster verschoben, bevor der
|
||||||
|
harte Anlagenregler die Wechselrichter reduziert. Ein fehlender oder
|
||||||
|
abgelaufener Fahrplan fuehrt automatisch zum konfigurierten
|
||||||
|
`SollwertSolarladen` zurueck; die harte Einspeisebegrenzung bleibt unabhaengig
|
||||||
|
davon aktiv.
|
||||||
|
|
||||||
## Anlagentopologie und Prognoseexport
|
## Anlagentopologie und Prognoseexport
|
||||||
|
|
||||||
Der Manager ist die fuehrende Quelle fuer die technischen Stammdaten. Die
|
Der Manager ist die fuehrende Quelle fuer die technischen Stammdaten. Die
|
||||||
@@ -137,8 +171,11 @@ wird vom Lizenzserver abgelehnt.
|
|||||||
|
|
||||||
| Berechtigung | Freigegebene Funktion |
|
| Berechtigung | Freigegebene Funktion |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `manager_standard` | Manager-Grundregelung ohne Lastspitzenmodus. |
|
| `manager_standard` | Manager-Grundregelung ohne Peak Shaving. |
|
||||||
| `manager_peak` | Manager-Grundregelung einschliesslich konstantem oder monatlichem Lastspitzenmodus. |
|
| `manager_peak` | Bezugs- und Einspeisebegrenzung am Netzanschlusspunkt. |
|
||||||
|
| `forecast_pv` | PV-Ertragsprognose; interne Modellvarianten bleiben verborgen. |
|
||||||
|
| `forecast_load` | Verbrauchsprognose; interne Modellvarianten bleiben verborgen. |
|
||||||
|
| `grid_schedule` | Intelligenter Netzfahrplan inklusive PV- und Verbrauchsprognose sowie Vermeidung von Abregelung. |
|
||||||
|
|
||||||
Wird mit `manager_standard` ein Lastspitzenmodus aktiviert, bleibt der Manager
|
Wird mit `manager_standard` ein Lastspitzenmodus aktiviert, bleibt der Manager
|
||||||
mit dem Hinweis `Peak Shaving ist nicht lizenziert` gesperrt. `manager_peak`
|
mit dem Hinweis `Peak Shaving ist nicht lizenziert` gesperrt. `manager_peak`
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verteilt eine gemeinsame Einspeisegrenze auf regelbare PV-Wechselrichter.
|
||||||
|
*
|
||||||
|
* Netzleistung: positiv Bezug, negativ Einspeisung.
|
||||||
|
*/
|
||||||
|
final class EinspeiseRegler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param list<array{ID: string, Nennleistung_W: float|int, Istleistung_W: float|int}> $wechselrichter
|
||||||
|
* @param array<string, float|int> $letzteGrenzen
|
||||||
|
* @return array{
|
||||||
|
* Netzabweichung_W: float,
|
||||||
|
* ZielGesamtleistung_W: float,
|
||||||
|
* Abregelbedarf_W: float,
|
||||||
|
* Grenzen_W: array<string, float>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nennleistungGesamt = 0.0;
|
||||||
|
$istleistungGesamt = 0.0;
|
||||||
|
$bisherigeGrenzeGesamt = 0.0;
|
||||||
|
$normalisiert = [];
|
||||||
|
$kennungen = [];
|
||||||
|
|
||||||
|
foreach ($wechselrichter as $eintrag) {
|
||||||
|
$id = $eintrag['ID'] ?? null;
|
||||||
|
$nennleistung = $eintrag['Nennleistung_W'] ?? null;
|
||||||
|
$istleistung = $eintrag['Istleistung_W'] ?? null;
|
||||||
|
if (!is_string($id) || trim($id) === '' || isset($kennungen[$id])) {
|
||||||
|
throw new InvalidArgumentException('Wechselrichter-ID fehlt oder ist mehrfach vorhanden.');
|
||||||
|
}
|
||||||
|
if (!is_numeric($nennleistung) || (float) $nennleistung <= 0.0) {
|
||||||
|
throw new InvalidArgumentException('Wechselrichter-Nennleistung muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
if (!is_numeric($istleistung) || (float) $istleistung < 0.0) {
|
||||||
|
throw new InvalidArgumentException('Wechselrichter-Istleistung darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nennleistung = (float) $nennleistung;
|
||||||
|
$istleistung = min((float) $istleistung, $nennleistung);
|
||||||
|
$bisherigeGrenze = $letzteGrenzen[$id] ?? $nennleistung;
|
||||||
|
if (!is_numeric($bisherigeGrenze)) {
|
||||||
|
throw new InvalidArgumentException('Gespeicherte Wechselrichtergrenze ist ungueltig.');
|
||||||
|
}
|
||||||
|
$bisherigeGrenze = max(0.0, min((float) $bisherigeGrenze, $nennleistung));
|
||||||
|
$normalisiert[] = [
|
||||||
|
'ID' => $id,
|
||||||
|
'Nennleistung_W' => $nennleistung,
|
||||||
|
'Istleistung_W' => $istleistung,
|
||||||
|
];
|
||||||
|
$kennungen[$id] = true;
|
||||||
|
$nennleistungGesamt += $nennleistung;
|
||||||
|
$istleistungGesamt += $istleistung;
|
||||||
|
$bisherigeGrenzeGesamt += $bisherigeGrenze;
|
||||||
|
}
|
||||||
|
|
||||||
|
$netzabweichung = $netzleistung + $einspeisegrenze;
|
||||||
|
$zielGesamt = $bisherigeGrenzeGesamt;
|
||||||
|
$nachIstleistungVerteilen = false;
|
||||||
|
if ($netzabweichung < -$toleranz) {
|
||||||
|
$nachIstleistungVerteilen = $istleistungGesamt > 0.0;
|
||||||
|
$wirksameAbweichung = $netzabweichung + $toleranz;
|
||||||
|
$zielGesamt = min($bisherigeGrenzeGesamt, $istleistungGesamt) + $wirksameAbweichung;
|
||||||
|
} elseif ($netzabweichung > $toleranz) {
|
||||||
|
$zielGesamt = $bisherigeGrenzeGesamt + $netzabweichung - $toleranz;
|
||||||
|
}
|
||||||
|
$zielGesamt = max(0.0, min($zielGesamt, $nennleistungGesamt));
|
||||||
|
|
||||||
|
$grenzen = [];
|
||||||
|
foreach ($normalisiert as $eintrag) {
|
||||||
|
$anteil = $nachIstleistungVerteilen
|
||||||
|
? $eintrag['Istleistung_W'] / $istleistungGesamt
|
||||||
|
: $eintrag['Nennleistung_W'] / $nennleistungGesamt;
|
||||||
|
$grenzen[$eintrag['ID']] = round(
|
||||||
|
min($eintrag['Nennleistung_W'], $zielGesamt * $anteil),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Netzabweichung_W' => $netzabweichung,
|
||||||
|
'ZielGesamtleistung_W' => round($zielGesamt, 3),
|
||||||
|
'Abregelbedarf_W' => round(max(0.0, $istleistungGesamt - $zielGesamt), 3),
|
||||||
|
'Grenzen_W' => $grenzen,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stellwert(float $grenzeW, float $nennleistungW, string $art): float
|
||||||
|
{
|
||||||
|
if ($nennleistungW <= 0.0 || $grenzeW < 0.0) {
|
||||||
|
throw new InvalidArgumentException('Leistungsgrenzen sind ungueltig.');
|
||||||
|
}
|
||||||
|
$grenzeW = min($grenzeW, $nennleistungW);
|
||||||
|
if ($art === 'percent') {
|
||||||
|
return round($grenzeW / $nennleistungW * 100.0, 2);
|
||||||
|
}
|
||||||
|
if ($art === 'watt') {
|
||||||
|
return round($grenzeW, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Begrenzungsart muss percent oder watt sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ final class Lizenzpruefung
|
|||||||
{
|
{
|
||||||
public const MANAGER_STANDARD = 'manager_standard';
|
public const MANAGER_STANDARD = 'manager_standard';
|
||||||
public const MANAGER_PEAK = 'manager_peak';
|
public const MANAGER_PEAK = 'manager_peak';
|
||||||
|
public const PROGNOSE_PV = 'forecast_pv';
|
||||||
|
public const PROGNOSE_VERBRAUCH = 'forecast_load';
|
||||||
|
public const NETZFAHRPLAN = 'grid_schedule';
|
||||||
|
|
||||||
public static function normalisiereCode(string $code): string
|
public static function normalisiereCode(string $code): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\EinspeiseRegler;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class EinspeiseReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testUeberschreitungWirdAufDieGesamtanlageBegrenzt(): void
|
||||||
|
{
|
||||||
|
$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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAbregelungFolgtDerTatsaechlichenErzeugung(): void
|
||||||
|
{
|
||||||
|
$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],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(5000.0, $ergebnis['Grenzen_W']['wr-1']);
|
||||||
|
self::assertSame(0.0, $ergebnis['Grenzen_W']['wr-2']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFreierNetzspielraumHebtEineBestehendeBegrenzungAn(): void
|
||||||
|
{
|
||||||
|
$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],
|
||||||
|
],
|
||||||
|
['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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToleranzVerhindertPendelnAmGrenzwert(): 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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStellwerteWerdenInProzentOderWattAusgegeben(): void
|
||||||
|
{
|
||||||
|
self::assertSame(62.5, EinspeiseRegler::stellwert(5000.0, 8000.0, 'percent'));
|
||||||
|
self::assertSame(5000.0, EinspeiseRegler::stellwert(5000.0, 8000.0, 'watt'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUngueltigeBegrenzungsartWirdAbgelehnt(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
EinspeiseRegler::stellwert(5000.0, 8000.0, 'volt');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,13 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
'VerbraucherTimeout',
|
'VerbraucherTimeout',
|
||||||
'MesswertMaxAlter',
|
'MesswertMaxAlter',
|
||||||
'Umschaltdifferenz',
|
'Umschaltdifferenz',
|
||||||
|
'EinspeisebegrenzungAktiv',
|
||||||
|
'Einspeisegrenze',
|
||||||
|
'Einspeisetoleranz',
|
||||||
|
'NetzfahrplanAktiv',
|
||||||
|
'IstleistungVariableID',
|
||||||
|
'BegrenzungVariableID',
|
||||||
|
'Begrenzungsart',
|
||||||
'DiagnosevariablenAnzeigen',
|
'DiagnosevariablenAnzeigen',
|
||||||
'LoggingEin',
|
'LoggingEin',
|
||||||
'Lizenzcode',
|
'Lizenzcode',
|
||||||
@@ -230,6 +237,24 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testManagerRegeltDieAnlagenweiteEinspeisegrenzeUndDenNetzfahrplan(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
self::assertFileExists(__DIR__ . '/../libs/EinspeiseRegler.php');
|
||||||
|
self::assertStringContainsString('EinspeiseRegler::verteile(', $inhalt);
|
||||||
|
self::assertStringContainsString('EinspeiseRegler::stellwert(', $inhalt);
|
||||||
|
self::assertStringContainsString("RegisterAttributeString('LetztePvGrenzen'", $inhalt);
|
||||||
|
self::assertStringContainsString("RegisterAttributeString('Netzfahrplan'", $inhalt);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'https://license.enelix.ch/api/v1/installations/%s/prognosis/schedule',
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('Lizenzpruefung::NETZFAHRPLAN', $inhalt);
|
||||||
|
self::assertStringContainsString('aktuelleNetzfahrplanVorgabe()', $inhalt);
|
||||||
|
self::assertStringContainsString('gibEinspeisebegrenzungFrei()', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
public function testManagerVerwendetDieNeuenZeitdefaults(): void
|
public function testManagerVerwendetDieNeuenZeitdefaults(): void
|
||||||
{
|
{
|
||||||
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ return [
|
|||||||
'file' => __DIR__ . '/modules/Manager.php',
|
'file' => __DIR__ . '/modules/Manager.php',
|
||||||
'paths' => [
|
'paths' => [
|
||||||
'Manager/*',
|
'Manager/*',
|
||||||
|
'libs/EinspeiseRegler.php',
|
||||||
'libs/Lizenzpruefung.php',
|
'libs/Lizenzpruefung.php',
|
||||||
'libs/ManagerRegler.php',
|
'libs/ManagerRegler.php',
|
||||||
'libs/ManagerSchnittstelle.php',
|
'libs/ManagerSchnittstelle.php',
|
||||||
|
|||||||
Reference in New Issue
Block a user