diff --git a/Manager/form.json b/Manager/form.json index 8993cdd..ffd452a 100644 --- a/Manager/form.json +++ b/Manager/form.json @@ -1,159 +1,258 @@ { "elements": [ { - "type": "Select", - "name": "Rolle", - "caption": "Rolle", - "options": [ - {"caption": "Alleine", "value": 0}, - {"caption": "Hauptmanager", "value": 1}, - {"caption": "Untermanager", "value": 2} - ] - }, - { - "type": "SelectVariable", - "name": "NetzleistungVariableID", - "caption": "Netzleistung" - }, - { - "type": "NumberSpinner", - "name": "Netzleistungsfaktor", - "caption": "Netzleistungsfaktor", - "digits": 4 - }, - { - "type": "NumberSpinner", - "name": "MesswertMaxAlter", - "caption": "Maximales Messwertalter", - "suffix": " s", - "minimum": 1 - }, - { - "type": "List", - "name": "VerbraucherZuordnung", - "caption": "Verbraucher", - "add": true, - "delete": true, - "sortable": true, - "columns": [ + "type": "ExpansionPanel", + "caption": "Grundeinstellungen", + "expanded": true, + "items": [ { - "caption": "Instanz", - "name": "InstanzID", - "width": "auto", - "add": 0, - "edit": {"type": "SelectInstance"} + "type": "Select", + "name": "Rolle", + "caption": "Rolle", + "options": [ + {"caption": "Alleine", "value": 0}, + {"caption": "Hauptmanager", "value": 1}, + {"caption": "Untermanager", "value": 2} + ] }, { - "caption": "Aktiv", - "name": "Aktiv", - "width": "100px", + "type": "SelectVariable", + "name": "NetzleistungVariableID", + "caption": "Netzleistung" + }, + { + "type": "NumberSpinner", + "name": "SollwertSolarladen", + "caption": "Sollwert Solarladen", + "suffix": " W", + "digits": 1 + }, + { + "type": "Select", + "name": "Lastspitzenmodus", + "caption": "Lastspitzenmodus", + "onChange": "IPS_RequestAction($id, 'FormLastspitzenmodus', $Lastspitzenmodus);", + "options": [ + {"caption": "Aus", "value": 0}, + {"caption": "Konstant", "value": 1}, + {"caption": "Monatlich", "value": 2} + ] + }, + { + "type": "NumberSpinner", + "name": "Lastspitzengrenze", + "caption": "Lastspitzengrenze", + "suffix": " W", + "digits": 1, + "visible": false + }, + { + "type": "Button", + "name": "MonatsgrenzenUmschalten", + "caption": "Monatsgrenzen anzeigen", + "visible": false, + "onClick": "IPS_RequestAction($id, 'FormMonatsgrenzenUmschalten', true);" + }, + { + "type": "List", + "name": "Monatsgrenzen", + "caption": "Monatliche Lastspitzengrenzen", + "rowCount": 12, "add": false, - "edit": {"type": "CheckBox"} + "delete": false, + "sortable": false, + "loadValuesFromConfiguration": false, + "visible": false, + "columns": [ + { + "caption": "Monat", + "name": "Monat", + "width": "auto", + "save": true + }, + { + "caption": "Grenze", + "name": "Grenze_W", + "width": "180px", + "save": true, + "edit": { + "type": "NumberSpinner", + "suffix": " W", + "digits": 1 + } + }, + { + "caption": "Monatsnummer", + "name": "MonatIndex", + "width": "0px", + "visible": false, + "save": true + } + ], + "values": [] + }, + { + "type": "CheckBox", + "name": "AutomatischeSuche", + "caption": "Verbraucher automatisch suchen", + "onChange": "IPS_RequestAction($id, 'FormAutomatischeSuche', $AutomatischeSuche);" + }, + { + "type": "SelectObject", + "name": "SuchbereichID", + "caption": "Suchbereich", + "visible": true + }, + { + "type": "List", + "name": "VerbraucherZuordnung", + "caption": "Verbraucher manuell auswaehlen", + "rowCount": 6, + "add": true, + "delete": true, + "sortable": true, + "visible": false, + "columns": [ + { + "caption": "Instanz", + "name": "InstanzID", + "width": "auto", + "add": 0, + "edit": {"type": "SelectInstance"} + }, + { + "caption": "Aktiv", + "name": "Aktiv", + "width": "100px", + "add": false, + "edit": {"type": "CheckBox"} + } + ] + }, + { + "type": "List", + "name": "AutomatischeVerbraucherZuordnung", + "caption": "Gefundene Verbraucher", + "rowCount": 8, + "add": false, + "delete": false, + "loadValuesFromConfiguration": false, + "visible": true, + "columns": [ + { + "caption": "Verbraucher", + "name": "Name", + "width": "auto", + "save": true + }, + { + "caption": "Instanz-ID", + "name": "InstanzID", + "width": "100px", + "save": true + }, + { + "caption": "Verwenden", + "name": "Aktiv", + "width": "100px", + "save": true, + "edit": {"type": "CheckBox"} + } + ], + "values": [] } ] }, { - "type": "CheckBox", - "name": "AutomatischeSuche", - "caption": "Verbraucher automatisch suchen" - }, - { - "type": "SelectObject", - "name": "SuchbereichID", - "caption": "Suchbereich" - }, - { - "type": "NumberSpinner", - "name": "Regelintervall", - "caption": "Regelintervall", - "suffix": " s", - "minimum": 1 - }, - { - "type": "NumberSpinner", - "name": "Vorgabeintervall", - "caption": "Vorgabeintervall", - "suffix": " s", - "minimum": 1 - }, - { - "type": "NumberSpinner", - "name": "VerbraucherTimeout", - "caption": "Verbraucher-Timeout", - "suffix": " s", - "minimum": 1 - }, - { - "type": "Select", - "name": "Lastspitzenmodus", - "caption": "Lastspitzenmodus", - "options": [ - {"caption": "Aus", "value": 0}, - {"caption": "Konstant", "value": 1}, - {"caption": "Monatlich", "value": 2} + "type": "ExpansionPanel", + "caption": "Erweiterte Einstellungen", + "items": [ + { + "type": "NumberSpinner", + "name": "KeepAlive", + "caption": "Keep alive", + "suffix": " s", + "minimum": 1 + }, + { + "type": "NumberSpinner", + "name": "VerbraucherTimeout", + "caption": "Verbraucher-Timeout", + "suffix": " s", + "minimum": 1 + }, + { + "type": "NumberSpinner", + "name": "MesswertMaxAlter", + "caption": "Maximales Messwertalter", + "suffix": " s", + "minimum": 1 + }, + { + "type": "NumberSpinner", + "name": "Umschaltdifferenz", + "caption": "Mindestdifferenz vor Umschaltung", + "suffix": " %", + "minimum": 0, + "maximum": 100, + "digits": 1 + }, + { + "type": "NumberSpinner", + "name": "Netzleistungsfaktor", + "caption": "Netzleistungsfaktor", + "digits": 4 + }, + { + "type": "CheckBox", + "name": "LoggingEin", + "caption": "Logging ein" + } ] }, - { - "type": "NumberSpinner", - "name": "Lastspitzengrenze", - "caption": "Lastspitzengrenze", - "suffix": " W", - "digits": 1 - }, - { - "type": "ValidationTextBox", - "name": "Monatsgrenzen", - "caption": "Monatsgrenzen als JSON" - }, - { - "type": "CheckBox", - "name": "MonatsgrenzenVisuBedienbar", - "caption": "Monatsgrenzen in der Visualisierung bedienen" - }, - { - "type": "NumberSpinner", - "name": "SollwertSolarladen", - "caption": "Sollwert Solarladen", - "suffix": " W", - "digits": 1 - }, - { - "type": "NumberSpinner", - "name": "UmschaltschwelleUeberschuss", - "caption": "Umschaltschwelle Ueberschuss", - "suffix": " W", - "digits": 1 - }, - { - "type": "NumberSpinner", - "name": "UmschaltschwelleLastspitze", - "caption": "Umschaltschwelle Lastspitze", - "suffix": " W", - "digits": 1 - }, - { - "type": "NumberSpinner", - "name": "Umschaltdifferenz", - "caption": "Mindestdifferenz vor Umschaltung", - "suffix": " %", - "minimum": 0, - "maximum": 100, - "digits": 1 - }, { "type": "ExpansionPanel", - "caption": "Optionale obere Anschluesse", + "caption": "Prognose / Forecast", "items": [ - {"type": "ValidationTextBox", "name": "PrognoseAnschluss", "caption": "Prognose / Forecast als JSON"}, - {"type": "ValidationTextBox", "name": "SDLAnschluss", "caption": "SDL / VGT als JSON"}, - {"type": "ValidationTextBox", "name": "LizenzAnschluss", "caption": "Lizenzierung als JSON"}, - {"type": "ValidationTextBox", "name": "StoermeldeAnschluss", "caption": "Stoerueberwachung als JSON"} + { + "type": "ValidationTextBox", + "name": "PrognoseAnschluss", + "caption": "Konfiguration als JSON" + } ] }, { - "type": "CheckBox", - "name": "LoggingEin", - "caption": "Logging ein" + "type": "ExpansionPanel", + "caption": "SDL / VGT", + "items": [ + { + "type": "ValidationTextBox", + "name": "SDLAnschluss", + "caption": "Konfiguration als JSON" + } + ] + }, + { + "type": "ExpansionPanel", + "caption": "Lizenzierung", + "items": [ + { + "type": "ValidationTextBox", + "name": "LizenzAnschluss", + "caption": "Konfiguration als JSON" + } + ] + }, + { + "type": "ExpansionPanel", + "caption": "Stoerueberwachung", + "items": [ + { + "type": "ValidationTextBox", + "name": "StoermeldeAnschluss", + "caption": "Konfiguration als JSON" + } + ] } ], "actions": [ diff --git a/Manager/module.php b/Manager/module.php index d2a899a..e06a1a1 100644 --- a/Manager/module.php +++ b/Manager/module.php @@ -16,6 +16,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle private const STATUS_MESSWERT_UNGUELTIG = 201; private const STATUS_KONFIGURATION_UNGUELTIG = 202; private const VM_UPDATE = 10603; + private const VERBRAUCHER_MODUL_IDS = [ + '{B7C54AF4-AD7D-4FE4-B75D-203693906251}', + ]; + private const MONATSNAMEN = [ + 1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember', + ]; public function Create(): void { @@ -24,21 +31,18 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->RegisterPropertyInteger('Rolle', 0); $this->RegisterPropertyInteger('NetzleistungVariableID', 0); $this->RegisterPropertyFloat('Netzleistungsfaktor', 1.0); - $this->RegisterPropertyInteger('MesswertMaxAlter', 30); + $this->RegisterPropertyInteger('MesswertMaxAlter', 60); $this->RegisterPropertyString('VerbraucherZuordnung', '[]'); + $this->RegisterPropertyString('AutomatischeVerbraucherZuordnung', '[]'); $this->RegisterPropertyBoolean('AutomatischeSuche', true); $this->RegisterPropertyInteger('SuchbereichID', 0); - $this->RegisterPropertyInteger('Regelintervall', 2); - $this->RegisterPropertyInteger('Vorgabeintervall', 10); - $this->RegisterPropertyInteger('VerbraucherTimeout', 30); + $this->RegisterPropertyInteger('KeepAlive', 60); + $this->RegisterPropertyInteger('VerbraucherTimeout', 60); $this->RegisterPropertyInteger('Lastspitzenmodus', 0); $this->RegisterPropertyFloat('Lastspitzengrenze', 0.0); $this->RegisterPropertyString('Monatsgrenzen', $this->standardMonatsgrenzen()); - $this->RegisterPropertyBoolean('MonatsgrenzenVisuBedienbar', false); $this->RegisterPropertyFloat('SollwertSolarladen', 0.0); - $this->RegisterPropertyFloat('UmschaltschwelleUeberschuss', 0.0); - $this->RegisterPropertyFloat('UmschaltschwelleLastspitze', 0.0); - $this->RegisterPropertyFloat('Umschaltdifferenz', 0.0); + $this->RegisterPropertyFloat('Umschaltdifferenz', 5.0); $this->RegisterPropertyString('PrognoseAnschluss', '{}'); $this->RegisterPropertyString('SDLAnschluss', '{}'); $this->RegisterPropertyString('LizenzAnschluss', '{}'); @@ -67,13 +71,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->RegisterAttributeString('LetzteSollwerte', '{}'); $this->RegisterAttributeInteger('RegistrierteNetzleistungVariableID', 0); + // Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren. + $this->RegisterTimer('Regelzyklus', 0, ''); + $this->RegisterTimer('VorgabenErneuern', 0, ''); $this->RegisterTimer( - 'Regelzyklus', - 0, - "IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', false);" - ); - $this->RegisterTimer( - 'VorgabenErneuern', + 'KeepAlive', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);" ); @@ -89,19 +91,82 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('VorgabenErneuern', 0); + $this->SetTimerInterval('KeepAlive', 0); $this->protokolliere('Konfiguration', $fehler->getMessage()); return; } $this->registriereNetzleistungsmeldung(); - $this->SetTimerInterval('Regelzyklus', $this->ReadPropertyInteger('Regelintervall') * 1000); - $this->SetTimerInterval('VorgabenErneuern', $this->ReadPropertyInteger('Vorgabeintervall') * 1000); + $this->SetTimerInterval('Regelzyklus', 0); + $this->SetTimerInterval('VorgabenErneuern', 0); + $this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000); $this->SetValue('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen')); $this->aktualisiereSichtbarkeit(); $this->aktualisiereAnschlussstatus(); $this->SetStatus(self::STATUS_AKTIV); } + public function GetConfigurationForm(): string + { + $formular = json_decode( + (string) file_get_contents(__DIR__ . '/form.json'), + true, + 512, + JSON_THROW_ON_ERROR + ); + $automatisch = $this->ReadPropertyBoolean('AutomatischeSuche'); + $lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus'); + $monatsgrenzenSichtbar = $lastspitzenmodus === 2 + && $this->GetBuffer('MonatsgrenzenSichtbar') === '1'; + + $this->setzeFormularParameter($formular['elements'], 'VerbraucherZuordnung', 'visible', !$automatisch); + $this->setzeFormularParameter( + $formular['elements'], + 'AutomatischeVerbraucherZuordnung', + 'visible', + $automatisch + ); + $this->setzeFormularParameter($formular['elements'], 'SuchbereichID', 'visible', $automatisch); + $this->setzeFormularParameter( + $formular['elements'], + 'AutomatischeVerbraucherZuordnung', + 'values', + $this->automatischeVerbraucherFuerFormular() + ); + $this->setzeFormularParameter( + $formular['elements'], + 'Lastspitzengrenze', + 'visible', + $lastspitzenmodus === 1 + ); + $this->setzeFormularParameter( + $formular['elements'], + 'MonatsgrenzenUmschalten', + 'visible', + $lastspitzenmodus === 2 + ); + $this->setzeFormularParameter( + $formular['elements'], + 'MonatsgrenzenUmschalten', + 'caption', + $monatsgrenzenSichtbar ? 'Monatsgrenzen ausblenden' : 'Monatsgrenzen anzeigen' + ); + $this->setzeFormularParameter( + $formular['elements'], + 'Monatsgrenzen', + 'visible', + $monatsgrenzenSichtbar + ); + $this->setzeFormularParameter( + $formular['elements'], + 'Monatsgrenzen', + 'values', + $this->monatsgrenzenFuerFormular() + ); + + return json_encode($formular, JSON_THROW_ON_ERROR); + } + public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void { if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID') @@ -123,6 +188,39 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->regeln((bool) $wert); return; + case 'FormAutomatischeSuche': + $automatisch = (bool) $wert; + $this->UpdateFormField('VerbraucherZuordnung', 'visible', !$automatisch); + $this->UpdateFormField('AutomatischeVerbraucherZuordnung', 'visible', $automatisch); + $this->UpdateFormField('SuchbereichID', 'visible', $automatisch); + return; + + case 'FormLastspitzenmodus': + $modus = (int) $wert; + $this->UpdateFormField('Lastspitzengrenze', 'visible', $modus === 1); + $this->UpdateFormField('MonatsgrenzenUmschalten', 'visible', $modus === 2); + if ($modus !== 2) { + $this->SetBuffer('MonatsgrenzenSichtbar', '0'); + $this->UpdateFormField('Monatsgrenzen', 'visible', false); + $this->UpdateFormField( + 'MonatsgrenzenUmschalten', + 'caption', + 'Monatsgrenzen anzeigen' + ); + } + return; + + case 'FormMonatsgrenzenUmschalten': + $sichtbar = $this->GetBuffer('MonatsgrenzenSichtbar') !== '1'; + $this->SetBuffer('MonatsgrenzenSichtbar', $sichtbar ? '1' : '0'); + $this->UpdateFormField('Monatsgrenzen', 'visible', $sichtbar); + $this->UpdateFormField( + 'MonatsgrenzenUmschalten', + 'caption', + $sichtbar ? 'Monatsgrenzen ausblenden' : 'Monatsgrenzen anzeigen' + ); + return; + case 'VerbraucherdatenEmpfangen': if (!is_string($wert)) { throw new InvalidArgumentException('Verbraucherdaten muessen als JSON uebergeben werden.'); @@ -134,14 +232,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->VerbraucherdatenEmpfangen($daten); return; - case 'Monatsgrenzen': - if (!$this->ReadPropertyBoolean('MonatsgrenzenVisuBedienbar')) { - throw new RuntimeException('Monatsgrenzen sind in der Visualisierung nicht bedienbar.'); - } - $this->dekodiereMonatsgrenzen((string) $wert); - IPS_SetProperty($this->InstanceID, 'Monatsgrenzen', (string) $wert); - IPS_ApplyChanges($this->InstanceID); - return; } throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident); @@ -200,8 +290,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle $netzleistung, (string) $this->GetValue('Betriebsart'), $lastspitzengrenze !== null, - $this->ReadPropertyFloat('UmschaltschwelleUeberschuss'), - $this->ReadPropertyFloat('UmschaltschwelleLastspitze'), $this->ReadPropertyFloat('SollwertSolarladen'), $lastspitzengrenze ?? 0.0, $this->ReadPropertyFloat('Umschaltdifferenz') @@ -297,9 +385,30 @@ class Manager extends IPSModule implements ManagerSchnittstelle /** @return list */ private function aktiveVerbraucherIDs(): array { - $zuordnung = json_decode($this->ReadPropertyString('VerbraucherZuordnung'), true, 512, JSON_THROW_ON_ERROR); + if (!$this->ReadPropertyBoolean('AutomatischeSuche')) { + return $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung'); + } + + $gespeichert = json_decode( + $this->ReadPropertyString('AutomatischeVerbraucherZuordnung'), + true, + 512, + JSON_THROW_ON_ERROR + ); + if ($gespeichert === []) { + // Bestehende Installationen behalten ihre Auswahl, bis die neue Liste gespeichert wurde. + return $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung'); + } + + return $this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung'); + } + + /** @return list */ + private function leseAktiveVerbraucherIDs(string $property): array + { + $zuordnung = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR); if (!is_array($zuordnung)) { - throw new InvalidArgumentException('VerbraucherZuordnung muss eine JSON-Liste sein.'); + throw new InvalidArgumentException($property . ' muss eine JSON-Liste sein.'); } $ids = []; @@ -319,6 +428,102 @@ class Manager extends IPSModule implements ManagerSchnittstelle return array_values(array_unique($ids)); } + /** @return list */ + private function automatischeVerbraucherFuerFormular(): array + { + $gespeichert = json_decode( + $this->ReadPropertyString('AutomatischeVerbraucherZuordnung'), + true, + 512, + JSON_THROW_ON_ERROR + ); + $ausgewaehlt = $gespeichert === [] + ? $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung') + : $this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung'); + + $suchbereichID = $this->ReadPropertyInteger('SuchbereichID'); + $werte = []; + foreach (self::VERBRAUCHER_MODUL_IDS as $modulID) { + foreach (IPS_GetInstanceListByModuleID($modulID) as $instanzID) { + if (!$this->istImSuchbereich($instanzID, $suchbereichID)) { + continue; + } + $werte[] = [ + 'Name' => IPS_GetName($instanzID), + 'InstanzID' => $instanzID, + 'Aktiv' => in_array($instanzID, $ausgewaehlt, true), + ]; + } + } + + usort($werte, static function (array $links, array $rechts): int { + $nameVergleich = strcasecmp($links['Name'], $rechts['Name']); + + return $nameVergleich !== 0 + ? $nameVergleich + : $links['InstanzID'] <=> $rechts['InstanzID']; + }); + + return $werte; + } + + private function istImSuchbereich(int $objektID, int $suchbereichID): bool + { + if ($suchbereichID <= 0) { + return true; + } + + while ($objektID > 0) { + if ($objektID === $suchbereichID) { + return true; + } + $objektID = IPS_GetParent($objektID); + } + + return false; + } + + /** @return list */ + private function monatsgrenzenFuerFormular(): array + { + $grenzen = $this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')); + $werte = []; + foreach (self::MONATSNAMEN as $monat => $name) { + $werte[] = [ + 'Monat' => $name, + 'Grenze_W' => $grenzen[$monat], + 'MonatIndex' => $monat, + ]; + } + + return $werte; + } + + /** @param list> $elemente */ + private function setzeFormularParameter( + array &$elemente, + string $name, + string $parameter, + $wert + ): bool { + foreach ($elemente as &$element) { + if (($element['name'] ?? null) === $name) { + $element[$parameter] = $wert; + + return true; + } + if (isset($element['items']) + && is_array($element['items']) + && $this->setzeFormularParameter($element['items'], $name, $parameter, $wert) + ) { + return true; + } + } + unset($element); + + return false; + } + private function sendeSollwert(int $instanzID, int $sollleistung): void { if (!IPS_InstanceExists($instanzID)) { @@ -381,13 +586,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle ] as $ident) { IPS_SetHidden($this->GetIDForIdent($ident), !$logging); } - $monatsgrenzenBedienbar = $logging && $this->ReadPropertyBoolean('MonatsgrenzenVisuBedienbar'); - IPS_SetHidden($this->GetIDForIdent('Monatsgrenzen'), !$monatsgrenzenBedienbar); - if ($monatsgrenzenBedienbar) { - $this->EnableAction('Monatsgrenzen'); - } else { - $this->DisableAction('Monatsgrenzen'); - } + IPS_SetHidden($this->GetIDForIdent('Monatsgrenzen'), true); + $this->DisableAction('Monatsgrenzen'); } private function aktualisiereAnschlussstatus(): void @@ -406,7 +606,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle private function pruefeKonfiguration(): void { - foreach (['MesswertMaxAlter', 'Regelintervall', 'Vorgabeintervall', 'VerbraucherTimeout'] as $property) { + foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) { if ($this->ReadPropertyInteger($property) <= 0) { throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); } @@ -416,7 +616,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle ) { throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 liegen.'); } - $this->aktiveVerbraucherIDs(); + $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung'); + $this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung'); $this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')); foreach (['PrognoseAnschluss', 'SDLAnschluss', 'LizenzAnschluss', 'StoermeldeAnschluss'] as $property) { $wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR); @@ -434,13 +635,35 @@ class Manager extends IPSModule implements ManagerSchnittstelle throw new InvalidArgumentException('Monatsgrenzen muss ein JSON-Objekt sein.'); } $ergebnis = []; - for ($monat = 1; $monat <= 12; ++$monat) { - $wert = $werte[(string) $monat] ?? $werte[$monat] ?? null; - if (!is_int($wert) && !is_float($wert)) { - throw new InvalidArgumentException('Monatsgrenzen muss fuer jeden Monat einen Zahlenwert enthalten.'); + if (isset($werte[0]) && is_array($werte[0])) { + foreach ($werte as $eintrag) { + if (!is_array($eintrag)) { + throw new InvalidArgumentException('Monatsgrenzen muss eine Liste von Monatswerten sein.'); + } + $monat = $eintrag['MonatIndex'] ?? null; + $wert = $eintrag['Grenze_W'] ?? null; + if (!is_int($monat) || $monat < 1 || $monat > 12 + || (!is_int($wert) && !is_float($wert)) + ) { + throw new InvalidArgumentException('Jeder Monatswert benoetigt Monatsnummer und Grenze.'); + } + $ergebnis[$monat] = (float) $wert; + } + } else { + for ($monat = 1; $monat <= 12; ++$monat) { + $wert = $werte[(string) $monat] ?? $werte[$monat] ?? null; + if (!is_int($wert) && !is_float($wert)) { + throw new InvalidArgumentException( + 'Monatsgrenzen muss fuer jeden Monat einen Zahlenwert enthalten.' + ); + } + $ergebnis[$monat] = (float) $wert; } - $ergebnis[$monat] = (float) $wert; } + if (count($ergebnis) !== 12) { + throw new InvalidArgumentException('Monatsgrenzen muss genau zwoelf Monatswerte enthalten.'); + } + ksort($ergebnis); return $ergebnis; } @@ -455,7 +678,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle private function standardMonatsgrenzen(): string { - return json_encode(array_fill_keys(range(1, 12), 0), JSON_THROW_ON_ERROR); + $werte = []; + foreach (self::MONATSNAMEN as $monat => $name) { + $werte[] = [ + 'Monat' => $name, + 'Grenze_W' => 0.0, + 'MonatIndex' => $monat, + ]; + } + + return json_encode($werte, JSON_THROW_ON_ERROR); } /** @param mixed $daten */ diff --git a/docs/Schnittstelle.md b/docs/Schnittstelle.md index e25fad5..b48652c 100644 --- a/docs/Schnittstelle.md +++ b/docs/Schnittstelle.md @@ -98,7 +98,7 @@ Symcon-Datenpunkte werden durch `VerbraucherBasisTrait` registriert. | `PrioritaetPV` | Integer | `0` | Priorität ab 0; kleinere Zahl bedeutet höhere Priorität. | | `PrioritaetPeak` | Integer | `0` | Wie PV-Priorität, ohne fachliche Obergrenze. | | `Meldeintervall` | Integer | `10` | Vollständige Rückmeldung in Sekunden; muss grösser als 0 sein. | -| `VorgabeTimeout` | Integer | `30` | Sollwert wird nach dieser Zeit ohne Erneuerung ungültig. | +| `VorgabeTimeout` | Integer | `120` | Sollwert wird nach dieser Zeit ohne Erneuerung ungültig. | | `EinstellungenInVisu` | Boolean | `false` | Blendet lokale Einstellvariablen ein und macht sie geprüft bedienbar. | | `LoggingEin` | Boolean | `false` | Aktiviert das laufende Diagnoseprotokoll. Module können zusätzliche Diagnosevariablen getrennt einblenden. | @@ -125,7 +125,7 @@ ergänzt. - Rückmeldung nach dem Start, bei relevanten Änderungen und zusätzlich alle `Meldeintervall` Sekunden. -- Laufende Vorgaben werden vom Manager standardmässig alle 10 Sekunden erneuert. +- Laufende Vorgaben werden vom Manager standardmässig alle 60 Sekunden erneuert. - Nach `VorgabeTimeout` Sekunden ist eine nicht erneuerte Vorgabe ungültig. - Nach einem Neustart wird zuerst der Gerätezustand erfasst; eine alte Vorgabe wird nicht ungeprüft wieder aufgenommen. diff --git a/docs/module/Manager/README.md b/docs/module/Manager/README.md index 35f7cbf..25abac4 100644 --- a/docs/module/Manager/README.md +++ b/docs/module/Manager/README.md @@ -35,21 +35,18 @@ Ohne Logging sind nur `Aktiv`, `Betriebsart` und `Netzleistung` sichtbar. | `Rolle` | Auswahl | `Alleine`; alternativ `Hauptmanager` oder `Untermanager`. | | `NetzleistungVariableID` | Integer | `0*`; gültige Messquelle vor Regelstart. | | `Netzleistungsfaktor` | Float | `1`; auf W und die vereinbarte Vorzeichenrichtung normieren. | -| `MesswertMaxAlter` | Integer | `30` s; auf echte Messaktualisierung bezogen. | -| `VerbraucherZuordnung` | String/JSON | `[]`; Instanz-ID und Aktiv je Verbraucher. Einzige gespeicherte Zuordnung. | -| `AutomatischeSuche` | Boolean | `true`; findet Kandidaten, aktiviert sie aber nicht automatisch. | -| `SuchbereichID` | Integer | `0`; optionaler Suchbereich. | -| `Regelintervall` | Integer | `2` s; zusätzlich auf relevante Änderungen reagieren. | -| `Vorgabeintervall` | Integer | `10` s; laufende Vorgaben erneuern. | -| `VerbraucherTimeout` | Integer | `30` s; ausgebliebene Meldung erkennen. | +| `MesswertMaxAlter` | Integer | `60` s; auf echte Messaktualisierung bezogen. | +| `VerbraucherZuordnung` | String/JSON | `[]`; manuelle Auswahl aus Instanz-ID und Aktiv-Status. | +| `AutomatischeVerbraucherZuordnung` | String/JSON | `[]`; Auswahl aus automatisch gefundenen Verbrauchern. | +| `AutomatischeSuche` | Boolean | `true`; zeigt gefundene Verbraucher zur gezielten Auswahl. | +| `SuchbereichID` | Integer | `0`; optionaler Suchbereich für die automatische Suche. | +| `KeepAlive` | Integer | `60` s; erneuert laufende Vorgaben zyklisch. | +| `VerbraucherTimeout` | Integer | `60` s; erkennt ausgebliebene Verbrauchermeldungen. | | `Lastspitzenmodus` | Auswahl | `Aus`; alternativ `Konstant` oder `Monatlich`. | | `Lastspitzengrenze` | Float | Feste Grenze in W für Modus `Konstant`. | -| `Monatsgrenzen` | String/JSON | Zwölf Monatswerte in W. | -| `MonatsgrenzenVisuBedienbar` | Boolean | `false`; Monatstabelle in der Visualisierung bedienen. | +| `Monatsgrenzen` | String/JSON | Editierbare Liste mit zwölf Monatswerten in W. | | `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. | -| `UmschaltschwelleUeberschuss` | Float | Feste W-Schwelle für PV-Betrieb. | -| `UmschaltschwelleLastspitze` | Float | Feste W-Schwelle für Peak-Betrieb. | -| `Umschaltdifferenz` | Float | `0` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. | +| `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. | | `PrognoseAnschluss` | String/JSON | Optionaler Forecast-Anschluss. | | `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. | | `LizenzAnschluss` | String/JSON | Optionaler Lizenzanschluss. | @@ -58,6 +55,9 @@ Ohne Logging sind nur `Aktiv`, `Betriebsart` und `Netzleistung` sichtbar. Es gibt keine Sollwertquellenauswahl und keine zweite Prioritätseinstellung im Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt. +Die Monatsgrenzen werden in den Grundeinstellungen über eine Schaltfläche +ein- und ausgeblendet. Die vier optionalen oberen Anschlüsse besitzen jeweils +einen eigenen Bereich. ## Verteilalgorithmus @@ -75,15 +75,15 @@ Bereiche werden direkt verarbeitet; Lücken werden nie durch unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im `Verbraucherstatus` dokumentiert. -Die Betriebsart wechselt unterhalb der Überschussschwelle zu `PV` und oberhalb -der Lastspitzenschwelle zu `Peak`. Zwischen den Schwellen bleibt sie erhalten. -`Umschaltdifferenz` verhindert zusätzlich einen Wechsel, wenn sich die beiden -berechneten Korrekturen nicht ausreichend unterscheiden. +Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb +der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten +bleibt sie erhalten. `Umschaltdifferenz` verhindert zusätzlich einen Wechsel, +wenn sich die beiden berechneten Korrekturen nicht ausreichend unterscheiden. ## Laufzeit und Fehlerverhalten -- Netzleistungsänderungen und der Regel-Timer lösen die Berechnung aus. -- Identische Sollwerte werden nur beim Vorgabeintervall erneut gesendet. +- Netzleistungsänderungen und Verbrauchermeldungen lösen die Berechnung aus. +- Identische Sollwerte werden nur beim Keep-alive erneut gesendet. - Verbraucherpakete werden zentral geprüft und nur von aktiv zugeordneten Absendern angenommen. - Veraltete oder noch fehlende Verbrauchermeldungen werden nicht verteilt und diff --git a/libs/ManagerRegler.php b/libs/ManagerRegler.php index 5ca1d2f..b8fe7d2 100644 --- a/libs/ManagerRegler.php +++ b/libs/ManagerRegler.php @@ -50,8 +50,6 @@ final class ManagerRegler float $netzleistung, string $bisherigeBetriebsart, bool $peakAktiv, - float $umschaltschwelleUeberschuss, - float $umschaltschwelleLastspitze, float $sollwertSolarladen, float $wirksameLastspitzengrenze, float $umschaltdifferenzProzent @@ -67,9 +65,9 @@ final class ManagerRegler } $kandidat = $bisherigeBetriebsart; - if ($netzleistung < $umschaltschwelleUeberschuss) { + if ($netzleistung < $sollwertSolarladen) { $kandidat = self::BETRIEBSART_PV; - } elseif ($netzleistung > $umschaltschwelleLastspitze) { + } elseif ($netzleistung > $wirksameLastspitzengrenze) { $kandidat = self::BETRIEBSART_PEAK; } elseif (!in_array($kandidat, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)) { $kandidat = self::BETRIEBSART_PV; diff --git a/libs/VerbraucherBasisTrait.php b/libs/VerbraucherBasisTrait.php index ac69d9b..083d324 100644 --- a/libs/VerbraucherBasisTrait.php +++ b/libs/VerbraucherBasisTrait.php @@ -17,7 +17,7 @@ trait VerbraucherBasisTrait $this->RegisterPropertyInteger('PrioritaetPV', 0); $this->RegisterPropertyInteger('PrioritaetPeak', 0); $this->RegisterPropertyInteger('Meldeintervall', 10); - $this->RegisterPropertyInteger('VorgabeTimeout', 30); + $this->RegisterPropertyInteger('VorgabeTimeout', 120); $this->RegisterPropertyBoolean('EinstellungenInVisu', false); $this->RegisterPropertyBoolean('LoggingEin', false); diff --git a/tests/ManagerModulstrukturTest.php b/tests/ManagerModulstrukturTest.php index b85cfc5..a7c21d8 100644 --- a/tests/ManagerModulstrukturTest.php +++ b/tests/ManagerModulstrukturTest.php @@ -40,14 +40,46 @@ final class ManagerModulstrukturTest extends TestCase foreach ([ 'NetzleistungVariableID', 'VerbraucherZuordnung', + 'AutomatischeVerbraucherZuordnung', 'Lastspitzenmodus', 'Monatsgrenzen', 'SollwertSolarladen', + 'KeepAlive', + 'VerbraucherTimeout', + 'MesswertMaxAlter', 'Umschaltdifferenz', 'LoggingEin', ] as $property) { self::assertStringContainsString($property, $json); } + foreach (['Regelintervall', 'Vorgabeintervall', 'UmschaltschwelleUeberschuss', + 'UmschaltschwelleLastspitze', 'Optionale obere Anschluesse'] as $entfernt + ) { + self::assertStringNotContainsString($entfernt, $json); + } + foreach (['Grundeinstellungen', 'Erweiterte Einstellungen', 'Prognose / Forecast', + 'SDL / VGT', 'Lizenzierung', 'Stoerueberwachung'] as $bereich + ) { + self::assertStringContainsString($bereich, $json); + } + } + + public function testManagerVerwendetDieNeuenZeitdefaults(): void + { + $inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php'); + + self::assertStringContainsString("RegisterPropertyInteger('KeepAlive', 60)", $inhalt); + self::assertStringContainsString("RegisterPropertyInteger('VerbraucherTimeout', 60)", $inhalt); + self::assertStringContainsString("RegisterPropertyInteger('MesswertMaxAlter', 60)", $inhalt); + self::assertStringContainsString("RegisterPropertyFloat('Umschaltdifferenz', 5.0)", $inhalt); + self::assertStringNotContainsString("RegisterPropertyInteger('Regelintervall'", $inhalt); + self::assertStringNotContainsString("RegisterPropertyInteger('Vorgabeintervall'", $inhalt); + + $verbraucherBasis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php'); + self::assertStringContainsString( + "RegisterPropertyInteger('VorgabeTimeout', 120)", + $verbraucherBasis + ); } public function testManagerImplementiertDenGemeinsamenEmpfangsvertrag(): void diff --git a/tests/ManagerReglerTest.php b/tests/ManagerReglerTest.php index f4ea4e3..5f75928 100644 --- a/tests/ManagerReglerTest.php +++ b/tests/ManagerReglerTest.php @@ -21,8 +21,6 @@ final class ManagerReglerTest extends TestCase 25000.0, ManagerRegler::BETRIEBSART_INAKTIV, false, - -100.0, - 10000.0, 0.0, 15000.0, 0.0 @@ -30,7 +28,7 @@ final class ManagerReglerTest extends TestCase ); } - public function testBetriebsartBleibtZwischenDenSchwellenErhalten(): void + public function testBetriebsartBleibtZwischenDenZielwertenErhalten(): void { self::assertSame( ManagerRegler::BETRIEBSART_PEAK, @@ -41,8 +39,6 @@ final class ManagerReglerTest extends TestCase ManagerRegler::BETRIEBSART_PEAK, true, 0.0, - 10000.0, - 0.0, 12000.0, 0.0 ) @@ -59,8 +55,6 @@ final class ManagerReglerTest extends TestCase 11000.0, ManagerRegler::BETRIEBSART_PV, true, - 0.0, - 10000.0, 10000.0, 10500.0, 60.0 @@ -68,6 +62,40 @@ final class ManagerReglerTest extends TestCase ); } + public function testWechseltOberhalbDerPeakgrenzeInDenPeakbetrieb(): void + { + self::assertSame( + ManagerRegler::BETRIEBSART_PEAK, + ManagerRegler::bestimmeBetriebsart( + true, + true, + 12500.0, + ManagerRegler::BETRIEBSART_PV, + true, + 0.0, + 12000.0, + 5.0 + ) + ); + } + + public function testWechseltUnterhalbDesSolarsollwertsInDenPvBetrieb(): void + { + self::assertSame( + ManagerRegler::BETRIEBSART_PV, + ManagerRegler::bestimmeBetriebsart( + true, + true, + -500.0, + ManagerRegler::BETRIEBSART_PEAK, + true, + 0.0, + 12000.0, + 5.0 + ) + ); + } + public function testVerteilungBeruecksichtigtPrioritaetEinzelwerteUndBereiche(): void { $ergebnis = ManagerRegler::verteile(