Enelix-2-Manager implementieren
Tests / test (push) Canceled after 0s

Installierbares IP-Symcon-Manager-Modul mit testbarer Verteilungslogik, Verbraucher-Timeouts, Sollwertversand und aktualisierter Dokumentation.
This commit is contained in:
dh
2026-09-15 15:59:25 +00:00
parent 21a6466ca5
commit 5f1bdbc0fa
10 changed files with 1175 additions and 11 deletions
+171
View File
@@ -0,0 +1,171 @@
{
"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": [
{
"caption": "Instanz",
"name": "InstanzID",
"width": "auto",
"add": 0,
"edit": {"type": "SelectInstance"}
},
{
"caption": "Aktiv",
"name": "Aktiv",
"width": "100px",
"add": false,
"edit": {"type": "CheckBox"}
}
]
},
{
"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": "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",
"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": "CheckBox",
"name": "LoggingEin",
"caption": "Logging ein"
}
],
"actions": [
{
"type": "Button",
"caption": "Regelung jetzt ausfuehren",
"onClick": "IPS_RequestAction($id, 'Regeln', true);"
}
],
"status": [
{"code": 102, "icon": "active", "caption": "Aktiv"},
{"code": 201, "icon": "inactive", "caption": "Netzleistungsmessung fehlt oder ist veraltet"},
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"id": "{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}",
"name": "Manager",
"type": 3,
"vendor": "Belevo AG",
"aliases": [
"Enelix 2 Manager"
],
"parentRequirements": [],
"childRequirements": [],
"implemented": [],
"prefix": "ENELIX",
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Manager"
}
+468
View File
@@ -0,0 +1,468 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../libs/ManagerSchnittstelle.php';
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
require_once __DIR__ . '/../libs/ManagerRegler.php';
use Belevo\EnelixEMS\ManagerRegler;
use Belevo\EnelixEMS\ManagerSchnittstelle;
use Belevo\EnelixEMS\Nachrichtenvertrag;
class Manager extends IPSModule implements ManagerSchnittstelle
{
private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201;
private const STATUS_KONFIGURATION_UNGUELTIG = 202;
private const VM_UPDATE = 10603;
public function Create(): void
{
parent::Create();
$this->RegisterPropertyInteger('Rolle', 0);
$this->RegisterPropertyInteger('NetzleistungVariableID', 0);
$this->RegisterPropertyFloat('Netzleistungsfaktor', 1.0);
$this->RegisterPropertyInteger('MesswertMaxAlter', 30);
$this->RegisterPropertyString('VerbraucherZuordnung', '[]');
$this->RegisterPropertyBoolean('AutomatischeSuche', true);
$this->RegisterPropertyInteger('SuchbereichID', 0);
$this->RegisterPropertyInteger('Regelintervall', 2);
$this->RegisterPropertyInteger('Vorgabeintervall', 10);
$this->RegisterPropertyInteger('VerbraucherTimeout', 30);
$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->RegisterPropertyString('PrognoseAnschluss', '{}');
$this->RegisterPropertyString('SDLAnschluss', '{}');
$this->RegisterPropertyString('LizenzAnschluss', '{}');
$this->RegisterPropertyString('StoermeldeAnschluss', '{}');
$this->RegisterPropertyBoolean('LoggingEin', false);
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
$this->EnableAction('Aktiv');
$this->RegisterVariableString('Betriebsart', 'Betriebsart', '', 20);
$this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30);
$this->RegisterVariableBoolean('NetzleistungGueltig', 'Netzleistung gueltig', '~Switch', 40);
$this->RegisterVariableFloat('WirksameLastspitzengrenze', 'Wirksame Lastspitzengrenze', '', 50);
$this->RegisterVariableFloat('Verteilbudget', 'Verteilbudget', '', 60);
$this->RegisterVariableInteger('VerbraucherAnzahl', 'Verbraucher Anzahl', '', 70);
$this->RegisterVariableInteger('VerbraucherVerfuegbar', 'Verbraucher verfuegbar', '', 80);
$this->RegisterVariableString('Verbraucherstatus', 'Verbraucherstatus', '', 90);
$this->RegisterVariableString('Monatsgrenzen', 'Monatsgrenzen', '', 100);
$this->RegisterVariableString('Prognosestatus', 'Prognosestatus', '', 110);
$this->RegisterVariableString('SDLStatus', 'SDL-Status', '', 120);
$this->RegisterVariableString('Lizenzstatus', 'Lizenzstatus', '', 130);
$this->RegisterVariableString('Stoerueberwachungsstatus', 'Stoerueberwachungsstatus', '', 140);
$this->RegisterVariableBoolean('Sammelstoerung', 'Sammelstoerung', '~Alert', 150);
$this->RegisterVariableString('Stoertext', 'Stoertext', '', 160);
$this->RegisterAttributeString('VerbraucherCache', '{}');
$this->RegisterAttributeString('LetzteSollwerte', '{}');
$this->RegisterAttributeInteger('RegistrierteNetzleistungVariableID', 0);
$this->RegisterTimer(
'Regelzyklus',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', false);"
);
$this->RegisterTimer(
'VorgabenErneuern',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
);
}
public function ApplyChanges(): void
{
parent::ApplyChanges();
try {
$this->pruefeKonfiguration();
} catch (Throwable $fehler) {
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0);
$this->protokolliere('Konfiguration', $fehler->getMessage());
return;
}
$this->registriereNetzleistungsmeldung();
$this->SetTimerInterval('Regelzyklus', $this->ReadPropertyInteger('Regelintervall') * 1000);
$this->SetTimerInterval('VorgabenErneuern', $this->ReadPropertyInteger('Vorgabeintervall') * 1000);
$this->SetValue('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
$this->aktualisiereSichtbarkeit();
$this->aktualisiereAnschlussstatus();
$this->SetStatus(self::STATUS_AKTIV);
}
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
{
if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID')
&& (int) $nachricht === self::VM_UPDATE
) {
$this->regeln(false);
}
}
public function RequestAction($ident, $wert): void
{
switch ($ident) {
case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert);
$this->regeln(false);
return;
case 'Regeln':
$this->regeln((bool) $wert);
return;
case 'VerbraucherdatenEmpfangen':
if (!is_string($wert)) {
throw new InvalidArgumentException('Verbraucherdaten muessen als JSON uebergeben werden.');
}
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($daten)) {
throw new InvalidArgumentException('Verbraucherdaten muessen ein JSON-Objekt sein.');
}
$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);
}
/**
* @param array<string, mixed> $daten
*/
public function VerbraucherdatenEmpfangen(array $daten): void
{
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) {
throw new InvalidArgumentException('Verbraucherdaten sind an eine andere Instanz adressiert.');
}
$absenderID = $daten['Kopf']['AbsenderID'];
if (!in_array($absenderID, $this->aktiveVerbraucherIDs(), true)) {
throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.');
}
$cache = $this->leseJsonAttribut('VerbraucherCache');
$cache[(string) $absenderID] = [
'EmpfangenAm' => time(),
'Daten' => $daten,
];
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
$this->regeln(false);
}
private function regeln(bool $erneuern): void
{
[$netzleistung, $netzleistungGueltig] = $this->leseNetzleistung();
$this->SetValue('Netzleistung', $netzleistung);
$this->SetValue('NetzleistungGueltig', $netzleistungGueltig);
$aktiv = (bool) $this->GetValue('Aktiv');
if (!$aktiv || !$netzleistungGueltig) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
$this->SetValue('Verteilbudget', 0.0);
$this->SetStatus($aktiv ? self::STATUS_MESSWERT_UNGUELTIG : self::STATUS_AKTIV);
return;
}
$stoerungen = [];
$verbraucher = $this->aktuelleVerbraucher($stoerungen);
$lastspitzengrenze = ManagerRegler::wirksameLastspitzengrenze(
$this->ReadPropertyInteger('Lastspitzenmodus'),
$this->ReadPropertyFloat('Lastspitzengrenze'),
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')),
(int) date('n')
);
$betriebsart = ManagerRegler::bestimmeBetriebsart(
true,
true,
$netzleistung,
(string) $this->GetValue('Betriebsart'),
$lastspitzengrenze !== null,
$this->ReadPropertyFloat('UmschaltschwelleUeberschuss'),
$this->ReadPropertyFloat('UmschaltschwelleLastspitze'),
$this->ReadPropertyFloat('SollwertSolarladen'),
$lastspitzengrenze ?? 0.0,
$this->ReadPropertyFloat('Umschaltdifferenz')
);
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
? (float) $lastspitzengrenze
: $this->ReadPropertyFloat('SollwertSolarladen');
$ergebnis = ManagerRegler::verteile(
$netzleistung,
$zielnetzleistung,
$verbraucher,
$betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV'
);
$this->SetValue('Betriebsart', $betriebsart);
$this->SetValue('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
$this->SetValue('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
$this->SetValue('Verbraucherstatus', json_encode([
'Betriebsart' => $betriebsart,
'Restabweichung_W' => $ergebnis['Restabweichung_W'],
'Verbraucher' => $verbraucher,
], JSON_THROW_ON_ERROR));
$letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte');
foreach ($ergebnis['Sollwerte'] as $instanzID => $sollleistung) {
if (!$erneuern && ($letzteSollwerte[(string) $instanzID] ?? null) === $sollleistung) {
continue;
}
try {
$this->sendeSollwert((int) $instanzID, $sollleistung);
$letzteSollwerte[(string) $instanzID] = $sollleistung;
} catch (Throwable $fehler) {
$stoerungen[] = 'Sollwert an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
}
}
$this->WriteAttributeString('LetzteSollwerte', json_encode($letzteSollwerte, JSON_THROW_ON_ERROR));
$this->aktualisiereStoerungen($verbraucher, $stoerungen);
$this->SetStatus(self::STATUS_AKTIV);
}
/** @return array{0: float, 1: bool} */
private function leseNetzleistung(): array
{
$variablenID = $this->ReadPropertyInteger('NetzleistungVariableID');
if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) {
return [0.0, false];
}
$variable = IPS_GetVariable($variablenID);
if (time() - (int) $variable['VariableUpdated'] > $this->ReadPropertyInteger('MesswertMaxAlter')) {
return [0.0, false];
}
$wert = GetValue($variablenID);
if (!is_int($wert) && !is_float($wert)) {
return [0.0, false];
}
return [(float) $wert * $this->ReadPropertyFloat('Netzleistungsfaktor'), true];
}
/**
* @param list<string> $stoerungen
* @return list<array{InstanzID: int, Daten: array<string, mixed>}>
*/
private function aktuelleVerbraucher(array &$stoerungen): array
{
$ids = $this->aktiveVerbraucherIDs();
$this->SetValue('VerbraucherAnzahl', count($ids));
$cache = $this->leseJsonAttribut('VerbraucherCache');
$timeout = $this->ReadPropertyInteger('VerbraucherTimeout');
$aktuell = [];
foreach ($ids as $instanzID) {
$eintrag = $cache[(string) $instanzID] ?? null;
if (!is_array($eintrag) || !isset($eintrag['EmpfangenAm'], $eintrag['Daten'])) {
$stoerungen[] = 'Noch keine Meldung von Instanz ' . $instanzID . '.';
continue;
}
if (time() - (int) $eintrag['EmpfangenAm'] > $timeout) {
$stoerungen[] = 'Meldung von Instanz ' . $instanzID . ' ist veraltet.';
continue;
}
$aktuell[] = ['InstanzID' => $instanzID, 'Daten' => $eintrag['Daten']];
}
$verfuegbar = array_filter($aktuell, static fn (array $eintrag): bool => $eintrag['Daten']['Verfuegbar']);
$this->SetValue('VerbraucherVerfuegbar', count($verfuegbar));
return $aktuell;
}
/** @return list<int> */
private function aktiveVerbraucherIDs(): array
{
$zuordnung = json_decode($this->ReadPropertyString('VerbraucherZuordnung'), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($zuordnung)) {
throw new InvalidArgumentException('VerbraucherZuordnung muss eine JSON-Liste sein.');
}
$ids = [];
foreach ($zuordnung as $eintrag) {
if (!is_array($eintrag)) {
throw new InvalidArgumentException('Jede Verbraucherzuordnung muss ein Objekt sein.');
}
$instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null;
if (!is_int($instanzID) || $instanzID <= 0 || !is_bool($eintrag['Aktiv'] ?? null)) {
throw new InvalidArgumentException('Verbraucherzuordnung benoetigt InstanzID und Aktiv.');
}
if ($eintrag['Aktiv']) {
$ids[] = $instanzID;
}
}
return array_values(array_unique($ids));
}
private function sendeSollwert(int $instanzID, int $sollleistung): void
{
if (!IPS_InstanceExists($instanzID)) {
throw new RuntimeException('Instanz existiert nicht.');
}
$paket = [
'Kopf' => [
'Version' => Nachrichtenvertrag::VERSION,
'AbsenderID' => $this->InstanceID,
'EmpfaengerID' => $instanzID,
'Zeitpunkt' => time(),
],
'Sollleistung_W' => $sollleistung,
];
Nachrichtenvertrag::pruefeManagerdaten($paket);
IPS_RequestAction($instanzID, 'ManagerdatenEmpfangen', json_encode($paket, JSON_THROW_ON_ERROR));
$this->protokolliere('Sollwert', ['InstanzID' => $instanzID, 'Sollleistung_W' => $sollleistung]);
}
/**
* @param list<array{InstanzID: int, Daten: array<string, mixed>}> $verbraucher
* @param list<string> $stoerungen
*/
private function aktualisiereStoerungen(array $verbraucher, array $stoerungen): void
{
foreach ($verbraucher as $eintrag) {
foreach ($eintrag['Daten']['Zustand'] as $zustand) {
if ($zustand['Art'] === 'Stoerung' && $zustand['Wert'] === true) {
$stoerungen[] = 'Instanz ' . $eintrag['InstanzID'] . ': '
. ($zustand['Text'] ?? $zustand['Kennung']);
}
}
}
$stoerungen = array_values(array_unique($stoerungen));
$this->SetValue('Sammelstoerung', $stoerungen !== []);
$this->SetValue('Stoertext', implode("\n", $stoerungen));
}
private function registriereNetzleistungsmeldung(): void
{
$alt = $this->ReadAttributeInteger('RegistrierteNetzleistungVariableID');
$neu = $this->ReadPropertyInteger('NetzleistungVariableID');
if ($alt > 0 && $alt !== $neu) {
$this->UnregisterMessage($alt, self::VM_UPDATE);
}
if ($neu > 0 && $neu !== $alt) {
$this->RegisterMessage($neu, self::VM_UPDATE);
}
$this->WriteAttributeInteger('RegistrierteNetzleistungVariableID', $neu);
}
private function aktualisiereSichtbarkeit(): void
{
$logging = $this->ReadPropertyBoolean('LoggingEin');
foreach ([
'NetzleistungGueltig', 'WirksameLastspitzengrenze', 'Verteilbudget',
'VerbraucherAnzahl', 'VerbraucherVerfuegbar', 'Verbraucherstatus',
'Prognosestatus', 'SDLStatus', 'Lizenzstatus',
'Stoerueberwachungsstatus', 'Sammelstoerung', 'Stoertext',
] 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');
}
}
private function aktualisiereAnschlussstatus(): void
{
foreach ([
'PrognoseAnschluss' => 'Prognosestatus',
'SDLAnschluss' => 'SDLStatus',
'LizenzAnschluss' => 'Lizenzstatus',
'StoermeldeAnschluss' => 'Stoerueberwachungsstatus',
] as $property => $variable) {
$konfiguration = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
$aktiv = is_array($konfiguration) && ($konfiguration['Aktiv'] ?? false) === true;
$this->SetValue($variable, $aktiv ? 'Fehler' : 'NichtVerwendet');
}
}
private function pruefeKonfiguration(): void
{
foreach (['MesswertMaxAlter', 'Regelintervall', 'Vorgabeintervall', 'VerbraucherTimeout'] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
}
}
if ($this->ReadPropertyFloat('Umschaltdifferenz') < 0
|| $this->ReadPropertyFloat('Umschaltdifferenz') > 100
) {
throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 liegen.');
}
$this->aktiveVerbraucherIDs();
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
foreach (['PrognoseAnschluss', 'SDLAnschluss', 'LizenzAnschluss', 'StoermeldeAnschluss'] as $property) {
$wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($wert)) {
throw new InvalidArgumentException($property . ' muss ein JSON-Objekt sein.');
}
}
}
/** @return array<int, float> */
private function dekodiereMonatsgrenzen(string $json): array
{
$werte = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($werte)) {
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.');
}
$ergebnis[$monat] = (float) $wert;
}
return $ergebnis;
}
/** @return array<string, mixed> */
private function leseJsonAttribut(string $name): array
{
$wert = json_decode($this->ReadAttributeString($name), true, 512, JSON_THROW_ON_ERROR);
return is_array($wert) ? $wert : [];
}
private function standardMonatsgrenzen(): string
{
return json_encode(array_fill_keys(range(1, 12), 0), JSON_THROW_ON_ERROR);
}
/** @param mixed $daten */
private function protokolliere(string $bezeichnung, $daten): void
{
if ($this->ReadPropertyBoolean('LoggingEin')) {
$this->SendDebug($bezeichnung, is_string($daten) ? $daten : json_encode($daten), 0);
}
}
}