Manager-Konfiguration neu strukturieren
Tests / test (push) Failing after 46s

This commit is contained in:
dh
2026-09-17 08:55:43 +00:00
parent 9e6a675b25
commit 1b4a788f22
8 changed files with 601 additions and 212 deletions
+175 -76
View File
@@ -1,5 +1,10 @@
{ {
"elements": [ "elements": [
{
"type": "ExpansionPanel",
"caption": "Grundeinstellungen",
"expanded": true,
"items": [
{ {
"type": "Select", "type": "Select",
"name": "Rolle", "name": "Rolle",
@@ -17,24 +22,96 @@
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Netzleistungsfaktor", "name": "SollwertSolarladen",
"caption": "Netzleistungsfaktor", "caption": "Sollwert Solarladen",
"digits": 4 "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", "type": "NumberSpinner",
"name": "MesswertMaxAlter", "name": "Lastspitzengrenze",
"caption": "Maximales Messwertalter", "caption": "Lastspitzengrenze",
"suffix": " s", "suffix": " W",
"minimum": 1 "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,
"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", "type": "List",
"name": "VerbraucherZuordnung", "name": "VerbraucherZuordnung",
"caption": "Verbraucher", "caption": "Verbraucher manuell auswaehlen",
"rowCount": 6,
"add": true, "add": true,
"delete": true, "delete": true,
"sortable": true, "sortable": true,
"visible": false,
"columns": [ "columns": [
{ {
"caption": "Instanz", "caption": "Instanz",
@@ -53,26 +130,47 @@
] ]
}, },
{ {
"type": "CheckBox", "type": "List",
"name": "AutomatischeSuche", "name": "AutomatischeVerbraucherZuordnung",
"caption": "Verbraucher automatisch suchen" "caption": "Gefundene Verbraucher",
"rowCount": 8,
"add": false,
"delete": false,
"loadValuesFromConfiguration": false,
"visible": true,
"columns": [
{
"caption": "Verbraucher",
"name": "Name",
"width": "auto",
"save": true
}, },
{ {
"type": "SelectObject", "caption": "Instanz-ID",
"name": "SuchbereichID", "name": "InstanzID",
"caption": "Suchbereich" "width": "100px",
"save": true
}, },
{
"caption": "Verwenden",
"name": "Aktiv",
"width": "100px",
"save": true,
"edit": {"type": "CheckBox"}
}
],
"values": []
}
]
},
{
"type": "ExpansionPanel",
"caption": "Erweiterte Einstellungen",
"items": [
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Regelintervall", "name": "KeepAlive",
"caption": "Regelintervall", "caption": "Keep alive",
"suffix": " s",
"minimum": 1
},
{
"type": "NumberSpinner",
"name": "Vorgabeintervall",
"caption": "Vorgabeintervall",
"suffix": " s", "suffix": " s",
"minimum": 1 "minimum": 1
}, },
@@ -83,53 +181,12 @@
"suffix": " s", "suffix": " s",
"minimum": 1 "minimum": 1
}, },
{
"type": "Select",
"name": "Lastspitzenmodus",
"caption": "Lastspitzenmodus",
"options": [
{"caption": "Aus", "value": 0},
{"caption": "Konstant", "value": 1},
{"caption": "Monatlich", "value": 2}
]
},
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Lastspitzengrenze", "name": "MesswertMaxAlter",
"caption": "Lastspitzengrenze", "caption": "Maximales Messwertalter",
"suffix": " W", "suffix": " s",
"digits": 1 "minimum": 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", "type": "NumberSpinner",
@@ -141,20 +198,62 @@
"digits": 1 "digits": 1
}, },
{ {
"type": "ExpansionPanel", "type": "NumberSpinner",
"caption": "Optionale obere Anschluesse", "name": "Netzleistungsfaktor",
"items": [ "caption": "Netzleistungsfaktor",
{"type": "ValidationTextBox", "name": "PrognoseAnschluss", "caption": "Prognose / Forecast als JSON"}, "digits": 4
{"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": "CheckBox", "type": "CheckBox",
"name": "LoggingEin", "name": "LoggingEin",
"caption": "Logging ein" "caption": "Logging ein"
} }
]
},
{
"type": "ExpansionPanel",
"caption": "Prognose / Forecast",
"items": [
{
"type": "ValidationTextBox",
"name": "PrognoseAnschluss",
"caption": "Konfiguration als JSON"
}
]
},
{
"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": [ "actions": [
{ {
+273 -41
View File
@@ -16,6 +16,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private const STATUS_MESSWERT_UNGUELTIG = 201; private const STATUS_MESSWERT_UNGUELTIG = 201;
private const STATUS_KONFIGURATION_UNGUELTIG = 202; private const STATUS_KONFIGURATION_UNGUELTIG = 202;
private const VM_UPDATE = 10603; 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 public function Create(): void
{ {
@@ -24,21 +31,18 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('Rolle', 0); $this->RegisterPropertyInteger('Rolle', 0);
$this->RegisterPropertyInteger('NetzleistungVariableID', 0); $this->RegisterPropertyInteger('NetzleistungVariableID', 0);
$this->RegisterPropertyFloat('Netzleistungsfaktor', 1.0); $this->RegisterPropertyFloat('Netzleistungsfaktor', 1.0);
$this->RegisterPropertyInteger('MesswertMaxAlter', 30); $this->RegisterPropertyInteger('MesswertMaxAlter', 60);
$this->RegisterPropertyString('VerbraucherZuordnung', '[]'); $this->RegisterPropertyString('VerbraucherZuordnung', '[]');
$this->RegisterPropertyString('AutomatischeVerbraucherZuordnung', '[]');
$this->RegisterPropertyBoolean('AutomatischeSuche', true); $this->RegisterPropertyBoolean('AutomatischeSuche', true);
$this->RegisterPropertyInteger('SuchbereichID', 0); $this->RegisterPropertyInteger('SuchbereichID', 0);
$this->RegisterPropertyInteger('Regelintervall', 2); $this->RegisterPropertyInteger('KeepAlive', 60);
$this->RegisterPropertyInteger('Vorgabeintervall', 10); $this->RegisterPropertyInteger('VerbraucherTimeout', 60);
$this->RegisterPropertyInteger('VerbraucherTimeout', 30);
$this->RegisterPropertyInteger('Lastspitzenmodus', 0); $this->RegisterPropertyInteger('Lastspitzenmodus', 0);
$this->RegisterPropertyFloat('Lastspitzengrenze', 0.0); $this->RegisterPropertyFloat('Lastspitzengrenze', 0.0);
$this->RegisterPropertyString('Monatsgrenzen', $this->standardMonatsgrenzen()); $this->RegisterPropertyString('Monatsgrenzen', $this->standardMonatsgrenzen());
$this->RegisterPropertyBoolean('MonatsgrenzenVisuBedienbar', false);
$this->RegisterPropertyFloat('SollwertSolarladen', 0.0); $this->RegisterPropertyFloat('SollwertSolarladen', 0.0);
$this->RegisterPropertyFloat('UmschaltschwelleUeberschuss', 0.0); $this->RegisterPropertyFloat('Umschaltdifferenz', 5.0);
$this->RegisterPropertyFloat('UmschaltschwelleLastspitze', 0.0);
$this->RegisterPropertyFloat('Umschaltdifferenz', 0.0);
$this->RegisterPropertyString('PrognoseAnschluss', '{}'); $this->RegisterPropertyString('PrognoseAnschluss', '{}');
$this->RegisterPropertyString('SDLAnschluss', '{}'); $this->RegisterPropertyString('SDLAnschluss', '{}');
$this->RegisterPropertyString('LizenzAnschluss', '{}'); $this->RegisterPropertyString('LizenzAnschluss', '{}');
@@ -67,13 +71,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeString('LetzteSollwerte', '{}'); $this->RegisterAttributeString('LetzteSollwerte', '{}');
$this->RegisterAttributeInteger('RegistrierteNetzleistungVariableID', 0); $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( $this->RegisterTimer(
'Regelzyklus', 'KeepAlive',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', false);"
);
$this->RegisterTimer(
'VorgabenErneuern',
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);" "IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
); );
@@ -89,19 +91,82 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0); $this->SetTimerInterval('VorgabenErneuern', 0);
$this->SetTimerInterval('KeepAlive', 0);
$this->protokolliere('Konfiguration', $fehler->getMessage()); $this->protokolliere('Konfiguration', $fehler->getMessage());
return; return;
} }
$this->registriereNetzleistungsmeldung(); $this->registriereNetzleistungsmeldung();
$this->SetTimerInterval('Regelzyklus', $this->ReadPropertyInteger('Regelintervall') * 1000); $this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', $this->ReadPropertyInteger('Vorgabeintervall') * 1000); $this->SetTimerInterval('VorgabenErneuern', 0);
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
$this->SetValue('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen')); $this->SetValue('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
$this->aktualisiereSichtbarkeit(); $this->aktualisiereSichtbarkeit();
$this->aktualisiereAnschlussstatus(); $this->aktualisiereAnschlussstatus();
$this->SetStatus(self::STATUS_AKTIV); $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 public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
{ {
if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID') if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID')
@@ -123,6 +188,39 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->regeln((bool) $wert); $this->regeln((bool) $wert);
return; 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': case 'VerbraucherdatenEmpfangen':
if (!is_string($wert)) { if (!is_string($wert)) {
throw new InvalidArgumentException('Verbraucherdaten muessen als JSON uebergeben werden.'); throw new InvalidArgumentException('Verbraucherdaten muessen als JSON uebergeben werden.');
@@ -134,14 +232,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->VerbraucherdatenEmpfangen($daten); $this->VerbraucherdatenEmpfangen($daten);
return; 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); throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
@@ -200,8 +290,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$netzleistung, $netzleistung,
(string) $this->GetValue('Betriebsart'), (string) $this->GetValue('Betriebsart'),
$lastspitzengrenze !== null, $lastspitzengrenze !== null,
$this->ReadPropertyFloat('UmschaltschwelleUeberschuss'),
$this->ReadPropertyFloat('UmschaltschwelleLastspitze'),
$this->ReadPropertyFloat('SollwertSolarladen'), $this->ReadPropertyFloat('SollwertSolarladen'),
$lastspitzengrenze ?? 0.0, $lastspitzengrenze ?? 0.0,
$this->ReadPropertyFloat('Umschaltdifferenz') $this->ReadPropertyFloat('Umschaltdifferenz')
@@ -297,9 +385,30 @@ class Manager extends IPSModule implements ManagerSchnittstelle
/** @return list<int> */ /** @return list<int> */
private function aktiveVerbraucherIDs(): array 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<int> */
private function leseAktiveVerbraucherIDs(string $property): array
{
$zuordnung = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($zuordnung)) { if (!is_array($zuordnung)) {
throw new InvalidArgumentException('VerbraucherZuordnung muss eine JSON-Liste sein.'); throw new InvalidArgumentException($property . ' muss eine JSON-Liste sein.');
} }
$ids = []; $ids = [];
@@ -319,6 +428,102 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return array_values(array_unique($ids)); return array_values(array_unique($ids));
} }
/** @return list<array{Name: string, InstanzID: int, Aktiv: bool}> */
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<array{Monat: string, Grenze_W: float, MonatIndex: int}> */
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<array<string, mixed>> $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 private function sendeSollwert(int $instanzID, int $sollleistung): void
{ {
if (!IPS_InstanceExists($instanzID)) { if (!IPS_InstanceExists($instanzID)) {
@@ -381,14 +586,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
] as $ident) { ] as $ident) {
IPS_SetHidden($this->GetIDForIdent($ident), !$logging); IPS_SetHidden($this->GetIDForIdent($ident), !$logging);
} }
$monatsgrenzenBedienbar = $logging && $this->ReadPropertyBoolean('MonatsgrenzenVisuBedienbar'); IPS_SetHidden($this->GetIDForIdent('Monatsgrenzen'), true);
IPS_SetHidden($this->GetIDForIdent('Monatsgrenzen'), !$monatsgrenzenBedienbar);
if ($monatsgrenzenBedienbar) {
$this->EnableAction('Monatsgrenzen');
} else {
$this->DisableAction('Monatsgrenzen'); $this->DisableAction('Monatsgrenzen');
} }
}
private function aktualisiereAnschlussstatus(): void private function aktualisiereAnschlussstatus(): void
{ {
@@ -406,7 +606,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function pruefeKonfiguration(): void private function pruefeKonfiguration(): void
{ {
foreach (['MesswertMaxAlter', 'Regelintervall', 'Vorgabeintervall', 'VerbraucherTimeout'] as $property) { foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) { if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); 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.'); throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 liegen.');
} }
$this->aktiveVerbraucherIDs(); $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')); $this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
foreach (['PrognoseAnschluss', 'SDLAnschluss', 'LizenzAnschluss', 'StoermeldeAnschluss'] as $property) { foreach (['PrognoseAnschluss', 'SDLAnschluss', 'LizenzAnschluss', 'StoermeldeAnschluss'] as $property) {
$wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR); $wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
@@ -434,13 +635,35 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new InvalidArgumentException('Monatsgrenzen muss ein JSON-Objekt sein.'); throw new InvalidArgumentException('Monatsgrenzen muss ein JSON-Objekt sein.');
} }
$ergebnis = []; $ergebnis = [];
for ($monat = 1; $monat <= 12; ++$monat) { if (isset($werte[0]) && is_array($werte[0])) {
$wert = $werte[(string) $monat] ?? $werte[$monat] ?? null; foreach ($werte as $eintrag) {
if (!is_int($wert) && !is_float($wert)) { if (!is_array($eintrag)) {
throw new InvalidArgumentException('Monatsgrenzen muss fuer jeden Monat einen Zahlenwert enthalten.'); 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; $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;
}
}
if (count($ergebnis) !== 12) {
throw new InvalidArgumentException('Monatsgrenzen muss genau zwoelf Monatswerte enthalten.');
}
ksort($ergebnis);
return $ergebnis; return $ergebnis;
} }
@@ -455,7 +678,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function standardMonatsgrenzen(): string 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 */ /** @param mixed $daten */
+2 -2
View File
@@ -98,7 +98,7 @@ Symcon-Datenpunkte werden durch `VerbraucherBasisTrait` registriert.
| `PrioritaetPV` | Integer | `0` | Priorität ab 0; kleinere Zahl bedeutet höhere Priorität. | | `PrioritaetPV` | Integer | `0` | Priorität ab 0; kleinere Zahl bedeutet höhere Priorität. |
| `PrioritaetPeak` | Integer | `0` | Wie PV-Priorität, ohne fachliche Obergrenze. | | `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. | | `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. | | `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. | | `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 - Rückmeldung nach dem Start, bei relevanten Änderungen und zusätzlich alle
`Meldeintervall` Sekunden. `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 `VorgabeTimeout` Sekunden ist eine nicht erneuerte Vorgabe ungültig.
- Nach einem Neustart wird zuerst der Gerätezustand erfasst; eine alte Vorgabe - Nach einem Neustart wird zuerst der Gerätezustand erfasst; eine alte Vorgabe
wird nicht ungeprüft wieder aufgenommen. wird nicht ungeprüft wieder aufgenommen.
+18 -18
View File
@@ -35,21 +35,18 @@ Ohne Logging sind nur `Aktiv`, `Betriebsart` und `Netzleistung` sichtbar.
| `Rolle` | Auswahl | `Alleine`; alternativ `Hauptmanager` oder `Untermanager`. | | `Rolle` | Auswahl | `Alleine`; alternativ `Hauptmanager` oder `Untermanager`. |
| `NetzleistungVariableID` | Integer | `0*`; gültige Messquelle vor Regelstart. | | `NetzleistungVariableID` | Integer | `0*`; gültige Messquelle vor Regelstart. |
| `Netzleistungsfaktor` | Float | `1`; auf W und die vereinbarte Vorzeichenrichtung normieren. | | `Netzleistungsfaktor` | Float | `1`; auf W und die vereinbarte Vorzeichenrichtung normieren. |
| `MesswertMaxAlter` | Integer | `30` s; auf echte Messaktualisierung bezogen. | | `MesswertMaxAlter` | Integer | `60` s; auf echte Messaktualisierung bezogen. |
| `VerbraucherZuordnung` | String/JSON | `[]`; Instanz-ID und Aktiv je Verbraucher. Einzige gespeicherte Zuordnung. | | `VerbraucherZuordnung` | String/JSON | `[]`; manuelle Auswahl aus Instanz-ID und Aktiv-Status. |
| `AutomatischeSuche` | Boolean | `true`; findet Kandidaten, aktiviert sie aber nicht automatisch. | | `AutomatischeVerbraucherZuordnung` | String/JSON | `[]`; Auswahl aus automatisch gefundenen Verbrauchern. |
| `SuchbereichID` | Integer | `0`; optionaler Suchbereich. | | `AutomatischeSuche` | Boolean | `true`; zeigt gefundene Verbraucher zur gezielten Auswahl. |
| `Regelintervall` | Integer | `2` s; zusätzlich auf relevante Änderungen reagieren. | | `SuchbereichID` | Integer | `0`; optionaler Suchbereich für die automatische Suche. |
| `Vorgabeintervall` | Integer | `10` s; laufende Vorgaben erneuern. | | `KeepAlive` | Integer | `60` s; erneuert laufende Vorgaben zyklisch. |
| `VerbraucherTimeout` | Integer | `30` s; ausgebliebene Meldung erkennen. | | `VerbraucherTimeout` | Integer | `60` s; erkennt ausgebliebene Verbrauchermeldungen. |
| `Lastspitzenmodus` | Auswahl | `Aus`; alternativ `Konstant` oder `Monatlich`. | | `Lastspitzenmodus` | Auswahl | `Aus`; alternativ `Konstant` oder `Monatlich`. |
| `Lastspitzengrenze` | Float | Feste Grenze in W für Modus `Konstant`. | | `Lastspitzengrenze` | Float | Feste Grenze in W für Modus `Konstant`. |
| `Monatsgrenzen` | String/JSON | Zwölf Monatswerte in W. | | `Monatsgrenzen` | String/JSON | Editierbare Liste mit zwölf Monatswerten in W. |
| `MonatsgrenzenVisuBedienbar` | Boolean | `false`; Monatstabelle in der Visualisierung bedienen. |
| `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. | | `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. |
| `UmschaltschwelleUeberschuss` | Float | Feste W-Schwelle für PV-Betrieb. | | `Umschaltdifferenz` | Float | `5` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
| `UmschaltschwelleLastspitze` | Float | Feste W-Schwelle für Peak-Betrieb. |
| `Umschaltdifferenz` | Float | `0` %; Mindestdifferenz der berechneten Sollleistungen vor Umschaltung. |
| `PrognoseAnschluss` | String/JSON | Optionaler Forecast-Anschluss. | | `PrognoseAnschluss` | String/JSON | Optionaler Forecast-Anschluss. |
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. | | `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
| `LizenzAnschluss` | String/JSON | Optionaler Lizenzanschluss. | | `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 Es gibt keine Sollwertquellenauswahl und keine zweite Prioritätseinstellung im
Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt. Manager. Im Modus `Aus` bleibt Solarladen aktiv, nur die Peak-Begrenzung entfällt.
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 ## Verteilalgorithmus
@@ -75,15 +75,15 @@ Bereiche werden direkt verarbeitet; Lücken werden nie durch unzulässige Werte
geschlossen. Eine verbleibende Abweichung wird im `Verbraucherstatus` geschlossen. Eine verbleibende Abweichung wird im `Verbraucherstatus`
dokumentiert. dokumentiert.
Die Betriebsart wechselt unterhalb der Überschussschwelle zu `PV` und oberhalb Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
der Lastspitzenschwelle zu `Peak`. Zwischen den Schwellen bleibt sie erhalten. der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
`Umschaltdifferenz` verhindert zusätzlich einen Wechsel, wenn sich die beiden bleibt sie erhalten. `Umschaltdifferenz` verhindert zusätzlich einen Wechsel,
berechneten Korrekturen nicht ausreichend unterscheiden. wenn sich die beiden berechneten Korrekturen nicht ausreichend unterscheiden.
## Laufzeit und Fehlerverhalten ## Laufzeit und Fehlerverhalten
- Netzleistungsänderungen und der Regel-Timer lösen die Berechnung aus. - Netzleistungsänderungen und Verbrauchermeldungen lösen die Berechnung aus.
- Identische Sollwerte werden nur beim Vorgabeintervall erneut gesendet. - Identische Sollwerte werden nur beim Keep-alive erneut gesendet.
- Verbraucherpakete werden zentral geprüft und nur von aktiv zugeordneten - Verbraucherpakete werden zentral geprüft und nur von aktiv zugeordneten
Absendern angenommen. Absendern angenommen.
- Veraltete oder noch fehlende Verbrauchermeldungen werden nicht verteilt und - Veraltete oder noch fehlende Verbrauchermeldungen werden nicht verteilt und
+2 -4
View File
@@ -50,8 +50,6 @@ final class ManagerRegler
float $netzleistung, float $netzleistung,
string $bisherigeBetriebsart, string $bisherigeBetriebsart,
bool $peakAktiv, bool $peakAktiv,
float $umschaltschwelleUeberschuss,
float $umschaltschwelleLastspitze,
float $sollwertSolarladen, float $sollwertSolarladen,
float $wirksameLastspitzengrenze, float $wirksameLastspitzengrenze,
float $umschaltdifferenzProzent float $umschaltdifferenzProzent
@@ -67,9 +65,9 @@ final class ManagerRegler
} }
$kandidat = $bisherigeBetriebsart; $kandidat = $bisherigeBetriebsart;
if ($netzleistung < $umschaltschwelleUeberschuss) { if ($netzleistung < $sollwertSolarladen) {
$kandidat = self::BETRIEBSART_PV; $kandidat = self::BETRIEBSART_PV;
} elseif ($netzleistung > $umschaltschwelleLastspitze) { } elseif ($netzleistung > $wirksameLastspitzengrenze) {
$kandidat = self::BETRIEBSART_PEAK; $kandidat = self::BETRIEBSART_PEAK;
} elseif (!in_array($kandidat, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)) { } elseif (!in_array($kandidat, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)) {
$kandidat = self::BETRIEBSART_PV; $kandidat = self::BETRIEBSART_PV;
+1 -1
View File
@@ -17,7 +17,7 @@ trait VerbraucherBasisTrait
$this->RegisterPropertyInteger('PrioritaetPV', 0); $this->RegisterPropertyInteger('PrioritaetPV', 0);
$this->RegisterPropertyInteger('PrioritaetPeak', 0); $this->RegisterPropertyInteger('PrioritaetPeak', 0);
$this->RegisterPropertyInteger('Meldeintervall', 10); $this->RegisterPropertyInteger('Meldeintervall', 10);
$this->RegisterPropertyInteger('VorgabeTimeout', 30); $this->RegisterPropertyInteger('VorgabeTimeout', 120);
$this->RegisterPropertyBoolean('EinstellungenInVisu', false); $this->RegisterPropertyBoolean('EinstellungenInVisu', false);
$this->RegisterPropertyBoolean('LoggingEin', false); $this->RegisterPropertyBoolean('LoggingEin', false);
+32
View File
@@ -40,14 +40,46 @@ final class ManagerModulstrukturTest extends TestCase
foreach ([ foreach ([
'NetzleistungVariableID', 'NetzleistungVariableID',
'VerbraucherZuordnung', 'VerbraucherZuordnung',
'AutomatischeVerbraucherZuordnung',
'Lastspitzenmodus', 'Lastspitzenmodus',
'Monatsgrenzen', 'Monatsgrenzen',
'SollwertSolarladen', 'SollwertSolarladen',
'KeepAlive',
'VerbraucherTimeout',
'MesswertMaxAlter',
'Umschaltdifferenz', 'Umschaltdifferenz',
'LoggingEin', 'LoggingEin',
] as $property) { ] as $property) {
self::assertStringContainsString($property, $json); 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 public function testManagerImplementiertDenGemeinsamenEmpfangsvertrag(): void
+35 -7
View File
@@ -21,8 +21,6 @@ final class ManagerReglerTest extends TestCase
25000.0, 25000.0,
ManagerRegler::BETRIEBSART_INAKTIV, ManagerRegler::BETRIEBSART_INAKTIV,
false, false,
-100.0,
10000.0,
0.0, 0.0,
15000.0, 15000.0,
0.0 0.0
@@ -30,7 +28,7 @@ final class ManagerReglerTest extends TestCase
); );
} }
public function testBetriebsartBleibtZwischenDenSchwellenErhalten(): void public function testBetriebsartBleibtZwischenDenZielwertenErhalten(): void
{ {
self::assertSame( self::assertSame(
ManagerRegler::BETRIEBSART_PEAK, ManagerRegler::BETRIEBSART_PEAK,
@@ -41,8 +39,6 @@ final class ManagerReglerTest extends TestCase
ManagerRegler::BETRIEBSART_PEAK, ManagerRegler::BETRIEBSART_PEAK,
true, true,
0.0, 0.0,
10000.0,
0.0,
12000.0, 12000.0,
0.0 0.0
) )
@@ -59,8 +55,6 @@ final class ManagerReglerTest extends TestCase
11000.0, 11000.0,
ManagerRegler::BETRIEBSART_PV, ManagerRegler::BETRIEBSART_PV,
true, true,
0.0,
10000.0,
10000.0, 10000.0,
10500.0, 10500.0,
60.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 public function testVerteilungBeruecksichtigtPrioritaetEinzelwerteUndBereiche(): void
{ {
$ergebnis = ManagerRegler::verteile( $ergebnis = ManagerRegler::verteile(