Compare commits
4
Commits
7845c66fd8
...
693beb2263
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
693beb2263 | ||
|
|
b1cb289922 | ||
|
|
98ee569178 | ||
|
|
2392a9c209 |
+8
-1
@@ -26,6 +26,13 @@
|
||||
"name": "LizenzPruefen",
|
||||
"caption": "Lizenz pruefen und binden",
|
||||
"onClick": "IPS_RequestAction($id, 'FormLizenzPruefen', $Lizenzcode);"
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"name": "LizenzVerwalten",
|
||||
"caption": "Lizenz verwalten",
|
||||
"link": true,
|
||||
"onClick": "echo 'https://license.enelix.ch';"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -296,6 +303,6 @@
|
||||
{"code": 102, "icon": "active", "caption": "Aktiv"},
|
||||
{"code": 201, "icon": "inactive", "caption": "Netzleistungsmessung fehlt oder ist veraltet"},
|
||||
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"},
|
||||
{"code": 203, "icon": "error", "caption": "Lizenz fehlt oder ist abgelaufen"}
|
||||
{"code": 203, "icon": "error", "caption": "Lizenz nicht freigegeben oder Verbraucherkontingent ueberschritten"}
|
||||
]
|
||||
}
|
||||
|
||||
+437
-29
@@ -21,14 +21,35 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
private const LIZENZ_ENDPOINT = 'https://license.enelix.ch/api/v1/licenses/activate';
|
||||
private const LIZENZ_RETRY_SEKUNDEN = 3600;
|
||||
private const VM_UPDATE = 10603;
|
||||
private const VERBRAUCHER_MODUL_IDS = [
|
||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
||||
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||
'{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}',
|
||||
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}',
|
||||
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
|
||||
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
|
||||
private const VERBRAUCHER_LIZENZEN = [
|
||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
|
||||
'Katalog' => 'boiler_multi',
|
||||
'Name' => 'Boiler mehrstufig',
|
||||
],
|
||||
'{15879A4E-D0C2-4495-83DE-46E1E462591E}' => [
|
||||
'Katalog' => 'consumer_single',
|
||||
'Name' => 'Verbraucher einstufig',
|
||||
],
|
||||
'{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}' => [
|
||||
'Katalog' => 'consumer_single',
|
||||
'Name' => 'Waermepumpe',
|
||||
],
|
||||
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}' => [
|
||||
'Katalog' => 'ev_charger',
|
||||
'Name' => 'EV-Ladestation Stand-Alone',
|
||||
],
|
||||
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}' => [
|
||||
'Katalog' => 'ev_charger',
|
||||
'Name' => 'EV-Ladestation Gateway',
|
||||
],
|
||||
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}' => [
|
||||
'Katalog' => 'buffer_storage',
|
||||
'Name' => 'Pufferspeicher',
|
||||
],
|
||||
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}' => [
|
||||
'Katalog' => 'battery',
|
||||
'Name' => 'Batteriespeicher',
|
||||
],
|
||||
];
|
||||
private const MONATSNAMEN = [
|
||||
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
||||
@@ -41,6 +62,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
'VerbraucherAnzahl',
|
||||
'VerbraucherVerfuegbar',
|
||||
'Verbraucherstatus',
|
||||
'NichtGeregelteGeraete',
|
||||
'Monatsgrenzen',
|
||||
'Prognosestatus',
|
||||
'SDLStatus',
|
||||
@@ -90,6 +112,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterAttributeString('LizenzLease', '{}');
|
||||
$this->RegisterAttributeString('LizenzcodeHash', '');
|
||||
$this->RegisterAttributeInteger('LizenzNaechsterVersuch', 0);
|
||||
$this->RegisterAttributeInteger('LizenzBerechtigungenNaechsterVersuch', 0);
|
||||
$this->RegisterAttributeString('LizenzLetzterFehler', '');
|
||||
if ($this->ReadAttributeString('LizenzInstallationID') === '') {
|
||||
$this->WriteAttributeString('LizenzInstallationID', $this->erzeugeUuidV4());
|
||||
@@ -133,6 +156,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->aktiveVerbraucherIDs();
|
||||
} catch (Throwable $fehler) {
|
||||
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
|
||||
$this->setzeDiagnosewert('Lizenzstatus', 'Nicht freigegeben: ' . $fehler->getMessage());
|
||||
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
|
||||
return;
|
||||
}
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
}
|
||||
|
||||
@@ -146,7 +177,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
);
|
||||
$automatisch = $this->ReadPropertyBoolean('AutomatischeSuche');
|
||||
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
|
||||
$monatsgrenzenSichtbar = $lastspitzenmodus === 2
|
||||
$peakShavingLizenziert = $this->peakShavingLizenziert();
|
||||
$monatsgrenzenSichtbar = $peakShavingLizenziert
|
||||
&& $lastspitzenmodus === 2
|
||||
&& $this->GetBuffer('MonatsgrenzenSichtbar') === '1';
|
||||
|
||||
$this->setzeFormularParameter(
|
||||
@@ -181,17 +214,24 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
'values',
|
||||
$this->automatischeVerbraucherFuerFormular()
|
||||
);
|
||||
$lastspitzenOptionen = $this->lastspitzenOptionen($peakShavingLizenziert);
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
'Lastspitzenmodus',
|
||||
'options',
|
||||
$lastspitzenOptionen
|
||||
);
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
'Lastspitzengrenze',
|
||||
'visible',
|
||||
$lastspitzenmodus === 1
|
||||
$peakShavingLizenziert && $lastspitzenmodus === 1
|
||||
);
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
'MonatsgrenzenUmschalten',
|
||||
'visible',
|
||||
$lastspitzenmodus === 2
|
||||
$peakShavingLizenziert && $lastspitzenmodus === 2
|
||||
);
|
||||
$this->setzeFormularParameter(
|
||||
$formular['elements'],
|
||||
@@ -242,6 +282,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
$this->aktualisiereLizenzfreigabe($wert, true);
|
||||
$this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert));
|
||||
$this->aktualisiereLastspitzenformular();
|
||||
return;
|
||||
|
||||
case 'FormAutomatischeSuche':
|
||||
@@ -277,6 +318,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
case 'FormLastspitzenmodus':
|
||||
$modus = (int) $wert;
|
||||
if ($modus !== 0 && !$this->peakShavingLizenziert()) {
|
||||
$this->UpdateFormField('Lastspitzenmodus', 'value', 0);
|
||||
$this->UpdateFormField('Lastspitzengrenze', 'visible', false);
|
||||
$this->UpdateFormField('MonatsgrenzenUmschalten', 'visible', false);
|
||||
throw new InvalidArgumentException(
|
||||
'Konstant und Monatlich benoetigen eine Peak-Shaving-Lizenz.'
|
||||
);
|
||||
}
|
||||
$this->UpdateFormField('Lastspitzengrenze', 'visible', $modus === 1);
|
||||
$this->UpdateFormField('MonatsgrenzenUmschalten', 'visible', $modus === 2);
|
||||
if ($modus !== 2) {
|
||||
@@ -346,6 +395,17 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
|
||||
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
|
||||
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||
$this->setzeNichtGeregelteVerbraucher('Lizenz nicht freigegeben');
|
||||
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->aktiveVerbraucherIDs();
|
||||
} catch (Throwable $fehler) {
|
||||
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
|
||||
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||
$this->setzeDiagnosewert('Lizenzstatus', 'Nicht freigegeben: ' . $fehler->getMessage());
|
||||
$this->setzeNichtGeregelteVerbraucher($fehler->getMessage());
|
||||
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
|
||||
return;
|
||||
}
|
||||
@@ -358,6 +418,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
if (!$aktiv || !$netzleistungGueltig) {
|
||||
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
|
||||
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||
$this->setzeNichtGeregelteVerbraucher(
|
||||
$aktiv ? 'Netzleistungsmessung fehlt oder ist veraltet' : 'Manager ist deaktiviert'
|
||||
);
|
||||
$this->SetStatus($aktiv ? self::STATUS_MESSWERT_UNGUELTIG : self::STATUS_AKTIV);
|
||||
return;
|
||||
}
|
||||
@@ -385,8 +448,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
|
||||
$stoerungen = [];
|
||||
$nichtGeregelt = [];
|
||||
$synchronisiert = true;
|
||||
$verbraucher = $this->aktuelleVerbraucher($stoerungen, $betriebsart, $synchronisiert);
|
||||
$verbraucher = $this->aktuelleVerbraucher(
|
||||
$stoerungen,
|
||||
$nichtGeregelt,
|
||||
$betriebsart,
|
||||
$synchronisiert
|
||||
);
|
||||
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
|
||||
if (!$synchronisiert) {
|
||||
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||
@@ -394,7 +463,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
'Betriebsart' => $betriebsart,
|
||||
'Synchronisiert' => false,
|
||||
'Verbraucher' => $verbraucher,
|
||||
'NichtGeregelt' => $nichtGeregelt,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
$this->setzeDiagnosewert(
|
||||
'NichtGeregelteGeraete',
|
||||
$this->nichtGeregelteGeraeteText($nichtGeregelt)
|
||||
);
|
||||
$this->aktualisiereStoerungen($verbraucher, $stoerungen);
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
return;
|
||||
@@ -415,7 +489,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
'Synchronisiert' => true,
|
||||
'Restabweichung_W' => $ergebnis['Restabweichung_W'],
|
||||
'Verbraucher' => $verbraucher,
|
||||
'NichtGeregelt' => [],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
$this->setzeDiagnosewert(
|
||||
'NichtGeregelteGeraete',
|
||||
'Alle aktiven Verbraucher werden geregelt.'
|
||||
);
|
||||
|
||||
$letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte');
|
||||
foreach ($ergebnis['Sollwerte'] as $instanzID => $sollleistung) {
|
||||
@@ -457,10 +536,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
/**
|
||||
* @param list<string> $stoerungen
|
||||
* @param list<array<string, mixed>> $nichtGeregelt
|
||||
* @return list<array{InstanzID: int, Daten: array<string, mixed>}>
|
||||
*/
|
||||
private function aktuelleVerbraucher(
|
||||
array &$stoerungen,
|
||||
array &$nichtGeregelt,
|
||||
string $betriebsart,
|
||||
bool &$synchronisiert
|
||||
): array {
|
||||
@@ -483,11 +564,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
if ($grund !== '') {
|
||||
$synchronisiert = false;
|
||||
$stoerungen[] = $grund . ' von Instanz ' . $instanzID . '.';
|
||||
$detail = $this->verbraucherDetail($instanzID, $grund, $eintrag);
|
||||
$nichtGeregelt[] = $detail;
|
||||
$stoerungen[] = $detail['Name'] . ': ' . $grund . '.';
|
||||
try {
|
||||
$this->sendeManagerdaten($instanzID, $betriebsart, null);
|
||||
} catch (Throwable $fehler) {
|
||||
$stoerungen[] = 'Betriebsart an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
|
||||
$stoerungen[] = $detail['Name'] . ': Betriebsart konnte nicht uebermittelt werden: '
|
||||
. $fehler->getMessage();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -500,8 +584,124 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $aktuell;
|
||||
}
|
||||
|
||||
private function setzeNichtGeregelteVerbraucher(string $grund): void
|
||||
{
|
||||
try {
|
||||
$ids = $this->konfigurierteAktiveVerbraucherIDs();
|
||||
} catch (Throwable $fehler) {
|
||||
$ids = [];
|
||||
$grund .= '; Verbraucherzuordnung ungueltig: ' . $fehler->getMessage();
|
||||
}
|
||||
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
||||
$nichtGeregelt = [];
|
||||
$stoerungen = [];
|
||||
foreach ($ids as $instanzID) {
|
||||
$detail = $this->verbraucherDetail(
|
||||
$instanzID,
|
||||
$grund,
|
||||
$cache[(string) $instanzID] ?? null
|
||||
);
|
||||
$nichtGeregelt[] = $detail;
|
||||
$stoerungen[] = $detail['Name'] . ': ' . $grund . '.';
|
||||
}
|
||||
|
||||
$this->setzeDiagnosewert('VerbraucherAnzahl', count($ids));
|
||||
$this->setzeDiagnosewert('VerbraucherVerfuegbar', 0);
|
||||
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
||||
'Betriebsart' => ManagerRegler::BETRIEBSART_INAKTIV,
|
||||
'Synchronisiert' => false,
|
||||
'Verbraucher' => [],
|
||||
'NichtGeregelt' => $nichtGeregelt,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
$this->setzeDiagnosewert(
|
||||
'NichtGeregelteGeraete',
|
||||
$this->nichtGeregelteGeraeteText($nichtGeregelt)
|
||||
);
|
||||
$this->aktualisiereStoerungen([], $stoerungen);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $cacheEintrag
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function verbraucherDetail(
|
||||
int $instanzID,
|
||||
string $grund,
|
||||
?array $cacheEintrag
|
||||
): array {
|
||||
$name = 'Instanz ' . $instanzID;
|
||||
$typ = 'Unbekannter Verbraucher';
|
||||
$instanzStatus = null;
|
||||
if (IPS_InstanceExists($instanzID)) {
|
||||
$name = IPS_GetName($instanzID);
|
||||
$instanz = IPS_GetInstance($instanzID);
|
||||
$instanzStatus = (int) ($instanz['InstanceStatus'] ?? 0);
|
||||
$modulID = strtoupper((string) ($instanz['ModuleInfo']['ModuleID'] ?? ''));
|
||||
$typ = self::VERBRAUCHER_LIZENZEN[$modulID]['Name'] ?? $typ;
|
||||
}
|
||||
|
||||
$daten = is_array($cacheEintrag['Daten'] ?? null)
|
||||
? $cacheEintrag['Daten']
|
||||
: [];
|
||||
$empfangenAm = is_int($cacheEintrag['EmpfangenAm'] ?? null)
|
||||
? $cacheEintrag['EmpfangenAm']
|
||||
: null;
|
||||
|
||||
return [
|
||||
'InstanzID' => $instanzID,
|
||||
'Name' => $name,
|
||||
'Typ' => $typ,
|
||||
'Grund' => $grund,
|
||||
'InstanzStatus' => $instanzStatus,
|
||||
'LetzteMeldung' => $empfangenAm !== null ? date(DATE_ATOM, $empfangenAm) : null,
|
||||
'GemeldeteBetriebsart' => is_string($daten['Betriebsart'] ?? null)
|
||||
? $daten['Betriebsart']
|
||||
: null,
|
||||
'Verfuegbar' => is_bool($daten['Verfuegbar'] ?? null)
|
||||
? $daten['Verfuegbar']
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $nichtGeregelt
|
||||
*/
|
||||
private function nichtGeregelteGeraeteText(array $nichtGeregelt): string
|
||||
{
|
||||
if ($nichtGeregelt === []) {
|
||||
return 'Alle aktiven Verbraucher werden geregelt.';
|
||||
}
|
||||
|
||||
$zeilen = [];
|
||||
foreach ($nichtGeregelt as $detail) {
|
||||
$teile = [
|
||||
(string) ($detail['Name'] ?? 'Unbekanntes Geraet'),
|
||||
(string) ($detail['Typ'] ?? 'Unbekannter Typ'),
|
||||
(string) ($detail['Grund'] ?? 'Grund unbekannt'),
|
||||
];
|
||||
if (is_string($detail['LetzteMeldung'] ?? null)) {
|
||||
$teile[] = 'letzte Meldung: ' . $detail['LetzteMeldung'];
|
||||
}
|
||||
if (is_bool($detail['Verfuegbar'] ?? null)) {
|
||||
$teile[] = 'verfuegbar: ' . ($detail['Verfuegbar'] ? 'ja' : 'nein');
|
||||
}
|
||||
$zeilen[] = implode(' | ', $teile);
|
||||
}
|
||||
|
||||
return implode("\n", $zeilen);
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function aktiveVerbraucherIDs(): array
|
||||
{
|
||||
$ids = $this->konfigurierteAktiveVerbraucherIDs();
|
||||
$this->pruefeVerbraucherlizenzen($ids);
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function konfigurierteAktiveVerbraucherIDs(): array
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('AutomatischeSuche')) {
|
||||
return $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
|
||||
@@ -521,6 +721,64 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
*/
|
||||
private function pruefeVerbraucherlizenzen(array $ids): void
|
||||
{
|
||||
[$mengen, $namen] = $this->verbraucherLizenzbelegung($ids);
|
||||
|
||||
try {
|
||||
Lizenzpruefung::pruefeVerbrauchermengen($this->leseLizenzLease(), $mengen);
|
||||
} catch (InvalidArgumentException $fehler) {
|
||||
foreach ($mengen as $kennung => $verwendet) {
|
||||
$lizenziert = Lizenzpruefung::berechtigungsMenge(
|
||||
$this->leseLizenzLease(),
|
||||
$kennung
|
||||
);
|
||||
if ($verwendet > $lizenziert) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s: %d aktiviert, aber nur %d lizenziert. Bitte Auswahl reduzieren oder Lizenz erweitern.',
|
||||
$namen[$kennung],
|
||||
$verwendet,
|
||||
$lizenziert
|
||||
), 0, $fehler);
|
||||
}
|
||||
}
|
||||
throw $fehler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
* @return array{0: array<string, int>, 1: array<string, string>}
|
||||
*/
|
||||
private function verbraucherLizenzbelegung(array $ids): array
|
||||
{
|
||||
$mengen = [];
|
||||
$namen = [];
|
||||
foreach ($ids as $instanzID) {
|
||||
if (!IPS_InstanceExists($instanzID)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Die Verbraucherinstanz ' . $instanzID . ' existiert nicht.'
|
||||
);
|
||||
}
|
||||
$instanz = IPS_GetInstance($instanzID);
|
||||
$modulID = strtoupper((string) ($instanz['ModuleInfo']['ModuleID'] ?? ''));
|
||||
$lizenz = self::VERBRAUCHER_LIZENZEN[$modulID] ?? null;
|
||||
if (!is_array($lizenz)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Instanz ' . $instanzID . ' ist kein lizenzierbarer Enelix-Verbraucher.'
|
||||
);
|
||||
}
|
||||
$kennung = $lizenz['Katalog'];
|
||||
$mengen[$kennung] = ($mengen[$kennung] ?? 0) + 1;
|
||||
$namen[$kennung] = $lizenz['Name'];
|
||||
}
|
||||
|
||||
return [$mengen, $namen];
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function leseAktiveVerbraucherIDs(string $property): array
|
||||
{
|
||||
@@ -579,7 +837,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
$suchbereichID ??= $this->ReadPropertyInteger('SuchbereichID');
|
||||
$werte = [];
|
||||
foreach (self::VERBRAUCHER_MODUL_IDS as $modulID) {
|
||||
foreach (array_keys(self::VERBRAUCHER_LIZENZEN) as $modulID) {
|
||||
foreach (IPS_GetInstanceListByModuleID($modulID) as $instanzID) {
|
||||
if (!$this->istImSuchbereich($instanzID, $suchbereichID)) {
|
||||
continue;
|
||||
@@ -743,6 +1001,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
80
|
||||
);
|
||||
$this->RegisterVariableString('Verbraucherstatus', 'Verbraucherstatus', '', 90);
|
||||
$this->RegisterVariableString(
|
||||
'NichtGeregelteGeraete',
|
||||
'Nicht geregelte Geraete',
|
||||
'',
|
||||
95
|
||||
);
|
||||
$this->RegisterVariableString('Monatsgrenzen', 'Monatsgrenzen', '', 100);
|
||||
$this->RegisterVariableString('Prognosestatus', 'Prognosestatus', '', 110);
|
||||
$this->RegisterVariableString('SDLStatus', 'SDL-Status', '', 120);
|
||||
@@ -796,23 +1060,59 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
|
||||
$lease = $this->leseLizenzLease();
|
||||
$codePasst = $gespeicherterHash !== '' && hash_equals($gespeicherterHash, $codeHash);
|
||||
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
|
||||
$basisGueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
|
||||
$lease,
|
||||
$installationID,
|
||||
0
|
||||
);
|
||||
$gueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
|
||||
$lease,
|
||||
$installationID,
|
||||
$this->ReadPropertyInteger('Lastspitzenmodus')
|
||||
$lastspitzenmodus
|
||||
);
|
||||
$faellig = !$gueltig || Lizenzpruefung::erneuerungFaellig($lease);
|
||||
$berechtigungenVeraltet = $basisGueltig
|
||||
&& !$this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
|
||||
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|
||||
|| Lizenzpruefung::erneuerungFaellig($lease);
|
||||
$jetzt = time();
|
||||
|
||||
if (!$erzwingen
|
||||
&& (!$faellig || $jetzt < $this->ReadAttributeInteger('LizenzNaechsterVersuch'))
|
||||
) {
|
||||
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
|
||||
if (!$erzwingen) {
|
||||
if (!$routineFaellig && !$berechtigungenVeraltet) {
|
||||
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
|
||||
|
||||
return $gueltig;
|
||||
return $gueltig;
|
||||
}
|
||||
if ($routineFaellig
|
||||
&& $jetzt < $this->ReadAttributeInteger('LizenzNaechsterVersuch')
|
||||
) {
|
||||
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
|
||||
|
||||
return $gueltig;
|
||||
}
|
||||
if ($berechtigungenVeraltet
|
||||
&& $jetzt < $this->ReadAttributeInteger(
|
||||
'LizenzBerechtigungenNaechsterVersuch'
|
||||
)
|
||||
) {
|
||||
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
|
||||
|
||||
return $gueltig;
|
||||
}
|
||||
}
|
||||
|
||||
$this->WriteAttributeInteger('LizenzNaechsterVersuch', $jetzt + self::LIZENZ_RETRY_SEKUNDEN);
|
||||
if ($routineFaellig || $erzwingen) {
|
||||
$this->WriteAttributeInteger(
|
||||
'LizenzNaechsterVersuch',
|
||||
$jetzt + self::LIZENZ_RETRY_SEKUNDEN
|
||||
);
|
||||
}
|
||||
if ($berechtigungenVeraltet || $erzwingen) {
|
||||
$this->WriteAttributeInteger(
|
||||
'LizenzBerechtigungenNaechsterVersuch',
|
||||
$jetzt + self::LIZENZ_RETRY_SEKUNDEN
|
||||
);
|
||||
}
|
||||
try {
|
||||
$neueLease = $this->rufeLizenzserverAuf($normalisiert, $installationID);
|
||||
Lizenzpruefung::pruefeLease(
|
||||
@@ -825,9 +1125,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
json_encode($neueLease, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
$this->WriteAttributeString('LizenzcodeHash', $codeHash);
|
||||
$naechsteErneuerung = Lizenzpruefung::erneuernAb($neueLease);
|
||||
$this->WriteAttributeInteger('LizenzNaechsterVersuch', $naechsteErneuerung);
|
||||
$this->WriteAttributeInteger(
|
||||
'LizenzNaechsterVersuch',
|
||||
Lizenzpruefung::erneuernAb($neueLease)
|
||||
'LizenzBerechtigungenNaechsterVersuch',
|
||||
$naechsteErneuerung
|
||||
);
|
||||
$this->WriteAttributeString('LizenzLetzterFehler', '');
|
||||
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
|
||||
@@ -860,6 +1162,108 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
}
|
||||
|
||||
private function peakShavingLizenziert(): bool
|
||||
{
|
||||
$lizenzcode = Lizenzpruefung::normalisiereCode(
|
||||
$this->ReadPropertyString('Lizenzcode')
|
||||
);
|
||||
if (!Lizenzpruefung::istLizenzcodeGueltig($lizenzcode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
|
||||
if ($gespeicherterHash === ''
|
||||
|| !hash_equals($gespeicherterHash, hash('sha256', $lizenzcode))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lease = $this->leseLizenzLease();
|
||||
if (!Lizenzpruefung::istLeaseGueltig(
|
||||
$lease,
|
||||
$this->ReadAttributeString('LizenzInstallationID'),
|
||||
0
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Lizenzpruefung::berechtigungsMenge(
|
||||
$lease,
|
||||
Lizenzpruefung::MANAGER_PEAK
|
||||
) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $lease
|
||||
*/
|
||||
private function lizenzBerechtigungenPassen(array $lease, int $lastspitzenmodus): bool
|
||||
{
|
||||
if ($lastspitzenmodus !== 0
|
||||
&& Lizenzpruefung::berechtigungsMenge(
|
||||
$lease,
|
||||
Lizenzpruefung::MANAGER_PEAK
|
||||
) < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
[$mengen] = $this->verbraucherLizenzbelegung(
|
||||
$this->konfigurierteAktiveVerbraucherIDs()
|
||||
);
|
||||
Lizenzpruefung::pruefeVerbrauchermengen($lease, $mengen);
|
||||
|
||||
return true;
|
||||
} catch (Throwable $fehler) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{caption: string, value: int}>
|
||||
*/
|
||||
private function lastspitzenOptionen(bool $lizenziert): array
|
||||
{
|
||||
$optionen = [[
|
||||
'caption' => $lizenziert
|
||||
? 'Aus (Peak-Shaving-Lizenz verfuegbar)'
|
||||
: 'Aus (Peak-Shaving-Lizenz fehlt)',
|
||||
'value' => 0,
|
||||
]];
|
||||
if ($lizenziert) {
|
||||
$optionen[] = ['caption' => 'Konstant', 'value' => 1];
|
||||
$optionen[] = ['caption' => 'Monatlich', 'value' => 2];
|
||||
}
|
||||
|
||||
return $optionen;
|
||||
}
|
||||
|
||||
private function aktualisiereLastspitzenformular(): void
|
||||
{
|
||||
$lizenziert = $this->peakShavingLizenziert();
|
||||
$modus = $lizenziert ? $this->ReadPropertyInteger('Lastspitzenmodus') : 0;
|
||||
$this->UpdateFormField(
|
||||
'Lastspitzenmodus',
|
||||
'options',
|
||||
json_encode($this->lastspitzenOptionen($lizenziert), JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$this->UpdateFormField('Lastspitzenmodus', 'value', $modus);
|
||||
$this->UpdateFormField(
|
||||
'Lastspitzengrenze',
|
||||
'visible',
|
||||
$lizenziert && $modus === 1
|
||||
);
|
||||
$this->UpdateFormField(
|
||||
'MonatsgrenzenUmschalten',
|
||||
'visible',
|
||||
$lizenziert && $modus === 2
|
||||
);
|
||||
if (!$lizenziert || $modus !== 2) {
|
||||
$this->SetBuffer('MonatsgrenzenSichtbar', '0');
|
||||
$this->UpdateFormField('Monatsgrenzen', 'visible', false);
|
||||
}
|
||||
}
|
||||
|
||||
private function lizenzStatustext(?string $lizenzcode = null): string
|
||||
{
|
||||
$code = Lizenzpruefung::normalisiereCode(
|
||||
@@ -886,12 +1290,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->ReadAttributeString('LizenzInstallationID'),
|
||||
$this->ReadPropertyInteger('Lastspitzenmodus')
|
||||
);
|
||||
$this->aktiveVerbraucherIDs();
|
||||
$status = ($lease['development'] ?? false) === true
|
||||
? 'Aktiv (Entwicklung)'
|
||||
: 'Aktiv';
|
||||
$status .= ' bis ' . date('d.m.Y H:i', Lizenzpruefung::offlineBis($lease));
|
||||
? 'Aktiv (Entwicklung) - unbeschraenkt gueltig.'
|
||||
: 'Aktiv - unbeschraenkt gueltig.';
|
||||
if ($fehler !== '') {
|
||||
$status .= '; Erneuerung fehlgeschlagen: ' . $fehler;
|
||||
$status .= ' Online-Pruefung fehlgeschlagen; lokale Freigabe endet am '
|
||||
. date('d.m.Y H:i', Lizenzpruefung::offlineBis($lease))
|
||||
. ', sofern keine Verbindung hergestellt wird. '
|
||||
. 'Die gekaufte Lizenz bleibt unbeschraenkt gueltig. Fehler: '
|
||||
. $fehler;
|
||||
}
|
||||
|
||||
return $status;
|
||||
|
||||
@@ -6,26 +6,64 @@ Energiemanagement, Manager und steuerbare Verbraucher fuer IP-Symcon.
|
||||
|
||||
Das Repository befindet sich im Aufbau. Der gemeinsame Nachrichtenvertrag, die
|
||||
Verbraucherbasis, der Manager, der mehrstufige Warmwassererwaermer, der
|
||||
1-stufige Verbraucher und die Ladestation Stand-Alone sind implementiert. Die
|
||||
weiteren Module sind als Diskussionsentwürfe dokumentiert.
|
||||
Pufferspeicher, die Batterie, der 1-stufige Verbraucher, die Ladestation
|
||||
Stand-Alone sowie das Easee Gateway mit Ladestation Gateway sind implementiert. Die weiteren
|
||||
Module sind als Diskussionsentwuerfe dokumentiert.
|
||||
|
||||
## Geplante Module
|
||||
## Module
|
||||
|
||||
- Manager (implementiert)
|
||||
- Batterie (implementiert)
|
||||
- Wassererwaermer (implementiert)
|
||||
- Pufferspeicher
|
||||
- Pufferspeicher (implementiert)
|
||||
- Verbraucher 1-Stufig (implementiert)
|
||||
- Waermepumpe (implementiert)
|
||||
- Ladestation Stand-Alone (implementiert)
|
||||
- Ladestation Gateway
|
||||
- Easee Gateway
|
||||
- Ladestation Gateway (implementiert)
|
||||
- Easee Gateway (implementiert)
|
||||
|
||||
Die Properties, Variablen, Zustandsdaten und offenen Punkte stehen in der
|
||||
[Modulübersicht](docs/module/README.md). Gemeinsame Felder werden dort nicht
|
||||
abweichend neu definiert, sondern verweisen auf den zentralen Vertrag.
|
||||
|
||||
Die Verbraucher kommunizieren mit dem Manager ueber den gemeinsamen Vertrag in `libs/`. Der Manager waehlt die Verbraucher aus; in einem Verbraucher gibt es keine Manager-ID-Property.
|
||||
Die Verbraucher kommunizieren mit dem Manager ueber den gemeinsamen Vertrag
|
||||
in `libs/`. Der Manager waehlt die Verbraucher aus; in einem Verbraucher gibt
|
||||
es keine Manager-ID-Property.
|
||||
|
||||
## Betriebsartabhaengige Leistungsangebote
|
||||
|
||||
Der Nachrichtenvertrag `4.0` uebermittelt in beiden Richtungen verpflichtend
|
||||
die aktuelle `Betriebsart`. Zulaessige Werte sind `PV` fuer Solarbetrieb und
|
||||
`Peak` fuer die Lastspitzenbegrenzung. Dadurch kann jeder Verbraucher fuer
|
||||
beide Betriebsarten unterschiedliche PowerSteps melden.
|
||||
|
||||
Ein Betriebsartwechsel wird synchronisiert:
|
||||
|
||||
1. Der Manager bestimmt die neue Betriebsart.
|
||||
2. Er sendet sie mit `Sollleistung_W: null` an alle aktiven Verbraucher.
|
||||
3. `null` ist nur eine Ankuendigung und kein Schaltbefehl.
|
||||
4. Jeder Verbraucher berechnet sein zustandsabhaengiges Leistungsangebot neu
|
||||
und meldet es mit derselben Betriebsart zurueck.
|
||||
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der
|
||||
Manager konkrete Sollleistungen.
|
||||
|
||||
Der Manager verwendet damit nie ein PV-Angebot fuer Peak oder umgekehrt. Eine
|
||||
technische Schaltsperre, eine ausstehende Rueckmeldung oder eine Mindestzeit
|
||||
kann das Angebot weiterhin auf die aktuell gehaltene Leistung begrenzen.
|
||||
|
||||
| Verbraucher | PV-Angebot | Peak-Angebot |
|
||||
| --- | --- | --- |
|
||||
| Verbraucher 1-Stufig | Normal `[0, Nennleistung]`; bei faelliger Tagesmindestlaufzeit `[Nennleistung]` | Normal `[0]`; bei faelliger Tagesmindestlaufzeit konfigurierbar `[0, Nennleistung]` oder `[Nennleistung]` |
|
||||
| Ladestation Stand-Alone | Mit Solarladen `[0, ...Ladestufen]`, sonst maximale Ladeleistung | Mit Solarladen `[0]`, sonst `[0, ...Ladestufen]` |
|
||||
| Ladestation Gateway | Mit Solarladen `[0, ...Ladestufen]`, sonst maximale Ladeleistung | Mit Solarladen `[0]`, sonst `[0, ...Ladestufen]` |
|
||||
| Warmwassererwaermer | Unter Mindesttemperatur maximale Stufe, sonst zustandsabhaengig | Unter wirksamer Mindesttemperatur `[0, ...Leistungsstufen]`, sonst `[0]` |
|
||||
| Pufferspeicher | Unter Einschaltschwelle `[0, ...Leistungsstufen]`, sonst `[0]` | Unabhaengig vom Zustand `[0]` |
|
||||
| Batterie | SoC-abhaengiges Lade-/Entladeraster | SoC-, Reserve- und netzabhaengiges Angebot |
|
||||
|
||||
Die vollstaendigen Nachrichtenfelder und JSON-Beispiele stehen in der
|
||||
[Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md). Hintergrund,
|
||||
Alternativen und Folgen beschreibt
|
||||
[ADR 0004](docs/adr/0004-betriebsartabhaengige-leistungsangebote.md).
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
@@ -63,11 +101,16 @@ der Uebernahme in einen Freigabebranch erfolgreich sein.
|
||||
- [Zentrale Liste offener Teamentscheidungen](docs/Offene-Punkte.md)
|
||||
- [Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md)
|
||||
- [Batterieschnittstelle](docs/Schnittstelle-Batterie.md)
|
||||
- [ADR 0004: Betriebsartabhaengige Leistungsangebote](docs/adr/0004-betriebsartabhaengige-leistungsangebote.md)
|
||||
- [Manager-Modul inklusive Lizenzierung](docs/module/Manager/README.md)
|
||||
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
||||
- [Pufferspeicher-Modul](docs/module/Pufferspeicher/README.md)
|
||||
- [Batteriemodul](docs/module/Batterie/README.md)
|
||||
- [Verbraucher-1-Stufig-Modul](docs/module/Verbraucher-1-Stufig/README.md)
|
||||
- [Ladestation-Stand-Alone-Modul](docs/module/Ladestation-Stand-Alone/README.md)
|
||||
- [Ladestation-Gateway-Modul](docs/module/Ladestation-Gateway/README.md)
|
||||
- [Easee-Gateway-Modul](docs/module/Easee-Gateway/README.md)
|
||||
- [Easee-Gateway-Schnittstelle](docs/Schnittstelle-Easee-Gateway.md)
|
||||
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
||||
- [Migration Verbraucher 1-Stufig](docs/migration/Verbraucher-1-Stufig.md)
|
||||
- [Migration Batterie](docs/migration/Batterie.md)
|
||||
|
||||
+14
-13
@@ -47,25 +47,13 @@ Prioritaeten, Fehlerverhalten und ein pruefbares Ergebnis.
|
||||
| ID | Status | Bereich | Kurzthema |
|
||||
| --- | --- | --- | --- |
|
||||
| OP-001 | Entschieden | Lizenzierung | Betriebsort des Lizenzservers |
|
||||
| OP-002 | Offen | Grundeinrichtung | Generator und kostenpflichtige Ersteinrichtung |
|
||||
| OP-002 | Erledigt | Grundeinrichtung | Generator und kostenpflichtige Ersteinrichtung |
|
||||
| OP-003 | Offen | Lizenzportal | VGT-Integration in license.enelix.ch |
|
||||
| OP-004 | Offen | Lizenzierung | Lizenzmodell des Verbrauchskostenreports |
|
||||
| OP-005 | Offen | Teststrategie | Standort fuer Feldtests |
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
### OP-002: Generator und kostenpflichtige Ersteinrichtung
|
||||
|
||||
- **Entscheidungsfrage:** Soll der automatische Generator fuer die
|
||||
Grundeinrichtung zugleich die kostenpflichtige Ersteinrichtung abbilden?
|
||||
- **Kontext:** Der vorgesehene automatische Generator fuer die Grundeinrichtung
|
||||
muss gegenueber der kostenpflichtigen Ersteinrichtung abgegrenzt werden.
|
||||
- **Auswirkung:** Die Entscheidung legt fest, ob beide als derselbe Vorgang oder
|
||||
als getrennte Leistungen behandelt werden.
|
||||
- **Verantwortlich:** Offen
|
||||
- **Ziel-Sprint:** Offen
|
||||
- **Ergebnis/Verweis:** Offen
|
||||
|
||||
### OP-003: VGT-Integration in license.enelix.ch
|
||||
|
||||
- **Entscheidungsfrage:** Soll die VGT-Anwendung als Bestandteil in die
|
||||
@@ -113,6 +101,19 @@ Prioritaeten, Fehlerverhalten und ein pruefbares Ergebnis.
|
||||
- **Ergebnis/Verweis:** [Obere Anschluesse](Obere-Anschluesse.md) und
|
||||
[Manager](module/Manager/README.md)
|
||||
|
||||
### OP-002: Generator und kostenpflichtige Ersteinrichtung
|
||||
|
||||
- **Entscheidung:** Der automatische Systemgenerator und die kostenpflichtige
|
||||
Ersteinrichtung sind derselbe Vorgang.
|
||||
- **Abrechnung:** Eine Bestellung aus dem Systemkonfigurator enthaelt
|
||||
automatisch die noch nicht bezahlten Einrichtungskosten fuer den Manager und
|
||||
die konfigurierten Verbrauchermodule. Bereits bezahlte Einrichtungsmengen
|
||||
werden je Anlage angerechnet. Direkte Lizenzbestellungen enthalten keine
|
||||
Einrichtungskosten.
|
||||
- **Status:** Erledigt
|
||||
- **Ergebnis/Verweis:** Umsetzung im Lizenzportal unter
|
||||
`https://license.enelix.ch`.
|
||||
|
||||
## Vorlage fuer neue Punkte
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -1,63 +1,296 @@
|
||||
# Ladestation Stand-Alone
|
||||
|
||||
> Status: Implementiert. Eigenstaendiges Verbrauchermodul mit direkter
|
||||
> Geraeteanbindung; keine Beziehung zur Ladestation Gateway.
|
||||
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `4.0`.
|
||||
|
||||
Das Modul bindet eine einzelne Ladestation direkt an Enelix EMS an. Es liest
|
||||
den Fahrzeug- und Ladezustand ueber die Geraete-API, ermittelt die Phasenzahl
|
||||
wie Enelix 1 und setzt den vom Manager gewaehlten Ladestrom. Es benoetigt keine
|
||||
Gateway-Instanz und hat keine technische Beziehung zum separaten Modul
|
||||
**Ladestation Gateway**.
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- direkte Statusabfrage und Steuerung der unterstuetzten Ladestationen,
|
||||
- Fahrzeug-, Lade- und Phasenerkennung nach der Enelix-1-Logik,
|
||||
- Leistungsangebote fuer PV- und Peakbetrieb,
|
||||
- lokale Freigabe und Umschaltung zwischen Solar- und Normalbetrieb,
|
||||
- Kommunikation mit dem Enelix Manager ueber den Nachrichtenvertrag `4.0`,
|
||||
- optional sichtbare Diagnosevariablen und schaltbares Debug-Logging,
|
||||
- automatisierte Tests mit Fake-HTTP-Transport ohne reale Ladestation.
|
||||
|
||||
## Unterstuetzte Geraete
|
||||
|
||||
| Geraetetyp | Status | Steuerung |
|
||||
| Geraetetyp | Statusabfrage | Steuerung | Erforderliche Konfiguration |
|
||||
| --- | --- | --- | --- |
|
||||
| go-e Charger, alte API | `GET /mqtt?payload=` | `alw` und `amp` | IP-Adresse oder Hostname |
|
||||
| go-e Charger Gemini / Gemini flex | `GET /api/status` | `frc` und `amp` | IP-Adresse oder Hostname |
|
||||
| smart-me Pico | Pico-Charging-API | Load-Management-Current-API | Geraete-ID, Seriennummer, Benutzername und Passwort |
|
||||
|
||||
Bei go-e wird die lokale HTTP-API verwendet. Die Geraeteadresse wird ohne
|
||||
`http://`, Pfad oder Parameter eingetragen. Die Pico-Anbindung verwendet HTTPS
|
||||
und HTTP Basic Auth gegen `api.smart-me.com`. Zugangsdaten werden weder als
|
||||
Variable noch im Debug-Protokoll oder im Diagnosefeld
|
||||
`LetzterGeraetebefehl` ausgegeben.
|
||||
|
||||
## Fahrzeug- und Phasenerkennung
|
||||
|
||||
Die Auswertung folgt bewusst dem Verhalten der bisherigen Enelix-1-Ladestation:
|
||||
|
||||
1. Bei go-e gilt das Fahrzeug als verbunden, wenn `car != 1` ist. Bei Pico
|
||||
wird entsprechend `State != 1` ausgewertet.
|
||||
2. Die gemessene Leistung wird bei der alten go-e-API mit Faktor 10, bei
|
||||
Gemini direkt in Watt und bei Pico von kW in Watt umgerechnet.
|
||||
3. Bei einer Leistung ueber 7500 W wird dreiphasiges Laden erkannt. Darunter
|
||||
wird bei verbundenem Fahrzeug einphasiges Laden angenommen.
|
||||
4. Der Ladestrom wird aus der Leistung mit `230 V` einphasig beziehungsweise
|
||||
`1.71 * 400 V` dreiphasig berechnet.
|
||||
5. War das Fahrzeug bereits im vorherigen Zyklus verbunden und liegt der
|
||||
ermittelte Maximalstrom aus Ladestrom plus `2.5 A` unter `6 A`, wird
|
||||
`FahrzeugGeladen=true` gesetzt.
|
||||
|
||||
Der normalisierte `Fahrzeugstatus` verwendet folgende Werte:
|
||||
|
||||
| Wert | Bedeutung |
|
||||
| ---: | --- |
|
||||
| `0` | unbekannt oder noch nicht ermittelt |
|
||||
| `1` | kein Fahrzeug verbunden |
|
||||
| `2` | Fahrzeug verbunden und bereit |
|
||||
| `3` | Fahrzeug laedt |
|
||||
| `4` | Fahrzeug geladen |
|
||||
|
||||
## Leistungsregelung
|
||||
|
||||
Ein Leistungsangebot groesser als `0 W` wird nur erzeugt, wenn das Modul
|
||||
aktiviert, die Ladefreigabe gesetzt, ein Fahrzeug verbunden und das Fahrzeug
|
||||
noch nicht als geladen erkannt ist. Andernfalls meldet das Modul `[0]` und
|
||||
stoppt die Ladestation.
|
||||
|
||||
| Betriebsart | Solarladen | Leistungsangebot |
|
||||
| --- | --- | --- |
|
||||
| go-e Charger, alte API | `GET /mqtt?payload=` | `alw` und `amp` |
|
||||
| go-e Charger Gemini / Gemini flex | `GET /api/status` | `frc` und `amp` |
|
||||
| smart-me Pico | Pico-Charging-API | Load-Management-Current-API mit Basic Auth |
|
||||
| PV | ein | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum |
|
||||
| PV | aus | ausschliesslich die maximale Ladeleistung |
|
||||
| Peak | ein | ausschliesslich `[0]`; die Ladestation wird gestoppt |
|
||||
| Peak | aus | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum |
|
||||
|
||||
Die Routen, Leistungsfaktoren und Erkennung wurden gezielt aus Enelix 1
|
||||
uebernommen. Zugangsdaten werden weder als Variable noch im Diagnoseprotokoll
|
||||
ausgegeben.
|
||||
Die festen ein- und dreiphasigen Leistungsstufen sowie deren Rundung wurden aus
|
||||
Enelix 1 uebernommen. Ohne gueltige Manager-Vorgabe faehrt das Modul bei
|
||||
aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es die
|
||||
maximale angebotene Leistung.
|
||||
|
||||
## Verhalten
|
||||
## Bedienung
|
||||
|
||||
- `FahrzeugVerbunden` entspricht bei go-e `car != 1` und bei Pico `State != 1`.
|
||||
- Die Phasenerkennung verwendet wie Enelix 1 die gemessene Ladeleistung:
|
||||
ueber 7500 W sind dreiphasig, sonst einphasig.
|
||||
- Ein bereits verbundenes Fahrzeug gilt bei einem ermittelten Maximalstrom
|
||||
unter 6 A als geladen.
|
||||
- PV mit Solarladen bietet `0 W` und alle Ladestufen von 6 A bis zum
|
||||
konfigurierten Maximum an.
|
||||
- PV ohne Solarladen bietet ausschliesslich die Maximalleistung an.
|
||||
- Peak mit Solarladen bietet nur `[0]` an und stoppt die Ladestation.
|
||||
- Peak ohne Solarladen bietet `[0, ...Ladestufen]` an, damit der Manager die
|
||||
Ladeleistung wie in Enelix 1 stufenweise reduzieren kann.
|
||||
- Ohne gueltige Manager-Vorgabe wird bei Solarladen mit 0 A und ohne
|
||||
Solarladen mit maximaler Leistung gefahren.
|
||||
| Ident | Wirkung |
|
||||
| --- | --- |
|
||||
| `Aktiv` | Gemeinsame lokale EMS-Freigabe. `false` setzt das Angebot auf `[0]` und stoppt die Ladestation. |
|
||||
| `Ladefreigabe` | Lokale Freigabe fuer das Laden. Diese Variable ist nur mit `EinstellungenInVisu=true` sichtbar. |
|
||||
| `Solarladen` | Schaltet das oben beschriebene PV-/Peak-Verhalten um. Diese Variable ist nur mit `EinstellungenInVisu=true` sichtbar. |
|
||||
|
||||
Die Properties `Ladefreigabe` und `Solarladen` definieren die Startwerte. Eine
|
||||
Aenderung der jeweiligen Property wird beim Anwenden der Konfiguration in den
|
||||
lokalen Zustand uebernommen. Eine Bedienung der Variablen verwirft eine noch
|
||||
gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Gemeinsame EMS-Einstellungen
|
||||
|
||||
| Property | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `PrioritaetPV` | Integer | `0` | Prioritaet des Verbrauchers im PV-Betrieb. |
|
||||
| `PrioritaetPeak` | Integer | `0` | Prioritaet des Verbrauchers im Peakbetrieb. |
|
||||
| `Meldeintervall` | Integer | `10 s` | Intervall fuer die periodische Vollmeldung an zugeordnete Manager. |
|
||||
| `VorgabeTimeout` | Integer | `120 s` | Gueltigkeitsdauer einer Manager-Vorgabe ohne Erneuerung. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Blendet `Ladefreigabe` und `Solarladen` als bedienbare Variablen ein. |
|
||||
| `LoggingEin` | Boolean | `false` | Aktiviert das laufende Debug-Protokoll des Moduls. |
|
||||
|
||||
### Ladestation und Diagnose
|
||||
|
||||
| Property | Typ | Standard | Beschreibung |
|
||||
| --- | --- | ---: | --- |
|
||||
| `Geraetetyp` | Integer | `0` | `1` go-e alt, `2` go-e Gemini, `3` smart-me Pico; `0` ist nicht konfiguriert. |
|
||||
| `Geraeteadresse` | String | leer | IP-Adresse oder Hostname fuer beide go-e-Varianten. |
|
||||
| `GeraeteID` | String | leer | Geraete-ID fuer die Pico-Statusabfrage. |
|
||||
| `Seriennummer` | String | leer | Seriennummer fuer die Pico-Stromvorgabe. |
|
||||
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
|
||||
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. |
|
||||
| `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
|
||||
| `Abfrageintervall` | Integer | `5 s` | Intervall der Geraetestatusabfrage. |
|
||||
| `Ladefreigabe` | Boolean | `false` | Startwert der lokalen Ladefreigabe. |
|
||||
| `Solarladen` | Boolean | `true` | Startwert der lokalen Solarlogik. |
|
||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Blendet gemeinsame und ladestationsspezifische Diagnosevariablen ein. |
|
||||
|
||||
Die internen Properties `Testmodus` und `Testantwort` gehoeren ausschliesslich
|
||||
zum automatisierten Symcon-Funktionstest und erscheinen nicht im
|
||||
Konfigurationsformular.
|
||||
|
||||
## Variablen
|
||||
|
||||
Immer sichtbar:
|
||||
|
||||
| Ident | Typ / Zugriff | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `FahrzeugVerbunden` | Boolean / Anzeige | Geraet meldet ein verbundenes Fahrzeug. |
|
||||
| `FahrzeugGeladen` | Boolean / Anzeige | Enelix-1-Erkennung unter 6 A. |
|
||||
| `Fahrzeugstatus` | Integer / Anzeige | `0` unbekannt, `1` getrennt, `2` bereit, `3` laedt, `4` geladen. |
|
||||
| `Ladestrom` | Float / Anzeige | Aus der gemessenen Leistung ermittelter Strom in A. |
|
||||
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig, `3` dreiphasig. |
|
||||
| `Ladefreigabe` | Boolean / bedienbar | Nur bei `EinstellungenInVisu`; lokale Ladeerlaubnis. |
|
||||
| `Solarladen` | Boolean / bedienbar | Nur bei `EinstellungenInVisu`; variable Ladestufen ein/aus. |
|
||||
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; der Initialwert ist `false`. |
|
||||
| `FahrzeugVerbunden` | Boolean / Anzeige | Zeigt, ob die API ein verbundenes Fahrzeug meldet. |
|
||||
| `FahrzeugGeladen` | Boolean / Anzeige | Ergebnis der kompatiblen Enelix-1-Voll-Erkennung. |
|
||||
| `Fahrzeugstatus` | Integer / Anzeige | Normalisierter Status von `0` bis `4`. |
|
||||
| `Ladestrom` | Float / Anzeige | Aus der gemessenen Leistung ermittelter Ladestrom in A. |
|
||||
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig oder `3` dreiphasig. |
|
||||
|
||||
## Properties
|
||||
Nur mit `EinstellungenInVisu=true` sichtbar und bedienbar:
|
||||
|
||||
Neben den gemeinsamen Verbraucher-Properties werden Geraetetyp, Adresse,
|
||||
Pico-ID, Seriennummer, Zugangsdaten, maximaler Ladestrom, Abfrageintervall
|
||||
sowie die Startwerte fuer Ladefreigabe und Solarladen konfiguriert.
|
||||
- `Ladefreigabe`
|
||||
- `Solarladen`
|
||||
|
||||
Die gemeinsamen Verbraucher- und Diagnosevariablen entsprechen der
|
||||
EMS-Schnittstelle Version 4.0. `LoggingEin` steuert ausschliesslich das
|
||||
laufende Debug-Protokoll.
|
||||
Nur mit `DiagnosevariablenAnzeigen=true` sichtbar:
|
||||
|
||||
| Ident | Beschreibung |
|
||||
| --- | --- |
|
||||
| `Istleistung` | Von der Ladestation gemessene Leistung in W. |
|
||||
| `Leistungsquelle` | Immer `2` fuer eine gemessene Leistung. |
|
||||
| `Sollleistung` | Letzte angenommene Manager-Vorgabe in W. |
|
||||
| `SollwertGueltig` | Zeigt, ob die Vorgabe noch gueltig und im aktuellen Angebot enthalten ist. |
|
||||
| `Verfuegbar` | Zeigt, ob die Ladestation grundsaetzlich Leistung aufnehmen kann. |
|
||||
| `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. |
|
||||
| `Stoerung` | Sammelstatus fuer Konfigurations- und Kommunikationsfehler. |
|
||||
| `Stoertext` | Letzte verstaendliche Fehlerbeschreibung. |
|
||||
| `LetzterGeraetebefehl` | Letzte HTTP-Methode und URL ohne Zugangsdaten. |
|
||||
| `LeistungsangebotDiagnose` | Aktuelles Leistungsangebot als JSON-Liste in W. |
|
||||
|
||||
Die Regellogik speichert ihren Zustand unabhaengig von der Sichtbarkeit der
|
||||
Diagnosevariablen. Das Ein- oder Ausblenden veraendert daher nicht das
|
||||
Regelverhalten.
|
||||
|
||||
## Managerkommunikation
|
||||
|
||||
Das Modul implementiert
|
||||
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
|
||||
[ausschliesslich den Nachrichtenvertrag `4.0`](../../Schnittstelle.md). Eine
|
||||
separate Manager-ID wird nicht konfiguriert. Das Modul akzeptiert Vorgaben nur
|
||||
von einem Manager, in dessen manueller oder automatischer
|
||||
Verbraucherzuordnung die Ladestation aktiv eingetragen ist.
|
||||
|
||||
An den Manager werden neben den gemeinsamen Feldern diese Zustaende gemeldet:
|
||||
|
||||
- `Sollleistung_W`
|
||||
- `FahrzeugVerbunden`
|
||||
- `FahrzeugGeladen`
|
||||
- `Fahrzeugstatus`
|
||||
- `Phasenzahl`
|
||||
- `Ladestrom_A`
|
||||
- `Ladefreigabe`
|
||||
- `Solarladen`
|
||||
- `Ladefehler` mit Stoertext
|
||||
|
||||
Eine Sollleistung wird nur angenommen, wenn sie im zuletzt berechneten
|
||||
Leistungsangebot enthalten ist. Nach `VorgabeTimeout` ohne Erneuerung wird sie
|
||||
ungueltig. Statusaenderungen werden kurz verzoegert und zusaetzlich alle
|
||||
`Meldeintervall` Sekunden an alle zugeordneten Manager gemeldet.
|
||||
|
||||
## Instanzstatus und Fehlerbehandlung
|
||||
|
||||
| Status | Bedeutung |
|
||||
| ---: | --- |
|
||||
| `102` | Konfiguration und letzte Geraetekommunikation sind gueltig. |
|
||||
| `201` | Konfiguration ungueltig, beispielsweise fehlende Adresse oder Pico-Zugangsdaten. |
|
||||
| `202` | Geraetekommunikation oder Antwortauswertung fehlgeschlagen. |
|
||||
|
||||
Bei einem Fehler meldet das Modul `Verfuegbar=false`,
|
||||
`AenderungMoeglich=false` und setzt `Stoerung` sowie `Stoertext`. HTTP-Anfragen
|
||||
verwenden 5 Sekunden Verbindungs- und 10 Sekunden Gesamt-Timeout. Antworten ab
|
||||
HTTP-Status 400 sowie unvollstaendige oder ungueltige JSON-Antworten gelten als
|
||||
Kommunikationsfehler.
|
||||
|
||||
## Installation und Inbetriebnahme
|
||||
|
||||
1. Im IP-Symcon Module Control den Testing-Branch `develop` der Bibliothek
|
||||
`https://git.belevo.ch/ENELIX/Enelix-EMS.git` installieren oder
|
||||
aktualisieren.
|
||||
2. Unter **Instanz hinzufuegen** nach **Ladestation Stand-Alone** suchen und
|
||||
eine Instanz anlegen.
|
||||
3. Den Geraetetyp auswaehlen und die dazugehoerigen Verbindungsdaten eintragen:
|
||||
bei go-e nur IP-Adresse oder Hostname, bei Pico Geraete-ID, Seriennummer,
|
||||
Benutzername und Passwort.
|
||||
4. Den maximal zulaessigen Ladestrom der Installation zwischen 6 und 32 A
|
||||
einstellen. Diese Grenze ersetzt keine elektrische Absicherung.
|
||||
5. `Abfrageintervall`, `Meldeintervall`, `VorgabeTimeout` sowie die beiden
|
||||
Prioritaeten festlegen.
|
||||
6. Die gewuenschten Startwerte fuer `Ladefreigabe` und `Solarladen` setzen.
|
||||
7. Fuer die Erstinbetriebnahme `EinstellungenInVisu`,
|
||||
`DiagnosevariablenAnzeigen` und bei Bedarf `LoggingEin` aktivieren.
|
||||
8. Die Instanz im Manager manuell aktiv zuordnen oder bei automatischer Suche
|
||||
in der gefundenen Liste aktivieren.
|
||||
9. Zuerst ohne Fahrzeug kontrollieren, ob die Statusabfrage fehlerfrei ist.
|
||||
Danach unter Aufsicht ein Fahrzeug verbinden und `Aktiv` einschalten.
|
||||
|
||||
Fuer go-e muss IP-Symcon das Geraet im lokalen Netz per HTTP erreichen koennen.
|
||||
Fuer Pico ist ausgehender HTTPS-Zugriff auf `api.smart-me.com` erforderlich.
|
||||
Passwoerter gehoeren ausschliesslich in das dafuer vorgesehene Passwortfeld und
|
||||
niemals in Repository-Dateien, Skripte oder Screenshots.
|
||||
|
||||
## Abnahmecheckliste
|
||||
|
||||
- Die Instanz erreicht Status `102`, und `Stoerung` bleibt `false`.
|
||||
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
|
||||
Leistungsangebot `[0]`.
|
||||
- Nach dem Anstecken wird das Fahrzeug erkannt und die gemessene Leistung
|
||||
plausibel in `Istleistung` und `Ladestrom` abgebildet.
|
||||
- Bei einer Ladeleistung bis 7500 W wird eine Phase, darueber werden drei
|
||||
Phasen angezeigt.
|
||||
- `Aktiv=false` oder `Ladefreigabe=false` stoppt die Ladestation.
|
||||
- PV mit `Solarladen=true` bietet `0 W` und die Ladestufen an.
|
||||
- PV mit `Solarladen=false` bietet nur die maximale Leistung an.
|
||||
- Peak mit `Solarladen=true` stoppt die Ladestation und bietet nur `[0]` an.
|
||||
- Peak mit `Solarladen=false` bietet `0 W` und die Ladestufen an.
|
||||
- Eine Manager-Vorgabe ausserhalb des gemeldeten Angebots wird abgewiesen.
|
||||
- Nach Ablauf des Vorgabe-Timeouts gilt wieder das lokale Ersatzverhalten.
|
||||
- Im Debug-Protokoll und in `LetzterGeraetebefehl` erscheinen keine
|
||||
Zugangsdaten.
|
||||
|
||||
## Fehlersuche
|
||||
|
||||
- Status `201`: Geraetetyp und Pflichtfelder kontrollieren. Bei go-e darf die
|
||||
Adresse kein Protokoll, keinen Pfad und keine Parameter enthalten. Bei Pico
|
||||
muessen alle vier Zugangsfelder befuellt sein.
|
||||
- Status `202`: Erreichbarkeit, DNS, lokale Firewall und API-Antwort pruefen.
|
||||
`Stoertext` enthaelt den konkreten Kommunikations- oder JSON-Fehler.
|
||||
- Fahrzeug wird nicht erkannt: Rohstatus der Geraete-API kontrollieren. Der
|
||||
Adapter erwartet bei go-e `car` und `nrg[11]`, bei Pico `State` und
|
||||
`ActiveChargingPower`.
|
||||
- Falsche Phasenzahl bei sehr kleiner oder stehender Ladung: Die kompatible
|
||||
Enelix-1-Erkennung basiert auf der momentanen Leistung. Fuer eine sichere
|
||||
Dreiphasenerkennung muss waehrend der Erkennung mehr als 7500 W anliegen.
|
||||
- Manager-Vorgabe wird abgewiesen: aktive Zuordnung im Manager sowie
|
||||
`LeistungsangebotDiagnose` und die Betriebsart kontrollieren.
|
||||
|
||||
## Tests
|
||||
|
||||
Die Adaptertests verwenden einen injizierten Fake-HTTP-Transport. Dadurch
|
||||
werden fuer alle drei Geraetevarianten Statusantworten, URL, HTTP-Methode,
|
||||
Authentisierung und Steueraufrufe geprueft, ohne ein reales Geraet anzusprechen.
|
||||
Der Symcon-Funktionstest nutzt den internen, nicht im Konfigurationsformular
|
||||
sichtbaren `Testmodus` und prueft zusaetzlich Fahrzeug-/Phasenerkennung,
|
||||
die vier PV-/Peak-Angebote sowie die Umschaltung ueber den `Solarladen`-Button.
|
||||
Die Unit- und Adaptertests verwenden einen injizierten Fake-HTTP-Transport.
|
||||
Damit werden fuer go-e alt, go-e Gemini und smart-me Pico Statusantworten,
|
||||
URL, HTTP-Methode, Authentisierung und Steueraufrufe ohne reale Hardware
|
||||
geprueft.
|
||||
|
||||
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
|
||||
API-Antworten. Er prueft alle drei Geraetevarianten, Fahrzeug- und
|
||||
Phasenerkennung, die Leistungsangebote in PV und Peak sowie die Umschaltung
|
||||
ueber den `Solarladen`-Button.
|
||||
|
||||
PHP-Syntax, Struktur- und Unit-Tests des gesamten Repositorys:
|
||||
|
||||
```bash
|
||||
composer check
|
||||
```
|
||||
|
||||
Funktionstest nur fuer die Ladestation gegen IP-Symcon 8:
|
||||
|
||||
```bash
|
||||
tests/Symcon/bin/run-symcon-tests.sh single LadestationStandAlone
|
||||
```
|
||||
|
||||
Vollstaendiger Symcon-Modultestlauf:
|
||||
|
||||
```bash
|
||||
tests/Symcon/bin/run-symcon-tests.sh all
|
||||
```
|
||||
|
||||
Aufbau, Testvertrag und Ergebnisdateien sind unter
|
||||
[`docs/testing/README.md`](../../testing/README.md) beschrieben.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# EMS-Module und Modulentwürfe
|
||||
|
||||
> Manager, Warmwassererwaermer, Batterie, Verbraucher 1-Stufig, Waermepumpe,
|
||||
> Ladestation Stand-Alone, Easee Gateway und Ladestation Gateway sind als
|
||||
> installierbare IP-Symcon-Module umgesetzt. Die weiteren Ordner enthalten Besprechungsgrundlagen.
|
||||
> Manager, Warmwassererwaermer, Pufferspeicher, Batterie, Verbraucher 1-Stufig,
|
||||
> Waermepumpe, Ladestation Stand-Alone, Easee Gateway und Ladestation Gateway
|
||||
> sind als installierbare IP-Symcon-Module umgesetzt. Die weiteren Ordner
|
||||
> enthalten Besprechungsgrundlagen.
|
||||
|
||||
Alle steuerbaren Verbraucher verwenden die gemeinsamen Datenpunkte aus der
|
||||
[EMS-Schnittstelle](../Schnittstelle.md). In den Modul-READMEs stehen deshalb
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
Die Demo erzeugt idempotent eine vollständige, spielbare EMS-Anlage in IP-Symcon 8.x:
|
||||
|
||||
- Enelix Manager mit manueller Zuordnung,
|
||||
- Enelix Manager mit manueller Zuordnung von fünf steuerbaren Teilnehmern,
|
||||
- zwei mehrstufige Boiler (1,2/2,4 kW und 1,5/3,0 kW),
|
||||
- einen einstufigen Entfeuchter (0,9 kW),
|
||||
- simulierte PV-Erzeugung, Grundlast, Netzleistung und Boilertemperaturen,
|
||||
- native Symcon-Instanz `Energy Distribution` für den Energiefluss,
|
||||
- eigene Kachelansichten mit allen Laufzeitparametern je Boiler,
|
||||
- kombinierten Verlauf für PV-Leistung, Hausverbrauch und Netzleistung,
|
||||
- einen zweistufigen Pufferspeicher (1,8/3,6 kW) mit Heizkurve,
|
||||
- einen bidirektionalen Batteriespeicher (10 kWh, +/-3,5 kW),
|
||||
- simulierte PV-Erzeugung, variable Bewölkung, Tageslastgang, Zusatzlast,
|
||||
Außentemperatur, Wärmeverluste, Netzleistung und Speicherzustände,
|
||||
- native Symcon-Instanz `Energy Distribution` für acht Energieflussknoten,
|
||||
- eigene Kachelansichten mit Laufzeitparametern für Boiler, Puffer und Batterie,
|
||||
- kombinierten Verlauf für PV-Leistung, Hausverbrauch, EMS-Leistung und Netzleistung,
|
||||
- kombinierten Verlauf der kumulierten PV-, Haus-, Bezugs- und Einspeiseenergie,
|
||||
- Archivierung der Leistungs-, Energie- und Temperaturwerte.
|
||||
|
||||
@@ -27,26 +30,43 @@ print_r($result);
|
||||
Ein weiterer Lauf aktualisiert dieselbe Anlage anhand stabiler Idents und erzeugt keine Duplikate.
|
||||
Bestehende, nicht zur Demo gehörende Objekte werden nicht verändert oder gelöscht.
|
||||
|
||||
Der Bootstrap hinterlegt absichtlich keinen Lizenzcode und überschreibt eine
|
||||
bereits konfigurierte Manager-Lizenz nicht. Bei einer neuen Anlage wird die
|
||||
Struktur vollständig erstellt; der Manager bleibt bis zur regulären
|
||||
Lizenzaktivierung im Konfigurationsformular auf Status 203. Solange der Manager
|
||||
nicht freigegeben ist, bleiben seine Zuordnung und die Verbraucher deaktiviert,
|
||||
damit keine Lizenzfehler protokolliert werden. Nach der Aktivierung den
|
||||
Bootstrap erneut ausführen; er ordnet dann alle fünf Teilnehmer zu und startet
|
||||
die Regelung.
|
||||
|
||||
## Bedienung
|
||||
|
||||
In der Kachelvisualisierung öffnet die Startkategorie `Enelix 2 Demoanlage`.
|
||||
Unter `Simulation` lassen sich Tageslauf, Tageszeit, Geschwindigkeit,
|
||||
PV-Spitzenleistung, Bewölkung und Haus-Grundlast verändern. Der Manager verteilt
|
||||
den verfügbaren PV-Überschuss nach Priorität auf beide Boiler und den Entfeuchter.
|
||||
dynamische Profile, PV-Spitzenleistung, mittlere Bewölkung, Haus-Grundlast und
|
||||
eine ungeregelte Zusatzlast verändern. Bei aktivierten dynamischen Profilen
|
||||
entstehen reproduzierbare Morgen- und Abendspitzen, ziehende Bewölkung und ein
|
||||
Tagesgang der Außentemperatur. Der Manager verteilt den verfügbaren
|
||||
PV-Überschuss nach Priorität auf beide Boiler, Entfeuchter, Pufferspeicher und
|
||||
Batterie.
|
||||
|
||||
`Übersicht` zeigt die native Symcon-Energieverteilung. Die Kacheln `Boiler 1`
|
||||
und `Boiler 2` öffnen direkt sämtliche Bedien-, Status- und Diagnosevariablen,
|
||||
ohne zuerst das Instanz-Konfigurationsformular zu zeigen. Unter `Leistung und
|
||||
Energie` liegen die beiden kombinierten Zeitdiagramme und die Energiezähler.
|
||||
Die Energiesummen verwenden die beschleunigte Simulationszeit.
|
||||
`Übersicht` zeigt die native Symcon-Energieverteilung. Die Kacheln `Boiler 1`,
|
||||
`Boiler 2`, `Pufferspeicher` und `Batteriespeicher` öffnen direkt die jeweiligen
|
||||
Bedien-, Status- und Diagnosevariablen, ohne zuerst das
|
||||
Instanz-Konfigurationsformular zu zeigen. Positive Batterieleistung bedeutet
|
||||
Laden, negative Leistung Entladen. Unter `Leistung und Energie` liegen die
|
||||
beiden kombinierten Zeitdiagramme und die Energiezähler. Die Energiesummen,
|
||||
Temperaturmodelle und der Ladezustand verwenden die beschleunigte
|
||||
Simulationszeit.
|
||||
|
||||
## Validierung
|
||||
|
||||
- Alle vier Enelix-Instanzen müssen Status 102 besitzen.
|
||||
- Der Manager muss drei Verbraucher melden.
|
||||
- Bei ausreichendem PV-Überschuss müssen Verbraucher schalten.
|
||||
- Netzleistung, PV-Leistung, Hausverbrauch, Verbraucherleistungen, Energiesummen und Temperaturen werden archiviert.
|
||||
- Die native Energieverteilung muss sechs Knoten enthalten.
|
||||
- Die fünf Verbraucher müssen Status 102 besitzen; der Manager nach Lizenzaktivierung ebenfalls (vorher erwarteter Status 203).
|
||||
- Der Manager muss fünf Teilnehmer aus vier unterschiedlichen Modultypen melden.
|
||||
- Bei ausreichendem PV-Überschuss müssen thermische Verbraucher schalten und die Batterie laden.
|
||||
- Puffertemperatur, Außentemperatur und Batterie-Ladezustand müssen sich über den Tageslauf verändern.
|
||||
- Netzleistung, PV-Leistung, Hausverbrauch, EMS-Leistung, Einzelverbräuche, Speicherleistung, Energiesummen und Temperaturen werden archiviert.
|
||||
- Die native Energieverteilung muss acht Knoten enthalten.
|
||||
- Beide Diagrammkacheln müssen mindestens drei beschriftete Zeitreihen anzeigen.
|
||||
- Ein erneuter Installationslauf muss dieselbe Root-ID zurückgeben.
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ const ENELIX_DEMO_ROOT_IDENT = 'ENELIX_DEMO_2';
|
||||
const ENELIX_DEMO_MANAGER_MODULE = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
||||
const ENELIX_DEMO_BOILER_MODULE = '{B7C54AF4-AD7D-4FE4-B75D-203693906251}';
|
||||
const ENELIX_DEMO_SINGLE_MODULE = '{15879A4E-D0C2-4495-83DE-46E1E462591E}';
|
||||
const ENELIX_DEMO_BUFFER_MODULE = '{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}';
|
||||
const ENELIX_DEMO_BATTERY_MODULE = '{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}';
|
||||
const ENELIX_DEMO_DUMMY_MODULE = '{485D0419-BE97-4548-AA9C-C083EB82E61E}';
|
||||
const ENELIX_DEMO_TILE_MODULE = '{B5B875BB-9B76-45FD-4E67-2607E45B3AC4}';
|
||||
const ENELIX_DEMO_ARCHIVE_MODULE = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
|
||||
@@ -28,15 +30,17 @@ function enelixDemoInstall(): array
|
||||
|
||||
enelixDemoEnsureProfiles();
|
||||
|
||||
$legacyFlow = enelixDemoEnsureVariable($dashboard, 'Energiefluss', 'Energiefluss (Legacy)', 3, '~HTMLBox', '', 90);
|
||||
$legacyFlow = enelixDemoEnsureVariable($dashboard, 'Energiefluss', 'Energiefluss (Legacy)', 3, '~HTMLBox', '', 900);
|
||||
IPS_SetHidden($legacyFlow, true);
|
||||
$pvDisplay = enelixDemoEnsureVariable($dashboard, 'PVLeistung', 'PV-Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 20);
|
||||
$houseDisplay = enelixDemoEnsureVariable($dashboard, 'Hausverbrauch', 'Hausverbrauch gesamt', 2, 'ENELIX.Demo.PowerPositive', 0.0, 30);
|
||||
$loadDisplay = enelixDemoEnsureVariable($dashboard, 'Verbraucherleistung', 'Geregelte Verbraucher', 2, 'ENELIX.Demo.PowerPositive', 0.0, 40);
|
||||
$loadDisplay = enelixDemoEnsureVariable($dashboard, 'Verbraucherleistung', 'EMS-Leistung inkl. Speicher', 2, 'ENELIX.Demo.Power', 0.0, 40);
|
||||
$grid = enelixDemoEnsureVariable($dashboard, 'Netzleistung', 'Netzleistung', 2, 'ENELIX.Demo.Power', 0.0, 50);
|
||||
$boiler1Power = enelixDemoEnsureVariable($dashboard, 'Boiler1Leistung', 'Boiler 1 Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 60);
|
||||
$boiler2Power = enelixDemoEnsureVariable($dashboard, 'Boiler2Leistung', 'Boiler 2 Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 70);
|
||||
$dryerPower = enelixDemoEnsureVariable($dashboard, 'EntfeuchterLeistung', 'Entfeuchter Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 80);
|
||||
$bufferPower = enelixDemoEnsureVariable($dashboard, 'PufferLeistung', 'Pufferspeicher Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 90);
|
||||
$batteryPower = enelixDemoEnsureVariable($dashboard, 'BatterieLeistung', 'Batterieleistung', 2, 'ENELIX.Demo.Power', 0.0, 100);
|
||||
|
||||
$powerChart = enelixDemoEnsureVariable($history, 'Leistungsdiagramm', 'Leistungsverlauf (Legacy)', 3, '~HTMLBox', '', 90);
|
||||
$energyChart = enelixDemoEnsureVariable($history, 'Energiediagramm', 'Kumulierte Energien (Legacy)', 3, '~HTMLBox', '', 100);
|
||||
@@ -50,9 +54,11 @@ function enelixDemoInstall(): array
|
||||
$auto = enelixDemoEnsureVariable($simulation, 'Automatik', 'Tageslauf automatisch', 0, '~Switch', true, 10);
|
||||
$hour = enelixDemoEnsureVariable($simulation, 'Tageszeit', 'Simulierte Tageszeit', 2, 'ENELIX.Demo.Hour', 8.0, 20);
|
||||
$speed = enelixDemoEnsureVariable($simulation, 'Geschwindigkeit', 'Minuten je Schritt', 1, 'ENELIX.Demo.Speed', 10, 30);
|
||||
$peak = enelixDemoEnsureVariable($simulation, 'PVSpitze', 'PV-Spitzenleistung', 2, 'ENELIX.Demo.PowerPositive', 7000.0, 40);
|
||||
$cloud = enelixDemoEnsureVariable($simulation, 'Bewoelkung', 'Bewölkung', 1, 'ENELIX.Demo.Percent', 20, 50);
|
||||
$base = enelixDemoEnsureVariable($simulation, 'Grundlast', 'Haus-Grundlast', 2, 'ENELIX.Demo.PowerPositive', 900.0, 60);
|
||||
$dynamic = enelixDemoEnsureVariable($simulation, 'DynamischeProfile', 'Dynamische Tagesprofile', 0, '~Switch', true, 40);
|
||||
$peak = enelixDemoEnsureVariable($simulation, 'PVSpitze', 'PV-Spitzenleistung', 2, 'ENELIX.Demo.PowerPositive', 9000.0, 50);
|
||||
$cloud = enelixDemoEnsureVariable($simulation, 'Bewoelkung', 'Mittlere Bewölkung', 1, 'ENELIX.Demo.Percent', 20, 60);
|
||||
$base = enelixDemoEnsureVariable($simulation, 'Grundlast', 'Mittlere Haus-Grundlast', 2, 'ENELIX.Demo.PowerPositive', 900.0, 70);
|
||||
$extraLoad = enelixDemoEnsureVariable($simulation, 'Zusatzlast', 'Ungeregelte Zusatzlast', 2, 'ENELIX.Demo.PowerPositive', 0.0, 80);
|
||||
|
||||
$actorScript = enelixDemoEnsureScript(
|
||||
$technical,
|
||||
@@ -61,6 +67,13 @@ function enelixDemoInstall(): array
|
||||
"<?php\nSetValue((int) \$_IPS['VARIABLE'], (bool) \$_IPS['VALUE']);\n",
|
||||
10
|
||||
);
|
||||
$registerScript = enelixDemoEnsureScript(
|
||||
$technical,
|
||||
'RegisterAktion',
|
||||
'Simulierte Register',
|
||||
"<?php\nSetValue((int) \$_IPS['VARIABLE'], \$_IPS['VALUE']);\n",
|
||||
15
|
||||
);
|
||||
$tickScript = enelixDemoEnsureScript(
|
||||
$technical,
|
||||
'SimulationTick',
|
||||
@@ -76,7 +89,7 @@ function enelixDemoInstall(): array
|
||||
30
|
||||
);
|
||||
|
||||
foreach ([$auto, $hour, $speed, $peak, $cloud, $base] as $variableId) {
|
||||
foreach ([$auto, $hour, $speed, $dynamic, $peak, $cloud, $base, $extraLoad] as $variableId) {
|
||||
IPS_SetVariableCustomAction($variableId, $controlScript);
|
||||
}
|
||||
|
||||
@@ -84,36 +97,57 @@ function enelixDemoInstall(): array
|
||||
|
||||
$temp1 = enelixDemoEnsureVariable($technical, 'Boiler1Temperatur', 'Boiler 1 Temperatur', 2, '~Temperature', 49.0, 100);
|
||||
$temp2 = enelixDemoEnsureVariable($technical, 'Boiler2Temperatur', 'Boiler 2 Temperatur', 2, '~Temperature', 47.0, 110);
|
||||
$boiler11 = enelixDemoEnsureVariable($technical, 'Boiler1Stufe1', 'Boiler 1 Stufe 1', 0, '~Switch', false, 120);
|
||||
$boiler12 = enelixDemoEnsureVariable($technical, 'Boiler1Stufe2', 'Boiler 1 Stufe 2', 0, '~Switch', false, 130);
|
||||
$boiler21 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe1', 'Boiler 2 Stufe 1', 0, '~Switch', false, 140);
|
||||
$boiler22 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe2', 'Boiler 2 Stufe 2', 0, '~Switch', false, 150);
|
||||
$dryContact = enelixDemoEnsureVariable($technical, 'EntfeuchterKontakt', 'Entfeuchter Kontakt', 0, '~Switch', false, 160);
|
||||
$chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 170);
|
||||
$bufferTemp = enelixDemoEnsureVariable($technical, 'PufferTemperatur', 'Puffertemperatur', 2, '~Temperature', 36.0, 120);
|
||||
$outsideTemp = enelixDemoEnsureVariable($technical, 'AussenTemperatur', 'Aussentemperatur', 2, '~Temperature', 6.0, 130);
|
||||
$boiler11 = enelixDemoEnsureVariable($technical, 'Boiler1Stufe1', 'Boiler 1 Stufe 1', 0, '~Switch', false, 200);
|
||||
$boiler12 = enelixDemoEnsureVariable($technical, 'Boiler1Stufe2', 'Boiler 1 Stufe 2', 0, '~Switch', false, 210);
|
||||
$boiler21 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe1', 'Boiler 2 Stufe 1', 0, '~Switch', false, 220);
|
||||
$boiler22 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe2', 'Boiler 2 Stufe 2', 0, '~Switch', false, 230);
|
||||
$dryContact = enelixDemoEnsureVariable($technical, 'EntfeuchterKontakt', 'Entfeuchter Kontakt', 0, '~Switch', false, 240);
|
||||
$buffer1 = enelixDemoEnsureVariable($technical, 'PufferStufe1', 'Puffer Stufe 1', 0, '~Switch', false, 250);
|
||||
$buffer2 = enelixDemoEnsureVariable($technical, 'PufferStufe2', 'Puffer Stufe 2', 0, '~Switch', false, 260);
|
||||
$batteryMaxCharge = enelixDemoEnsureVariable($technical, 'BatterieMaxLaden', 'Batterie max. laden', 2, 'ENELIX.Demo.PowerPositive', 3500.0, 300);
|
||||
$batteryMaxDischarge = enelixDemoEnsureVariable($technical, 'BatterieMaxEntladen', 'Batterie max. entladen', 2, 'ENELIX.Demo.PowerPositive', 3500.0, 310);
|
||||
$batterySoc = enelixDemoEnsureVariable($technical, 'BatterieLadezustand', 'Batterie Ladezustand', 2, 'ENELIX.Demo.PercentFloat', 52.0, 320);
|
||||
$batteryActual = enelixDemoEnsureVariable($technical, 'BatterieIstleistung', 'Batterie Istleistung', 2, 'ENELIX.Demo.Power', 0.0, 330);
|
||||
$batteryManagement = enelixDemoEnsureVariable($technical, 'BatterieManagement', 'Batterie Managementregister', 1, '', 0, 340);
|
||||
$batteryMode = enelixDemoEnsureVariable($technical, 'BatterieModus', 'Batterie Modusregister', 1, '', 0, 350);
|
||||
$batteryRegisterPower = enelixDemoEnsureVariable($technical, 'BatterieRegisterLeistung', 'Batterie Leistungsregister', 2, 'ENELIX.Demo.PowerPositive', 0.0, 360);
|
||||
$chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 390);
|
||||
IPS_SetHidden($chartUpdated, true);
|
||||
|
||||
$flowPv = enelixDemoEnsureVariable($technical, 'EnergieflussPVkW', 'PV-Leistung', 2, '~Power', 0.0, 300);
|
||||
$flowGrid = enelixDemoEnsureVariable($technical, 'EnergieflussNetzkW', 'Netzleistung', 2, '~Power', 0.0, 310);
|
||||
$flowBase = enelixDemoEnsureVariable($technical, 'EnergieflussGrundlastkW', 'Haus-Grundlast', 2, '~Power', 0.0, 320);
|
||||
$flowBoiler1 = enelixDemoEnsureVariable($technical, 'EnergieflussBoiler1kW', 'Boiler 1', 2, '~Power', 0.0, 330);
|
||||
$flowBoiler2 = enelixDemoEnsureVariable($technical, 'EnergieflussBoiler2kW', 'Boiler 2', 2, '~Power', 0.0, 340);
|
||||
$flowDryer = enelixDemoEnsureVariable($technical, 'EnergieflussEntfeuchterkW', 'Entfeuchter', 2, '~Power', 0.0, 350);
|
||||
$flowHouse = enelixDemoEnsureVariable($technical, 'DiagrammHausverbrauchkW', 'Hausverbrauch gesamt', 2, '~Power', 0.0, 360);
|
||||
$flowLoads = enelixDemoEnsureVariable($technical, 'DiagrammVerbraucherkW', 'Geregelte Verbraucher', 2, '~Power', 0.0, 370);
|
||||
$flowPv = enelixDemoEnsureVariable($technical, 'EnergieflussPVkW', 'PV-Leistung', 2, '~Power', 0.0, 500);
|
||||
$flowGrid = enelixDemoEnsureVariable($technical, 'EnergieflussNetzkW', 'Netzleistung', 2, '~Power', 0.0, 510);
|
||||
$flowBase = enelixDemoEnsureVariable($technical, 'EnergieflussGrundlastkW', 'Haus-Grundlast', 2, '~Power', 0.0, 520);
|
||||
$flowBoiler1 = enelixDemoEnsureVariable($technical, 'EnergieflussBoiler1kW', 'Boiler 1', 2, '~Power', 0.0, 530);
|
||||
$flowBoiler2 = enelixDemoEnsureVariable($technical, 'EnergieflussBoiler2kW', 'Boiler 2', 2, '~Power', 0.0, 540);
|
||||
$flowDryer = enelixDemoEnsureVariable($technical, 'EnergieflussEntfeuchterkW', 'Entfeuchter', 2, '~Power', 0.0, 550);
|
||||
$flowBuffer = enelixDemoEnsureVariable($technical, 'EnergieflussPufferkW', 'Pufferspeicher', 2, '~Power', 0.0, 560);
|
||||
$flowBattery = enelixDemoEnsureVariable($technical, 'EnergieflussBatteriekW', 'Batteriespeicher', 2, '~Power', 0.0, 570);
|
||||
$flowHouse = enelixDemoEnsureVariable($technical, 'DiagrammHausverbrauchkW', 'Hausverbrauch gesamt', 2, '~Power', 0.0, 580);
|
||||
$flowLoads = enelixDemoEnsureVariable($technical, 'DiagrammVerbraucherkW', 'EMS-Leistung inkl. Speicher', 2, '~Power', 0.0, 590);
|
||||
foreach ([
|
||||
$flowPv, $flowGrid, $flowBase, $flowBoiler1,
|
||||
$flowBoiler2, $flowDryer, $flowHouse, $flowLoads,
|
||||
$flowPv, $flowGrid, $flowBase, $flowBoiler1, $flowBoiler2,
|
||||
$flowDryer, $flowBuffer, $flowBattery, $flowHouse, $flowLoads,
|
||||
] as $flowVariableId) {
|
||||
IPS_SetHidden($flowVariableId, true);
|
||||
}
|
||||
|
||||
foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact] as $actorId) {
|
||||
foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact, $buffer1, $buffer2] as $actorId) {
|
||||
IPS_SetVariableCustomAction($actorId, $actorScript);
|
||||
}
|
||||
foreach ([$batteryManagement, $batteryMode, $batteryRegisterPower] as $registerId) {
|
||||
IPS_SetVariableCustomAction($registerId, $registerScript);
|
||||
}
|
||||
foreach ([$temp1, $temp2, $bufferTemp, $outsideTemp, $batteryMaxCharge, $batteryMaxDischarge, $batterySoc, $batteryActual] as $sensorId) {
|
||||
SetValue($sensorId, GetValue($sensorId));
|
||||
}
|
||||
|
||||
$boiler1 = enelixDemoEnsureInstance($ems, 'Boiler1', 'Boiler 1 · 300 Liter', ENELIX_DEMO_BOILER_MODULE, 20);
|
||||
$boiler2 = enelixDemoEnsureInstance($ems, 'Boiler2', 'Boiler 2 · 500 Liter', ENELIX_DEMO_BOILER_MODULE, 30);
|
||||
$dryer = enelixDemoEnsureInstance($ems, 'Entfeuchter', 'Entfeuchter · 1-stufig', ENELIX_DEMO_SINGLE_MODULE, 40);
|
||||
$buffer = enelixDemoEnsureInstance($ems, 'Pufferspeicher', 'Pufferspeicher · Heizkurve', ENELIX_DEMO_BUFFER_MODULE, 50);
|
||||
$battery = enelixDemoEnsureInstance($ems, 'Batterie', 'Batteriespeicher · 10 kWh', ENELIX_DEMO_BATTERY_MODULE, 60);
|
||||
|
||||
enelixDemoConfigure($boiler1, [
|
||||
'Boilerfuehler_PT1' => $temp1,
|
||||
@@ -157,6 +191,52 @@ function enelixDemoInstall(): array
|
||||
'Mindestausschaltdauer' => 2,
|
||||
'Mindestlaufzeit' => 0,
|
||||
'PrioritaetPV' => 2,
|
||||
'PrioritaetPeak' => 3,
|
||||
'Meldeintervall' => 5,
|
||||
'VorgabeTimeout' => 30,
|
||||
'EinstellungenInVisu' => true,
|
||||
'DiagnosevariablenAnzeigen' => true,
|
||||
]);
|
||||
enelixDemoConfigure($buffer, [
|
||||
'LeistungsStufen' => json_encode([
|
||||
['Stufe' => 1, 'Leistung' => 1800, 'Schaltkontakt_Stufe' => $buffer1],
|
||||
['Stufe' => 2, 'Leistung' => 3600, 'Schaltkontakt_Stufe' => $buffer2],
|
||||
], JSON_THROW_ON_ERROR),
|
||||
'PufferfuehlerVariableID' => $bufferTemp,
|
||||
'AussentemperaturVariableID' => $outsideTemp,
|
||||
'FusspunktVorlauftemperatur' => 34.0,
|
||||
'HeizkurvenSteigung' => 0.8,
|
||||
'HeizkurveMinimaltemperatur' => 28.0,
|
||||
'HeizkurveMaximaltemperatur' => 55.0,
|
||||
'Hysterese' => 4.0,
|
||||
'MindesttemperaturModus' => 0,
|
||||
'LastwechselSperrzeit' => 2,
|
||||
'TemperaturMaxAlter' => 30,
|
||||
'PuffertemperaturGlaetten' => false,
|
||||
'PrioritaetPV' => 3,
|
||||
'PrioritaetPeak' => 4,
|
||||
'Meldeintervall' => 5,
|
||||
'VorgabeTimeout' => 30,
|
||||
'EinstellungenInVisu' => true,
|
||||
'DiagnosevariablenAnzeigen' => true,
|
||||
]);
|
||||
enelixDemoConfigure($battery, [
|
||||
'Batterietyp' => 2,
|
||||
'Batteriemanagement' => 2,
|
||||
'MaxLadeleistungVariableID' => $batteryMaxCharge,
|
||||
'MaxEntladeleistungVariableID' => $batteryMaxDischarge,
|
||||
'LadezustandVariableID' => $batterySoc,
|
||||
'NetzleistungVariableID' => $grid,
|
||||
'IstleistungVariableID' => $batteryActual,
|
||||
'ManagementRegisterVariableID' => $batteryManagement,
|
||||
'ModusRegisterVariableID' => $batteryMode,
|
||||
'LeistungsRegisterVariableID' => $batteryRegisterPower,
|
||||
'ReserveLadezustand' => 20.0,
|
||||
'MindestLadezustand' => 10.0,
|
||||
'LadezustandHysterese' => 3.0,
|
||||
'MesswertMaxAlter' => 30,
|
||||
'Aenderungssperre' => 0,
|
||||
'PrioritaetPV' => 4,
|
||||
'PrioritaetPeak' => 0,
|
||||
'Meldeintervall' => 5,
|
||||
'VorgabeTimeout' => 30,
|
||||
@@ -178,6 +258,8 @@ function enelixDemoInstall(): array
|
||||
enelixDemoEnergyNode($flowBoiler1, 'Boiler 1', 'consumer', $temp1),
|
||||
enelixDemoEnergyNode($flowBoiler2, 'Boiler 2', 'consumer', $temp2),
|
||||
enelixDemoEnergyNode($flowDryer, 'Entfeuchter', 'consumer'),
|
||||
enelixDemoEnergyNode($flowBuffer, 'Pufferspeicher', 'consumer', $bufferTemp),
|
||||
enelixDemoEnergyNode($flowBattery, 'Batteriespeicher', 'battery', $batterySoc),
|
||||
]);
|
||||
IPS_SetHidden($energyDistribution, false);
|
||||
$oldEnergyDistribution = enelixDemoFind($dashboard, 'Energieverteilung');
|
||||
@@ -188,34 +270,47 @@ function enelixDemoInstall(): array
|
||||
foreach ([$boiler1, $boiler2] as $boilerId) {
|
||||
IPS_RequestAction($boilerId, 'Mindesttemperatur', 42.0);
|
||||
IPS_RequestAction($boilerId, 'Maximaltemperatur', 62.0);
|
||||
IPS_RequestAction($boilerId, 'Aktiv', true);
|
||||
}
|
||||
IPS_RequestAction($dryer, 'Aktiv', true);
|
||||
|
||||
$manager = enelixDemoEnsureInstance($ems, 'Manager', 'Enelix Manager', ENELIX_DEMO_MANAGER_MODULE, 10);
|
||||
$assignment = array_map(
|
||||
static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true],
|
||||
[$boiler1, $boiler2, $dryer]
|
||||
[$boiler1, $boiler2, $dryer, $buffer, $battery]
|
||||
);
|
||||
enelixDemoConfigure($manager, [
|
||||
$managerProperties = [
|
||||
'NetzleistungVariableID' => $grid,
|
||||
'Netzleistungsfaktor' => 1.0,
|
||||
'MesswertMaxAlter' => 10,
|
||||
'AutomatischeSuche' => false,
|
||||
'SuchbereichID' => $root,
|
||||
'VerbraucherZuordnung' => json_encode($assignment, JSON_THROW_ON_ERROR),
|
||||
'VerbraucherZuordnung' => '[]',
|
||||
'KeepAlive' => 5,
|
||||
'VerbraucherTimeout' => 15,
|
||||
'SollwertSolarladen' => 0.0,
|
||||
'Umschaltdifferenz' => 0.0,
|
||||
'DiagnosevariablenAnzeigen' => true,
|
||||
'LoggingEin' => false,
|
||||
]);
|
||||
|
||||
foreach ([$boiler1, $boiler2, $dryer] as $consumerId) {
|
||||
IPS_RequestAction($consumerId, 'Melden', true);
|
||||
];
|
||||
enelixDemoConfigure($manager, $managerProperties, [102, 203]);
|
||||
$managerReady = (int) IPS_GetInstance($manager)['InstanceStatus'] === 102;
|
||||
if ($managerReady) {
|
||||
$managerProperties['VerbraucherZuordnung'] = json_encode($assignment, JSON_THROW_ON_ERROR);
|
||||
enelixDemoConfigure($manager, $managerProperties, [102, 203]);
|
||||
$managerReady = (int) IPS_GetInstance($manager)['InstanceStatus'] === 102;
|
||||
}
|
||||
IPS_RequestAction($manager, 'Aktiv', true);
|
||||
if (!$managerReady && IPS_GetProperty($manager, 'VerbraucherZuordnung') !== '[]') {
|
||||
$managerProperties['VerbraucherZuordnung'] = '[]';
|
||||
enelixDemoConfigure($manager, $managerProperties, [203]);
|
||||
}
|
||||
|
||||
$consumerIds = [$boiler1, $boiler2, $dryer, $buffer, $battery];
|
||||
foreach ($consumerIds as $consumerId) {
|
||||
IPS_RequestAction($consumerId, 'Aktiv', $managerReady);
|
||||
if ($managerReady) {
|
||||
IPS_RequestAction($consumerId, 'Melden', true);
|
||||
}
|
||||
}
|
||||
IPS_RequestAction($manager, 'Aktiv', $managerReady);
|
||||
|
||||
$boiler1List = enelixDemoEnsureInstance(
|
||||
$dashboard,
|
||||
@@ -231,14 +326,32 @@ function enelixDemoInstall(): array
|
||||
ENELIX_DEMO_DUMMY_MODULE,
|
||||
120
|
||||
);
|
||||
enelixDemoConfigure($boiler1List, []);
|
||||
enelixDemoConfigure($boiler2List, []);
|
||||
$bufferList = enelixDemoEnsureInstance(
|
||||
$dashboard,
|
||||
'PufferListe',
|
||||
'Pufferspeicher',
|
||||
ENELIX_DEMO_DUMMY_MODULE,
|
||||
130
|
||||
);
|
||||
$batteryList = enelixDemoEnsureInstance(
|
||||
$dashboard,
|
||||
'BatterieListe',
|
||||
'Batteriespeicher',
|
||||
ENELIX_DEMO_DUMMY_MODULE,
|
||||
140
|
||||
);
|
||||
foreach ([$boiler1List, $boiler2List, $bufferList, $batteryList] as $listId) {
|
||||
enelixDemoConfigure($listId, []);
|
||||
}
|
||||
enelixDemoLinkInstanceVariables($boiler1List, $boiler1, $temp1);
|
||||
enelixDemoLinkInstanceVariables($boiler2List, $boiler2, $temp2);
|
||||
enelixDemoLinkInstanceVariables($bufferList, $buffer, $bufferTemp, 'Puffertemperatur', 'SensorPuffertemperatur');
|
||||
enelixDemoLinkInstanceVariables($batteryList, $battery, $batterySoc, 'Ladezustand', 'SensorLadezustand');
|
||||
IPS_SetHidden($boiler1View, true);
|
||||
IPS_SetHidden($boiler2View, true);
|
||||
IPS_SetHidden($boiler1, true);
|
||||
IPS_SetHidden($boiler2, true);
|
||||
foreach ([$boiler1, $boiler2, $buffer, $battery] as $hiddenInstanceId) {
|
||||
IPS_SetHidden($hiddenInstanceId, true);
|
||||
}
|
||||
|
||||
foreach (['Boiler1Link', 'Boiler2Link'] as $oldLinkIdent) {
|
||||
$oldLink = enelixDemoFind($dashboard, $oldLinkIdent);
|
||||
@@ -248,22 +361,25 @@ function enelixDemoInstall(): array
|
||||
}
|
||||
|
||||
enelixDemoEnsureLink($dashboard, 'ManagerLink', 'Manager', $manager, 100);
|
||||
enelixDemoEnsureLink($dashboard, 'EntfeuchterLink', 'Entfeuchter', $dryer, 130);
|
||||
enelixDemoEnsureLink($simulation, 'Boiler1TemperaturLink', 'Boiler 1 Temperatur', $temp1, 70);
|
||||
enelixDemoEnsureLink($simulation, 'Boiler2TemperaturLink', 'Boiler 2 Temperatur', $temp2, 80);
|
||||
enelixDemoEnsureLink($dashboard, 'EntfeuchterLink', 'Entfeuchter', $dryer, 150);
|
||||
enelixDemoEnsureLink($simulation, 'Boiler1TemperaturLink', 'Boiler 1 Temperatur', $temp1, 100);
|
||||
enelixDemoEnsureLink($simulation, 'Boiler2TemperaturLink', 'Boiler 2 Temperatur', $temp2, 110);
|
||||
enelixDemoEnsureLink($simulation, 'PufferTemperaturLink', 'Puffertemperatur', $bufferTemp, 120);
|
||||
enelixDemoEnsureLink($simulation, 'AussenTemperaturLink', 'Aussentemperatur', $outsideTemp, 130);
|
||||
enelixDemoEnsureLink($simulation, 'BatterieLadezustandLink', 'Batterie Ladezustand', $batterySoc, 140);
|
||||
|
||||
enelixDemoConfigureArchive([
|
||||
$pvDisplay, $houseDisplay, $loadDisplay, $grid,
|
||||
$boiler1Power, $boiler2Power, $dryerPower,
|
||||
$boiler1Power, $boiler2Power, $dryerPower, $bufferPower, $batteryPower,
|
||||
$pvEnergy, $houseEnergy, $gridImportEnergy, $gridExportEnergy,
|
||||
$temp1, $temp2,
|
||||
$temp1, $temp2, $bufferTemp, $outsideTemp, $batterySoc, $batteryActual,
|
||||
$flowPv, $flowGrid, $flowBase, $flowBoiler1, $flowBoiler2, $flowDryer,
|
||||
$flowHouse, $flowLoads,
|
||||
$flowBuffer, $flowBattery, $flowHouse, $flowLoads,
|
||||
]);
|
||||
enelixDemoEnsureChart($history, 'LeistungsdiagrammNativ', 'Leistungsverlauf', [
|
||||
enelixDemoChartDataset($flowPv, 'PV', '#f4c542', '~Power'),
|
||||
enelixDemoChartDataset($flowHouse, 'Hausverbrauch', '#2bb673', '~Power'),
|
||||
enelixDemoChartDataset($flowLoads, 'Geregelte Verbraucher', '#2786ac', '~Power'),
|
||||
enelixDemoChartDataset($flowLoads, 'EMS-Leistung inkl. Speicher', '#2786ac', '~Power'),
|
||||
enelixDemoChartDataset($flowGrid, 'Netzleistung', '#e95d5d', '~Power'),
|
||||
], 10);
|
||||
enelixDemoEnsureChart($history, 'EnergiediagrammNativ', 'Kumulierte Energien', [
|
||||
@@ -279,7 +395,7 @@ function enelixDemoInstall(): array
|
||||
'rootId' => $root,
|
||||
'visualizationId' => enelixDemoFirstInstance(ENELIX_DEMO_TILE_MODULE),
|
||||
'managerId' => $manager,
|
||||
'consumerIds' => [$boiler1, $boiler2, $dryer],
|
||||
'consumerIds' => $consumerIds,
|
||||
'tickScriptId' => $tickScript,
|
||||
];
|
||||
}
|
||||
@@ -301,13 +417,20 @@ function enelixDemoTick(string $source = 'Manual'): void
|
||||
SetValue($hourId, $hour);
|
||||
}
|
||||
|
||||
$dynamic = (bool) GetValue(enelixDemoRequiredId($simulation, 'DynamischeProfile'));
|
||||
$environment = enelixDemoEnvironment(
|
||||
$hour,
|
||||
(float) GetValue(enelixDemoRequiredId($simulation, 'Grundlast')),
|
||||
(int) GetValue(enelixDemoRequiredId($simulation, 'Bewoelkung')),
|
||||
$dynamic
|
||||
);
|
||||
$base = $environment['base']
|
||||
+ (float) GetValue(enelixDemoRequiredId($simulation, 'Zusatzlast'));
|
||||
$peak = (float) GetValue(enelixDemoRequiredId($simulation, 'PVSpitze'));
|
||||
$cloud = (int) GetValue(enelixDemoRequiredId($simulation, 'Bewoelkung'));
|
||||
$base = (float) GetValue(enelixDemoRequiredId($simulation, 'Grundlast'));
|
||||
$sun = ($hour >= 6.0 && $hour <= 18.0)
|
||||
? max(0.0, sin(M_PI * ($hour - 6.0) / 12.0))
|
||||
: 0.0;
|
||||
$pv = $peak * $sun * (1.0 - (min(100, max(0, $cloud)) * 0.0075));
|
||||
$pv = $peak * $sun * (1.0 - ($environment['cloud'] * 0.0085));
|
||||
|
||||
$contacts = [
|
||||
'boiler11' => enelixDemoRequiredId($technical, 'Boiler1Stufe1'),
|
||||
@@ -315,15 +438,46 @@ function enelixDemoTick(string $source = 'Manual'): void
|
||||
'boiler21' => enelixDemoRequiredId($technical, 'Boiler2Stufe1'),
|
||||
'boiler22' => enelixDemoRequiredId($technical, 'Boiler2Stufe2'),
|
||||
'dryer' => enelixDemoRequiredId($technical, 'EntfeuchterKontakt'),
|
||||
'buffer1' => enelixDemoRequiredId($technical, 'PufferStufe1'),
|
||||
'buffer2' => enelixDemoRequiredId($technical, 'PufferStufe2'),
|
||||
'battery' => enelixDemoRequiredId($technical, 'BatterieIstleistung'),
|
||||
];
|
||||
$powerBefore = enelixDemoControlledPower($contacts);
|
||||
$temp1 = enelixDemoRequiredId($technical, 'Boiler1Temperatur');
|
||||
$temp2 = enelixDemoRequiredId($technical, 'Boiler2Temperatur');
|
||||
$bufferTemp = enelixDemoRequiredId($technical, 'PufferTemperatur');
|
||||
$outsideTemp = enelixDemoRequiredId($technical, 'AussenTemperatur');
|
||||
$batterySoc = enelixDemoRequiredId($technical, 'BatterieLadezustand');
|
||||
$batteryMaxCharge = enelixDemoRequiredId($technical, 'BatterieMaxLaden');
|
||||
$batteryMaxDischarge = enelixDemoRequiredId($technical, 'BatterieMaxEntladen');
|
||||
$previousBatteryPower = (float) GetValue($contacts['battery']);
|
||||
|
||||
SetValue($outsideTemp, round($environment['outside'], 2));
|
||||
SetValue($batteryMaxCharge, GetValue($batteryMaxCharge));
|
||||
SetValue($batteryMaxDischarge, GetValue($batteryMaxDischarge));
|
||||
if ($source === 'Timer') {
|
||||
enelixDemoUpdateTemperature($temp1, (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0), $minutes);
|
||||
enelixDemoUpdateTemperature($temp2, (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0), $minutes);
|
||||
enelixDemoUpdateTemperature(
|
||||
$temp1,
|
||||
(bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0),
|
||||
$minutes
|
||||
);
|
||||
enelixDemoUpdateTemperature(
|
||||
$temp2,
|
||||
(bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0),
|
||||
$minutes
|
||||
);
|
||||
enelixDemoUpdateBufferTemperature(
|
||||
$bufferTemp,
|
||||
(bool) GetValue($contacts['buffer2']) ? 3600 : ((bool) GetValue($contacts['buffer1']) ? 1800 : 0),
|
||||
$environment['outside'],
|
||||
$minutes
|
||||
);
|
||||
enelixDemoUpdateBatterySoc($batterySoc, $previousBatteryPower, $minutes);
|
||||
} else {
|
||||
SetValue($bufferTemp, GetValue($bufferTemp));
|
||||
SetValue($batterySoc, GetValue($batterySoc));
|
||||
}
|
||||
|
||||
$powerBefore = enelixDemoControlledPower($contacts);
|
||||
$gridId = enelixDemoRequiredId($dashboard, 'Netzleistung');
|
||||
SetValue($gridId, round($base + $powerBefore - $pv, 1));
|
||||
|
||||
@@ -331,38 +485,55 @@ function enelixDemoTick(string $source = 'Manual'): void
|
||||
enelixDemoRequiredId($ems, 'Boiler1'),
|
||||
enelixDemoRequiredId($ems, 'Boiler2'),
|
||||
enelixDemoRequiredId($ems, 'Entfeuchter'),
|
||||
enelixDemoRequiredId($ems, 'Pufferspeicher'),
|
||||
enelixDemoRequiredId($ems, 'Batterie'),
|
||||
];
|
||||
foreach ($consumers as $consumerId) {
|
||||
IPS_RequestAction($consumerId, 'Melden', true);
|
||||
}
|
||||
$manager = enelixDemoRequiredId($ems, 'Manager');
|
||||
IPS_RequestAction($manager, 'Regeln', true);
|
||||
$managerReady = (int) IPS_GetInstance($manager)['InstanceStatus'] === 102;
|
||||
if ($managerReady) {
|
||||
foreach ($consumers as $consumerId) {
|
||||
IPS_RequestAction($consumerId, 'Melden', true);
|
||||
}
|
||||
IPS_RequestAction($manager, 'Regeln', true);
|
||||
}
|
||||
|
||||
$battery = $managerReady
|
||||
? enelixDemoBatteryRegisterPower(
|
||||
(int) GetValue(enelixDemoRequiredId($technical, 'BatterieManagement')),
|
||||
(int) GetValue(enelixDemoRequiredId($technical, 'BatterieModus')),
|
||||
(float) GetValue(enelixDemoRequiredId($technical, 'BatterieRegisterLeistung'))
|
||||
)
|
||||
: 0.0;
|
||||
SetValue($contacts['battery'], $battery);
|
||||
|
||||
$boiler1 = (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0);
|
||||
$boiler2 = (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0);
|
||||
$dryer = (bool) GetValue($contacts['dryer']) ? 900 : 0;
|
||||
$power = $boiler1 + $boiler2 + $dryer;
|
||||
$house = $base + $power;
|
||||
$grid = $house - $pv;
|
||||
$buffer = (bool) GetValue($contacts['buffer2']) ? 3600 : ((bool) GetValue($contacts['buffer1']) ? 1800 : 0);
|
||||
$loads = $boiler1 + $boiler2 + $dryer + $buffer;
|
||||
$emsPower = $loads + $battery;
|
||||
$house = $base + $loads;
|
||||
$grid = $house + $battery - $pv;
|
||||
|
||||
$pvId = enelixDemoRequiredId($dashboard, 'PVLeistung');
|
||||
$houseId = enelixDemoRequiredId($dashboard, 'Hausverbrauch');
|
||||
$loadId = enelixDemoRequiredId($dashboard, 'Verbraucherleistung');
|
||||
SetValue($gridId, round($grid, 1));
|
||||
SetValue($pvId, round($pv, 1));
|
||||
SetValue($houseId, round($house, 1));
|
||||
SetValue($loadId, (float) $power);
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'PVLeistung'), round($pv, 1));
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'Hausverbrauch'), round($house, 1));
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'Verbraucherleistung'), (float) $emsPower);
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'Boiler1Leistung'), (float) $boiler1);
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'Boiler2Leistung'), (float) $boiler2);
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'EntfeuchterLeistung'), (float) $dryer);
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'PufferLeistung'), (float) $buffer);
|
||||
SetValue(enelixDemoRequiredId($dashboard, 'BatterieLeistung'), $battery);
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussPVkW'), round($pv / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussNetzkW'), round($grid / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussGrundlastkW'), round($base / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussBoiler1kW'), round($boiler1 / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussBoiler2kW'), round($boiler2 / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussEntfeuchterkW'), round($dryer / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussPufferkW'), round($buffer / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'EnergieflussBatteriekW'), round($battery / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'DiagrammHausverbrauchkW'), round($house / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'DiagrammVerbraucherkW'), round($power / 1000.0, 3));
|
||||
SetValue(enelixDemoRequiredId($technical, 'DiagrammVerbraucherkW'), round($emsPower / 1000.0, 3));
|
||||
|
||||
$history = enelixDemoRequiredId($root, 'Historie');
|
||||
$energyIds = [
|
||||
@@ -378,7 +549,6 @@ function enelixDemoTick(string $source = 'Manual'): void
|
||||
enelixDemoAddEnergy($energyIds['import'], max(0.0, $grid), $hours);
|
||||
enelixDemoAddEnergy($energyIds['export'], max(0.0, -$grid), $hours);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function enelixDemoRenderFlow(array $state): string
|
||||
@@ -390,9 +560,15 @@ function enelixDemoRenderFlow(array $state): string
|
||||
$gridLabel = $grid < -25 ? 'Einspeisung' : ($grid > 25 ? 'Netzbezug' : 'Ausgeglichen');
|
||||
$gridArrow = $grid < -25 ? '→' : ($grid > 25 ? '←' : '↔');
|
||||
$gridClass = abs($grid) <= 250 ? 'good' : ($grid < 0 ? 'export' : 'import');
|
||||
$buffer = (float) ($state['buffer'] ?? 0.0);
|
||||
$battery = (float) ($state['battery'] ?? 0.0);
|
||||
$bufferTemp = (float) ($state['bufferTemp'] ?? 36.0);
|
||||
$batterySoc = (float) ($state['batterySoc'] ?? 50.0);
|
||||
$b1Class = (float) $state['boiler1'] > 0 ? 'on' : '';
|
||||
$b2Class = (float) $state['boiler2'] > 0 ? 'on' : '';
|
||||
$dryClass = (float) $state['dryer'] > 0 ? 'on' : '';
|
||||
$bufferClass = $buffer > 0 ? 'on' : '';
|
||||
$batteryClass = abs($battery) > 25 ? 'on' : '';
|
||||
|
||||
return '<style>
|
||||
.enelix-flow{box-sizing:border-box;font-family:Inter,Segoe UI,Arial,sans-serif;background:#10151c;color:#f4f7fa;padding:18px;min-height:420px}
|
||||
@@ -403,14 +579,14 @@ function enelixDemoRenderFlow(array $state): string
|
||||
.enelix-node small{display:block;color:#9dacbd;margin-bottom:8px}.enelix-node b{font-size:22px}
|
||||
.enelix-node.solar{border-color:#b99628}.enelix-node.bus{border-color:#2786ac}.enelix-node.good{border-color:#2bb673}
|
||||
.enelix-node.export{border-color:#38a3db}.enelix-node.import{border-color:#e95d5d}.enelix-arrow{text-align:center;font-size:30px;color:#9dacbd}
|
||||
.enelix-loads{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:18px}
|
||||
.enelix-loads{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin-top:18px}
|
||||
.enelix-load{border-left:4px solid #566371;background:#17202a;padding:13px 14px;border-radius:6px}
|
||||
.enelix-load.on{border-left-color:#2bb673}.enelix-load b{display:block;font-size:17px;margin:4px 0}.enelix-load span{color:#9dacbd;font-size:12px}
|
||||
.enelix-foot{display:flex;justify-content:space-between;gap:12px;margin-top:16px;padding-top:12px;border-top:1px solid #2a3541;color:#9dacbd;font-size:12px}
|
||||
@media(max-width:640px){.enelix-line{grid-template-columns:1fr}.enelix-arrow{transform:rotate(90deg)}.enelix-loads{grid-template-columns:1fr}.enelix-head{align-items:start;flex-direction:column;gap:4px}}
|
||||
</style><div class="enelix-flow"><div class="enelix-head"><div><div class="enelix-title">Enelix 2 Energiefluss</div><div class="enelix-sub">Manager regelt drei flexible Verbraucher</div></div><div class="enelix-sub">Simulation ' . $time . ' Uhr</div></div>
|
||||
<div class="enelix-line"><div class="enelix-node solar"><small>PV-Erzeugung</small><b>' . $format((float) $state['pv']) . ' W</b></div><div class="enelix-arrow">→</div><div class="enelix-node bus"><small>Haus + flexible Lasten</small><b>' . $format((float) $state['base'] + (float) $state['boiler1'] + (float) $state['boiler2'] + (float) $state['dryer']) . ' W</b></div><div class="enelix-arrow">' . $gridArrow . '</div><div class="enelix-node ' . $gridClass . '"><small>' . $gridLabel . '</small><b>' . $format(abs($grid)) . ' W</b></div></div>
|
||||
<div class="enelix-loads"><div class="enelix-load ' . $b1Class . '"><span>Priorität 1</span><b>Boiler 1 · ' . $format((float) $state['boiler1']) . ' W</b><span>' . number_format((float) $state['temp1'], 1, ',', '') . ' °C</span></div><div class="enelix-load ' . $b2Class . '"><span>Priorität 2</span><b>Boiler 2 · ' . $format((float) $state['boiler2']) . ' W</b><span>' . number_format((float) $state['temp2'], 1, ',', '') . ' °C</span></div><div class="enelix-load ' . $dryClass . '"><span>Priorität 3</span><b>Entfeuchter · ' . $format((float) $state['dryer']) . ' W</b><span>einstufiger Verbraucher</span></div></div>
|
||||
</style><div class="enelix-flow"><div class="enelix-head"><div><div class="enelix-title">Enelix 2 Energiefluss</div><div class="enelix-sub">Manager regelt fünf flexible Teilnehmer aus vier Modultypen</div></div><div class="enelix-sub">Simulation ' . $time . ' Uhr</div></div>
|
||||
<div class="enelix-line"><div class="enelix-node solar"><small>PV-Erzeugung</small><b>' . $format((float) $state['pv']) . ' W</b></div><div class="enelix-arrow">→</div><div class="enelix-node bus"><small>Haus + EMS-Leistung</small><b>' . $format((float) $state['base'] + (float) $state['boiler1'] + (float) $state['boiler2'] + (float) $state['dryer'] + $buffer + $battery) . ' W</b></div><div class="enelix-arrow">' . $gridArrow . '</div><div class="enelix-node ' . $gridClass . '"><small>' . $gridLabel . '</small><b>' . $format(abs($grid)) . ' W</b></div></div>
|
||||
<div class="enelix-loads"><div class="enelix-load ' . $b1Class . '"><span>Priorität 1</span><b>Boiler 1 · ' . $format((float) $state['boiler1']) . ' W</b><span>' . number_format((float) $state['temp1'], 1, ',', '') . ' °C</span></div><div class="enelix-load ' . $b2Class . '"><span>Priorität 2</span><b>Boiler 2 · ' . $format((float) $state['boiler2']) . ' W</b><span>' . number_format((float) $state['temp2'], 1, ',', '') . ' °C</span></div><div class="enelix-load ' . $dryClass . '"><span>Priorität 3</span><b>Entfeuchter · ' . $format((float) $state['dryer']) . ' W</b><span>einstufiger Verbraucher</span></div><div class="enelix-load ' . $bufferClass . '"><span>Priorität 4</span><b>Pufferspeicher · ' . $format($buffer) . ' W</b><span>' . number_format($bufferTemp, 1, ',', '') . ' °C · Heizkurve</span></div><div class="enelix-load ' . $batteryClass . '"><span>Priorität 5 / Peak 1</span><b>Batterie · ' . $format($battery) . ' W</b><span>' . number_format($batterySoc, 1, ',', '') . ' % · bidirektional</span></div></div>
|
||||
<div class="enelix-foot"><span>Grundlast ' . $format((float) $state['base']) . ' W</span><span>Manager: ' . htmlspecialchars((string) $state['manager'], ENT_QUOTES, 'UTF-8') . '</span></div></div>';
|
||||
}
|
||||
|
||||
@@ -418,7 +594,65 @@ function enelixDemoControlledPower(array $contacts): int
|
||||
{
|
||||
$boiler1 = (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0);
|
||||
$boiler2 = (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0);
|
||||
return $boiler1 + $boiler2 + ((bool) GetValue($contacts['dryer']) ? 900 : 0);
|
||||
$buffer = (bool) GetValue($contacts['buffer2']) ? 3600 : ((bool) GetValue($contacts['buffer1']) ? 1800 : 0);
|
||||
$battery = (int) round((float) GetValue($contacts['battery']));
|
||||
|
||||
return $boiler1 + $boiler2 + ((bool) GetValue($contacts['dryer']) ? 900 : 0) + $buffer + $battery;
|
||||
}
|
||||
|
||||
/** @return array{base: float, cloud: float, outside: float} */
|
||||
function enelixDemoEnvironment(float $hour, float $base, int $cloud, bool $dynamic): array
|
||||
{
|
||||
$hour = fmod(max(0.0, $hour), 24.0);
|
||||
$cloud = min(100, max(0, $cloud));
|
||||
if (!$dynamic) {
|
||||
return ['base' => $base, 'cloud' => (float) $cloud, 'outside' => 6.0];
|
||||
}
|
||||
|
||||
$morning = exp(-pow(($hour - 7.5) / 1.5, 2));
|
||||
$evening = exp(-pow(($hour - 19.0) / 2.2, 2));
|
||||
$night = ($hour < 5.0 || $hour >= 23.0) ? 0.65 : 0.82;
|
||||
$baseFactor = $night + (0.48 * $morning) + (0.72 * $evening);
|
||||
$cloudVariation = (18.0 * sin(($hour + 0.4) * 1.7)) + (8.0 * sin($hour * 4.6));
|
||||
$outside = 7.0 + (6.0 * sin(2.0 * M_PI * ($hour - 9.0) / 24.0));
|
||||
|
||||
return [
|
||||
'base' => max(150.0, $base * $baseFactor),
|
||||
'cloud' => min(100.0, max(0.0, $cloud + $cloudVariation)),
|
||||
'outside' => $outside,
|
||||
];
|
||||
}
|
||||
|
||||
function enelixDemoBatteryRegisterPower(int $management, int $mode, float $power): float
|
||||
{
|
||||
if ($management !== 2) {
|
||||
return 0.0;
|
||||
}
|
||||
if ($mode === 11) {
|
||||
return max(0.0, $power);
|
||||
}
|
||||
if ($mode === 12) {
|
||||
return -max(0.0, $power);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
function enelixDemoUpdateBufferTemperature(int $variableId, int $power, float $outside, int $minutes): void
|
||||
{
|
||||
$current = (float) GetValue($variableId);
|
||||
$heating = ($power / 1000.0) * 0.018 * $minutes;
|
||||
$loss = max(0.0, $current - $outside) * 0.0007 * $minutes;
|
||||
SetValue($variableId, round(min(65.0, max(22.0, $current + $heating - $loss)), 2));
|
||||
}
|
||||
|
||||
function enelixDemoUpdateBatterySoc(int $variableId, float $power, int $minutes): void
|
||||
{
|
||||
$current = (float) GetValue($variableId);
|
||||
$energy = ($power / 1000.0) * ($minutes / 60.0);
|
||||
$storedEnergy = $energy >= 0.0 ? $energy * 0.94 : $energy / 0.94;
|
||||
$delta = ($storedEnergy / 10.0) * 100.0;
|
||||
SetValue($variableId, round(min(100.0, max(5.0, $current + $delta)), 2));
|
||||
}
|
||||
|
||||
function enelixDemoUpdateTemperature(int $variableId, int $power, int $minutes): void
|
||||
@@ -439,6 +673,8 @@ function enelixDemoValidateRuntime(): void
|
||||
ENELIX_DEMO_MANAGER_MODULE,
|
||||
ENELIX_DEMO_BOILER_MODULE,
|
||||
ENELIX_DEMO_SINGLE_MODULE,
|
||||
ENELIX_DEMO_BUFFER_MODULE,
|
||||
ENELIX_DEMO_BATTERY_MODULE,
|
||||
ENELIX_DEMO_DUMMY_MODULE,
|
||||
ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE,
|
||||
] as $moduleId) {
|
||||
@@ -450,10 +686,11 @@ function enelixDemoValidateRuntime(): void
|
||||
|
||||
function enelixDemoEnsureProfiles(): void
|
||||
{
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.Power', 2, -12000, 12000, 100, 0, ' W');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.PowerPositive', 2, 0, 12000, 100, 0, ' W');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.Power', 2, -20000, 20000, 100, 0, ' W');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.PowerPositive', 2, 0, 20000, 100, 0, ' W');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.Hour', 2, 0, 23.75, 0.25, 2, ' h');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.Percent', 1, 0, 100, 5, 0, ' %');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.PercentFloat', 2, 0, 100, 0.1, 1, ' %');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.Speed', 1, 1, 60, 1, 0, ' min');
|
||||
enelixDemoEnsureProfile('ENELIX.Demo.Energy', 2, 0, 1000000, 0.01, 2, ' kWh');
|
||||
}
|
||||
@@ -628,15 +865,34 @@ function enelixDemoEnsureCyclicEvent(int $scriptId, string $ident, int $seconds)
|
||||
return $id;
|
||||
}
|
||||
|
||||
function enelixDemoConfigure(int $instanceId, array $properties): void
|
||||
function enelixDemoConfigure(
|
||||
int $instanceId,
|
||||
array $properties,
|
||||
array $allowedStatuses = [102]
|
||||
): void
|
||||
{
|
||||
foreach ($properties as $name => $value) {
|
||||
IPS_SetProperty($instanceId, (string) $name, $value);
|
||||
}
|
||||
IPS_ApplyChanges($instanceId);
|
||||
if ((int) IPS_GetInstance($instanceId)['InstanceStatus'] !== 102) {
|
||||
throw new RuntimeException('Instanz ist nach der Konfiguration nicht aktiv: ' . IPS_GetName($instanceId));
|
||||
$deadline = microtime(true) + 2.0;
|
||||
do {
|
||||
$status = (int) IPS_GetInstance($instanceId)['InstanceStatus'];
|
||||
if ($status === 102) {
|
||||
return;
|
||||
}
|
||||
IPS_Sleep(50);
|
||||
} while (microtime(true) < $deadline);
|
||||
|
||||
if (in_array($status, $allowedStatuses, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
'Instanz ist nach der Konfiguration nicht aktiv: '
|
||||
. IPS_GetName($instanceId)
|
||||
. ' (Status ' . $status . ', erwartet: ' . implode(', ', $allowedStatuses) . ')'
|
||||
);
|
||||
}
|
||||
|
||||
function enelixDemoConfigureArchive(array $variableIds): void
|
||||
@@ -713,7 +969,7 @@ function enelixDemoRemove(bool $confirmed = false): void
|
||||
IPS_ApplyChanges($visualization);
|
||||
}
|
||||
enelixDemoDeleteTree($root);
|
||||
foreach (['ENELIX.Demo.Power', 'ENELIX.Demo.PowerPositive', 'ENELIX.Demo.Hour', 'ENELIX.Demo.Percent', 'ENELIX.Demo.Speed', 'ENELIX.Demo.Energy'] as $profile) {
|
||||
foreach (['ENELIX.Demo.Power', 'ENELIX.Demo.PowerPositive', 'ENELIX.Demo.Hour', 'ENELIX.Demo.Percent', 'ENELIX.Demo.PercentFloat', 'ENELIX.Demo.Speed', 'ENELIX.Demo.Energy'] as $profile) {
|
||||
if (IPS_VariableProfileExists($profile)) {
|
||||
IPS_DeleteVariableProfile($profile);
|
||||
}
|
||||
@@ -771,9 +1027,14 @@ function enelixDemoConfigureEnergyDistribution(int $instanceId, array $nodes): v
|
||||
]);
|
||||
}
|
||||
|
||||
function enelixDemoLinkInstanceVariables(int $categoryId, int $instanceId, int $sensorId): void
|
||||
{
|
||||
enelixDemoEnsureLink($categoryId, 'SensorTemperatur', 'Gemessene Temperatur', $sensorId, 5);
|
||||
function enelixDemoLinkInstanceVariables(
|
||||
int $categoryId,
|
||||
int $instanceId,
|
||||
int $sensorId,
|
||||
string $sensorName = 'Gemessene Temperatur',
|
||||
string $sensorIdent = 'SensorTemperatur'
|
||||
): void {
|
||||
enelixDemoEnsureLink($categoryId, $sensorIdent, $sensorName, $sensorId, 5);
|
||||
$position = 10;
|
||||
foreach (IPS_GetChildrenIDs($instanceId) as $childId) {
|
||||
if ((int) IPS_GetObject($childId)['ObjectType'] !== 2) {
|
||||
|
||||
@@ -82,6 +82,62 @@ final class Lizenzpruefung
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die aufsummierte aktive Berechtigung fuer einen Katalogeintrag.
|
||||
*
|
||||
* @param array<string, mixed> $lease
|
||||
*/
|
||||
public static function berechtigungsMenge(array $lease, string $kennung): int
|
||||
{
|
||||
$berechtigungen = $lease['entitlements'] ?? null;
|
||||
if (!is_array($berechtigungen)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$menge = 0;
|
||||
foreach ($berechtigungen as $berechtigung) {
|
||||
if (!is_array($berechtigung)
|
||||
|| ($berechtigung['status'] ?? null) !== 'active'
|
||||
|| !is_int($berechtigung['quantity'] ?? null)
|
||||
|| $berechtigung['quantity'] < 1
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$berechtigungsKennung = $berechtigung['catalog_key']
|
||||
?? $berechtigung['sku']
|
||||
?? null;
|
||||
if ($berechtigungsKennung === $kennung) {
|
||||
$menge += $berechtigung['quantity'];
|
||||
}
|
||||
}
|
||||
|
||||
return $menge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $lease
|
||||
* @param array<string, int> $verwendeteMengen
|
||||
*/
|
||||
public static function pruefeVerbrauchermengen(array $lease, array $verwendeteMengen): void
|
||||
{
|
||||
foreach ($verwendeteMengen as $kennung => $verwendet) {
|
||||
if ($verwendet < 0) {
|
||||
throw new InvalidArgumentException(
|
||||
'Die verwendete Menge fuer ' . $kennung . ' darf nicht negativ sein.'
|
||||
);
|
||||
}
|
||||
$lizenziert = self::berechtigungsMenge($lease, $kennung);
|
||||
if ($verwendet > $lizenziert) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Lizenzkontingent fuer %s ueberschritten: %d verwendet, %d lizenziert.',
|
||||
$kennung,
|
||||
$verwendet,
|
||||
$lizenziert
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $lease
|
||||
*/
|
||||
|
||||
@@ -18,8 +18,12 @@ final class DemoanlageBootstrapTest extends TestCase
|
||||
'boiler1' => 2400,
|
||||
'boiler2' => 1500,
|
||||
'dryer' => 900,
|
||||
'buffer' => 1800,
|
||||
'battery' => -1200,
|
||||
'temp1' => 51.2,
|
||||
'temp2' => 48.7,
|
||||
'bufferTemp' => 39.4,
|
||||
'batterySoc' => 63.5,
|
||||
'manager' => 'PV',
|
||||
]);
|
||||
|
||||
@@ -27,6 +31,9 @@ final class DemoanlageBootstrapTest extends TestCase
|
||||
self::assertStringContainsString('Boiler 1', $html);
|
||||
self::assertStringContainsString('Boiler 2', $html);
|
||||
self::assertStringContainsString('Entfeuchter', $html);
|
||||
self::assertStringContainsString('Pufferspeicher', $html);
|
||||
self::assertStringContainsString('Batterie', $html);
|
||||
self::assertStringContainsString('63,5 %', $html);
|
||||
self::assertStringContainsString('Einspeisung', $html);
|
||||
self::assertStringContainsString('12:30', $html);
|
||||
}
|
||||
@@ -105,6 +112,10 @@ final class DemoanlageBootstrapTest extends TestCase
|
||||
self::assertStringContainsString('ENELIX_DEMO_DUMMY_MODULE', $source);
|
||||
self::assertStringContainsString("'Boiler1Liste'", $source);
|
||||
self::assertStringContainsString("'Boiler2Liste'", $source);
|
||||
self::assertStringContainsString("'PufferListe'", $source);
|
||||
self::assertStringContainsString("'BatterieListe'", $source);
|
||||
self::assertStringContainsString('ENELIX_DEMO_BUFFER_MODULE', $source);
|
||||
self::assertStringContainsString('ENELIX_DEMO_BATTERY_MODULE', $source);
|
||||
self::assertStringContainsString("'EnergieflussPVkW'", $source);
|
||||
self::assertStringContainsString("'~Power'", $source);
|
||||
self::assertStringContainsString("'~Electricity'", $source);
|
||||
@@ -120,6 +131,20 @@ final class DemoanlageBootstrapTest extends TestCase
|
||||
"enelixDemoEnergyNode(\$flowDryer, 'Entfeuchter', 'consumer')",
|
||||
$source
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"enelixDemoEnergyNode(\$flowBuffer, 'Pufferspeicher', 'consumer', \$bufferTemp)",
|
||||
$source
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"enelixDemoEnergyNode(\$flowBattery, 'Batteriespeicher', 'battery', \$batterySoc)",
|
||||
$source
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"\$consumerIds = [\$boiler1, \$boiler2, \$dryer, \$buffer, \$battery]",
|
||||
$source
|
||||
);
|
||||
self::assertStringContainsString("'consumerIds' => \$consumerIds", $source);
|
||||
self::assertStringContainsString("'VerbraucherZuordnung' => '[]'", $source);
|
||||
self::assertStringContainsString(
|
||||
"enelixDemoEnsureChart(\$history, 'LeistungsdiagrammNativ'",
|
||||
$source
|
||||
@@ -135,6 +160,27 @@ final class DemoanlageBootstrapTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testDynamischeUmgebungBleibtBegrenztUndVeraendertDenLastgang(): void
|
||||
{
|
||||
$static = enelixDemoEnvironment(12.0, 1000.0, 30, false);
|
||||
self::assertSame(['base' => 1000.0, 'cloud' => 30.0, 'outside' => 6.0], $static);
|
||||
|
||||
$morning = enelixDemoEnvironment(7.5, 1000.0, 30, true);
|
||||
$night = enelixDemoEnvironment(2.0, 1000.0, 30, true);
|
||||
self::assertGreaterThan($night['base'], $morning['base']);
|
||||
self::assertGreaterThanOrEqual(0.0, $morning['cloud']);
|
||||
self::assertLessThanOrEqual(100.0, $morning['cloud']);
|
||||
self::assertNotSame($morning['outside'], $night['outside']);
|
||||
}
|
||||
|
||||
public function testBatterieregisterWerdenMitKorrektemVorzeichenAbgebildet(): void
|
||||
{
|
||||
self::assertSame(1500.0, enelixDemoBatteryRegisterPower(2, 11, 1500.0));
|
||||
self::assertSame(-1500.0, enelixDemoBatteryRegisterPower(2, 12, 1500.0));
|
||||
self::assertSame(0.0, enelixDemoBatteryRegisterPower(1, 11, 1500.0));
|
||||
self::assertSame(0.0, enelixDemoBatteryRegisterPower(2, 0, 1500.0));
|
||||
}
|
||||
|
||||
public function testRollbackIstExplizitBestaetigungspflichtig(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
@@ -104,6 +104,75 @@ final class LizenzpruefungTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testZweiLizenzierteBoilerWerdenFreigegeben(): void
|
||||
{
|
||||
$lease = $this->lease('manager_standard');
|
||||
$lease['entitlements'][] = [
|
||||
'sku' => 'ENX-BOILER-MULTI',
|
||||
'catalog_key' => 'boiler_multi',
|
||||
'quantity' => 2,
|
||||
'status' => 'active',
|
||||
];
|
||||
|
||||
self::assertSame(2, Lizenzpruefung::berechtigungsMenge($lease, 'boiler_multi'));
|
||||
Lizenzpruefung::pruefeVerbrauchermengen($lease, ['boiler_multi' => 2]);
|
||||
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
public function testDritterBoilerBeiZweiLizenzenWirdAbgelehnt(): void
|
||||
{
|
||||
$lease = $this->lease('manager_standard');
|
||||
$lease['entitlements'][] = [
|
||||
'sku' => 'ENX-BOILER-MULTI',
|
||||
'catalog_key' => 'boiler_multi',
|
||||
'quantity' => 2,
|
||||
'status' => 'active',
|
||||
];
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Lizenzkontingent fuer boiler_multi ueberschritten: 3 verwendet, 2 lizenziert.'
|
||||
);
|
||||
Lizenzpruefung::pruefeVerbrauchermengen($lease, ['boiler_multi' => 3]);
|
||||
}
|
||||
|
||||
public function testVerbraucherkontingenteWerdenJeTypSummiertUndGetrenntGeprueft(): void
|
||||
{
|
||||
$lease = $this->lease('manager_standard');
|
||||
$lease['entitlements'] = array_merge($lease['entitlements'], [
|
||||
[
|
||||
'catalog_key' => 'boiler_multi',
|
||||
'quantity' => 1,
|
||||
'status' => 'active',
|
||||
],
|
||||
[
|
||||
'catalog_key' => 'boiler_multi',
|
||||
'quantity' => 1,
|
||||
'status' => 'active',
|
||||
],
|
||||
[
|
||||
'catalog_key' => 'battery',
|
||||
'quantity' => 1,
|
||||
'status' => 'active',
|
||||
],
|
||||
[
|
||||
'catalog_key' => 'battery',
|
||||
'quantity' => 10,
|
||||
'status' => 'revoked',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertSame(2, Lizenzpruefung::berechtigungsMenge($lease, 'boiler_multi'));
|
||||
self::assertSame(1, Lizenzpruefung::berechtigungsMenge($lease, 'battery'));
|
||||
Lizenzpruefung::pruefeVerbrauchermengen($lease, [
|
||||
'boiler_multi' => 2,
|
||||
'battery' => 1,
|
||||
]);
|
||||
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void
|
||||
{
|
||||
self::assertSame(
|
||||
|
||||
@@ -52,6 +52,7 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
'LoggingEin',
|
||||
'Lizenzcode',
|
||||
'LizenzPruefen',
|
||||
'LizenzVerwalten',
|
||||
'LizenzInstallationID',
|
||||
'LizenzInformation',
|
||||
'VerbraucherAktualisieren',
|
||||
@@ -69,6 +70,8 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
self::assertStringContainsString($bereich, $json);
|
||||
}
|
||||
self::assertSame('Lizenzierung', $formular['elements'][0]['caption']);
|
||||
self::assertStringContainsString('https://license.enelix.ch', $json);
|
||||
self::assertStringContainsString('"link":true', $json);
|
||||
self::assertStringContainsString(
|
||||
"'SuchbereichID' => \$SuchbereichID",
|
||||
$json
|
||||
@@ -111,6 +114,44 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
self::assertStringContainsString("RegisterAttributeString('LizenzcodeHash'", $inhalt);
|
||||
self::assertStringContainsString('STATUS_LIZENZ_UNGUELTIG', $inhalt);
|
||||
self::assertStringContainsString('aktualisiereLizenzfreigabe(', $inhalt);
|
||||
self::assertStringContainsString('pruefeVerbraucherlizenzen(', $inhalt);
|
||||
self::assertStringContainsString('lizenzBerechtigungenPassen(', $inhalt);
|
||||
self::assertStringContainsString('peakShavingLizenziert(', $inhalt);
|
||||
self::assertStringContainsString('Aus (Peak-Shaving-Lizenz fehlt)', $inhalt);
|
||||
self::assertStringContainsString('Aus (Peak-Shaving-Lizenz verfuegbar)', $inhalt);
|
||||
self::assertStringContainsString(
|
||||
'Konstant und Monatlich benoetigen eine Peak-Shaving-Lizenz.',
|
||||
$inhalt
|
||||
);
|
||||
foreach (['battery', 'buffer_storage', 'ev_charger', 'consumer_single', 'boiler_multi'] as $katalog) {
|
||||
self::assertStringContainsString("'Katalog' => '" . $katalog . "'", $inhalt);
|
||||
}
|
||||
foreach ([
|
||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
||||
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}',
|
||||
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
|
||||
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
|
||||
] as $modulID) {
|
||||
self::assertStringContainsString("'" . $modulID . "' =>", $inhalt);
|
||||
}
|
||||
self::assertStringContainsString("'Name' => 'Pufferspeicher'", $inhalt);
|
||||
self::assertStringContainsString("'Name' => 'Batteriespeicher'", $inhalt);
|
||||
self::assertStringContainsString(
|
||||
'Bitte Auswahl reduzieren oder Lizenz erweitern.',
|
||||
$inhalt
|
||||
);
|
||||
foreach ([
|
||||
"'NichtGeregelt'",
|
||||
"'NichtGeregelteGeraete'",
|
||||
"'InstanzStatus'",
|
||||
"'LetzteMeldung'",
|
||||
"'GemeldeteBetriebsart'",
|
||||
"'Verfuegbar'",
|
||||
] as $detailfeld) {
|
||||
self::assertStringContainsString($detailfeld, $inhalt);
|
||||
}
|
||||
self::assertStringNotContainsString("'publicKey' =>", $inhalt);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ return static function (TestContext $test): void {
|
||||
if ($lizenzcode === '') {
|
||||
$test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function (
|
||||
TestContext $test
|
||||
) use ($managerId): void {
|
||||
) use ($managerId, $verbraucherstatus): void {
|
||||
IPS_RequestAction($managerId, 'Aktiv', true);
|
||||
IPS_RequestAction($managerId, 'Regeln', true);
|
||||
$test->assertInstanceStatus($managerId, 203);
|
||||
@@ -171,6 +171,21 @@ return static function (TestContext $test): void {
|
||||
'Inaktiv',
|
||||
GetValue($test->objectByIdent('Betriebsart', $managerId))
|
||||
);
|
||||
|
||||
$daten = $verbraucherstatus();
|
||||
$test->assertSame(false, $daten['Synchronisiert'] ?? null);
|
||||
$test->assertSame(3, count($daten['NichtGeregelt'] ?? []));
|
||||
foreach ($daten['NichtGeregelt'] as $detail) {
|
||||
$test->assertTrue(is_int($detail['InstanzID'] ?? null));
|
||||
$test->assertTrue(is_string($detail['Name'] ?? null));
|
||||
$test->assertTrue(is_string($detail['Typ'] ?? null));
|
||||
$test->assertSame('Lizenz nicht freigegeben', $detail['Grund'] ?? null);
|
||||
}
|
||||
$detailtext = (string) GetValue(
|
||||
$test->objectByIdent('NichtGeregelteGeraete', $managerId)
|
||||
);
|
||||
$test->assertTrue(str_contains($detailtext, 'Boiler'));
|
||||
$test->assertTrue(str_contains($detailtext, 'Lizenz nicht freigegeben'));
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user