Compare commits
30
Commits
main
...
6c2d6ddcde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c2d6ddcde | ||
|
|
d4828f1ee5 | ||
|
|
29cfd8ceaa | ||
|
|
92e8ec59df | ||
|
|
355e7163fb | ||
|
|
1b4a788f22 | ||
|
|
9e6a675b25 | ||
|
|
4514ccb557 | ||
|
|
9869d99641 | ||
|
|
98fb926fbd | ||
|
|
7a3c436039 | ||
|
|
0a3ece3a81 | ||
|
|
21a6466ca5 | ||
|
|
37304691eb | ||
|
|
8af7776f33 | ||
|
|
0c9d4d920a | ||
|
|
dcd310498d | ||
|
|
c1fdd37b2b | ||
|
|
8439213234 | ||
|
|
efda77ccad | ||
|
|
69da93270e | ||
|
|
b2779148ee | ||
|
|
f10d454eb7 | ||
|
|
9426b0f041 | ||
|
|
1541482bf6 | ||
|
|
9acbff610d | ||
|
|
9aab4abdb3 | ||
|
|
0f9860356d | ||
|
|
bd47faa593 | ||
|
|
578285b0d6 |
@@ -0,0 +1,282 @@
|
|||||||
|
{
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Lizenzierung",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "LizenzAnschluss",
|
||||||
|
"caption": "Konfiguration als JSON"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Grundeinstellungen",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"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": "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,
|
||||||
|
"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": "Button",
|
||||||
|
"name": "VerbraucherAktualisieren",
|
||||||
|
"caption": "Verbraucher aktualisieren",
|
||||||
|
"visible": true,
|
||||||
|
"onClick": "IPS_RequestAction($id, 'FormVerbraucherAktualisieren', json_encode(['SuchbereichID' => $SuchbereichID, 'Zuordnung' => $AutomatischeVerbraucherZuordnung]));"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "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": "DiagnosevariablenAnzeigen",
|
||||||
|
"caption": "Diagnosevariablen anlegen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "LoggingEin",
|
||||||
|
"caption": "Laufendes Debug-Logging aktivieren"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "Stoerueberwachung",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "StoermeldeAnschluss",
|
||||||
|
"caption": "Konfiguration als JSON"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}",
|
||||||
|
"name": "Manager",
|
||||||
|
"type": 3,
|
||||||
|
"vendor": "Enelix",
|
||||||
|
"aliases": [
|
||||||
|
"Manager"
|
||||||
|
],
|
||||||
|
"parentRequirements": [],
|
||||||
|
"childRequirements": [],
|
||||||
|
"implemented": [],
|
||||||
|
"prefix": "ENELIX",
|
||||||
|
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Manager"
|
||||||
|
}
|
||||||
@@ -0,0 +1,796 @@
|
|||||||
|
<?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;
|
||||||
|
private const VERBRAUCHER_MODUL_IDS = [
|
||||||
|
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
||||||
|
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||||
|
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||||
|
];
|
||||||
|
private const MONATSNAMEN = [
|
||||||
|
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
||||||
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||||
|
];
|
||||||
|
private const DIAGNOSE_VARIABLEN = [
|
||||||
|
'NetzleistungGueltig',
|
||||||
|
'WirksameLastspitzengrenze',
|
||||||
|
'Verteilbudget',
|
||||||
|
'VerbraucherAnzahl',
|
||||||
|
'VerbraucherVerfuegbar',
|
||||||
|
'Verbraucherstatus',
|
||||||
|
'Monatsgrenzen',
|
||||||
|
'Prognosestatus',
|
||||||
|
'SDLStatus',
|
||||||
|
'Lizenzstatus',
|
||||||
|
'Stoerueberwachungsstatus',
|
||||||
|
'Sammelstoerung',
|
||||||
|
'Stoertext',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function Create(): void
|
||||||
|
{
|
||||||
|
parent::Create();
|
||||||
|
|
||||||
|
$this->RegisterPropertyInteger('Rolle', 0);
|
||||||
|
$this->RegisterPropertyInteger('NetzleistungVariableID', 0);
|
||||||
|
$this->RegisterPropertyFloat('Netzleistungsfaktor', 1.0);
|
||||||
|
$this->RegisterPropertyInteger('MesswertMaxAlter', 60);
|
||||||
|
$this->RegisterPropertyString('VerbraucherZuordnung', '[]');
|
||||||
|
$this->RegisterPropertyString('AutomatischeVerbraucherZuordnung', '[]');
|
||||||
|
$this->RegisterPropertyBoolean('AutomatischeSuche', true);
|
||||||
|
$this->RegisterPropertyInteger('SuchbereichID', 0);
|
||||||
|
$this->RegisterPropertyInteger('KeepAlive', 60);
|
||||||
|
$this->RegisterPropertyInteger('VerbraucherTimeout', 60);
|
||||||
|
$this->RegisterPropertyInteger('Lastspitzenmodus', 0);
|
||||||
|
$this->RegisterPropertyFloat('Lastspitzengrenze', 0.0);
|
||||||
|
$this->RegisterPropertyString('Monatsgrenzen', $this->standardMonatsgrenzen());
|
||||||
|
$this->RegisterPropertyFloat('SollwertSolarladen', 0.0);
|
||||||
|
$this->RegisterPropertyFloat('Umschaltdifferenz', 5.0);
|
||||||
|
$this->RegisterPropertyString('PrognoseAnschluss', '{}');
|
||||||
|
$this->RegisterPropertyString('SDLAnschluss', '{}');
|
||||||
|
$this->RegisterPropertyString('LizenzAnschluss', '{}');
|
||||||
|
$this->RegisterPropertyString('StoermeldeAnschluss', '{}');
|
||||||
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||||
|
$this->RegisterPropertyBoolean('LoggingEin', false);
|
||||||
|
|
||||||
|
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
|
||||||
|
$this->EnableAction('Aktiv');
|
||||||
|
$this->RegisterVariableString('Betriebsart', 'Betriebsart', '', 20);
|
||||||
|
$this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30);
|
||||||
|
|
||||||
|
$this->RegisterAttributeString('VerbraucherCache', '{}');
|
||||||
|
$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(
|
||||||
|
'KeepAlive',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ApplyChanges(): void
|
||||||
|
{
|
||||||
|
parent::ApplyChanges();
|
||||||
|
|
||||||
|
$this->aktualisiereVariablen();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pruefeKonfiguration();
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$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', 0);
|
||||||
|
$this->SetTimerInterval('VorgabenErneuern', 0);
|
||||||
|
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
|
||||||
|
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
|
||||||
|
$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'],
|
||||||
|
'VerbraucherAktualisieren',
|
||||||
|
'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')
|
||||||
|
&& (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 'FormAutomatischeSuche':
|
||||||
|
$automatisch = (bool) $wert;
|
||||||
|
$this->UpdateFormField('VerbraucherZuordnung', 'visible', !$automatisch);
|
||||||
|
$this->UpdateFormField('AutomatischeVerbraucherZuordnung', 'visible', $automatisch);
|
||||||
|
$this->UpdateFormField('SuchbereichID', 'visible', $automatisch);
|
||||||
|
$this->UpdateFormField('VerbraucherAktualisieren', 'visible', $automatisch);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'FormVerbraucherAktualisieren':
|
||||||
|
if (!is_string($wert)) {
|
||||||
|
throw new InvalidArgumentException('Suchparameter muessen als JSON uebergeben werden.');
|
||||||
|
}
|
||||||
|
$suchparameter = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($suchparameter) || !is_array($suchparameter['Zuordnung'] ?? null)) {
|
||||||
|
throw new InvalidArgumentException('Suchparameter sind ungueltig.');
|
||||||
|
}
|
||||||
|
$suchbereichID = $suchparameter['SuchbereichID'] ?? 0;
|
||||||
|
if (!is_int($suchbereichID)) {
|
||||||
|
throw new InvalidArgumentException('SuchbereichID muss eine Ganzzahl sein.');
|
||||||
|
}
|
||||||
|
$werte = $this->automatischeVerbraucherFuerFormular(
|
||||||
|
$suchparameter['Zuordnung'],
|
||||||
|
$suchbereichID
|
||||||
|
);
|
||||||
|
$this->UpdateFormField(
|
||||||
|
'AutomatischeVerbraucherZuordnung',
|
||||||
|
'values',
|
||||||
|
json_encode($werte, JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
$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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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->setzeDiagnosewert('NetzleistungGueltig', $netzleistungGueltig);
|
||||||
|
|
||||||
|
$aktiv = (bool) $this->GetValue('Aktiv');
|
||||||
|
if (!$aktiv || !$netzleistungGueltig) {
|
||||||
|
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
|
||||||
|
$this->setzeDiagnosewert('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('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->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
|
||||||
|
$this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
|
||||||
|
$this->setzeDiagnosewert('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->setzeDiagnosewert('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->setzeDiagnosewert('VerbraucherVerfuegbar', count($verfuegbar));
|
||||||
|
|
||||||
|
return $aktuell;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
private function aktiveVerbraucherIDs(): array
|
||||||
|
{
|
||||||
|
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)) {
|
||||||
|
throw new InvalidArgumentException($property . ' muss eine JSON-Liste sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->aktiveVerbraucherIDsAusZuordnung($zuordnung);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $zuordnung
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
private function aktiveVerbraucherIDsAusZuordnung(array $zuordnung): array
|
||||||
|
{
|
||||||
|
$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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>>|null $aktuelleZuordnung
|
||||||
|
* @return list<array{Name: string, InstanzID: int, Aktiv: bool}>
|
||||||
|
*/
|
||||||
|
private function automatischeVerbraucherFuerFormular(
|
||||||
|
?array $aktuelleZuordnung = null,
|
||||||
|
?int $suchbereichID = null
|
||||||
|
): array {
|
||||||
|
if ($aktuelleZuordnung === null) {
|
||||||
|
$gespeichert = json_decode(
|
||||||
|
$this->ReadPropertyString('AutomatischeVerbraucherZuordnung'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$ausgewaehlt = $gespeichert === []
|
||||||
|
? $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung')
|
||||||
|
: $this->aktiveVerbraucherIDsAusZuordnung($gespeichert);
|
||||||
|
} else {
|
||||||
|
$ausgewaehlt = $this->aktiveVerbraucherIDsAusZuordnung($aktuelleZuordnung);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
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->setzeDiagnosewert('Sammelstoerung', $stoerungen !== []);
|
||||||
|
$this->setzeDiagnosewert('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 aktualisiereVariablen(): void
|
||||||
|
{
|
||||||
|
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||||
|
$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);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||||
|
$this->UnregisterVariable($ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param bool|float|int|string $wert */
|
||||||
|
private function setzeDiagnosewert(string $ident, $wert): void
|
||||||
|
{
|
||||||
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||||
|
$this->SetValue($ident, $wert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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->setzeDiagnosewert($variable, $aktiv ? 'Fehler' : 'NichtVerwendet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pruefeKonfiguration(): void
|
||||||
|
{
|
||||||
|
foreach (['MesswertMaxAlter', 'KeepAlive', '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->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);
|
||||||
|
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 = [];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count($ergebnis) !== 12) {
|
||||||
|
throw new InvalidArgumentException('Monatsgrenzen muss genau zwoelf Monatswerte enthalten.');
|
||||||
|
}
|
||||||
|
ksort($ergebnis);
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$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 */
|
||||||
|
private function protokolliere(string $bezeichnung, $daten): void
|
||||||
|
{
|
||||||
|
if ($this->ReadPropertyBoolean('LoggingEin')) {
|
||||||
|
$this->SendDebug($bezeichnung, is_string($daten) ? $daten : json_encode($daten), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,20 +4,27 @@ Energiemanagement, Manager und steuerbare Verbraucher fuer IP-Symcon.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Das Repository befindet sich im Aufbau. Aktuell ist ausschliesslich der gemeinsame Nachrichtenvertrag implementiert. Noch nicht implementierte Module werden nicht als leere Platzhalter angelegt.
|
Das Repository befindet sich im Aufbau. Der gemeinsame Nachrichtenvertrag, die
|
||||||
|
Verbraucherbasis, der Manager, der mehrstufige Warmwassererwaermer und der
|
||||||
|
1-stufige Verbraucher sind implementiert. Die weiteren Module sind als
|
||||||
|
Diskussionsentwürfe dokumentiert.
|
||||||
|
|
||||||
## Geplante Module
|
## Geplante Module
|
||||||
|
|
||||||
- Manager
|
- Manager (implementiert)
|
||||||
- Batterie
|
- Batterie
|
||||||
- Wassererwaermer
|
- Wassererwaermer (implementiert)
|
||||||
- Pufferspeicher
|
- Pufferspeicher
|
||||||
- Verbraucher 1-Stufig
|
- Verbraucher 1-Stufig (implementiert)
|
||||||
- Waermepumpe
|
- Waermepumpe
|
||||||
- Ladestation Stand-Alone
|
- Ladestation Stand-Alone
|
||||||
- Ladestation Gateway
|
- Ladestation Gateway
|
||||||
- Easee Gateway
|
- Easee Gateway
|
||||||
|
|
||||||
|
Die Properties, Variablen, Zustandsdaten und offenen Punkte stehen in der
|
||||||
|
[Modulübersicht](docs/module/README.md). Gemeinsame Felder werden dort nicht
|
||||||
|
abweichend neu definiert, sondern verweisen auf den zentralen Vertrag.
|
||||||
|
|
||||||
Die Verbraucher kommunizieren mit dem Manager ueber den gemeinsamen Vertrag in `libs/`. Der Manager waehlt die Verbraucher aus; in einem Verbraucher gibt es keine Manager-ID-Property.
|
Die Verbraucher kommunizieren mit dem Manager ueber den gemeinsamen Vertrag in `libs/`. Der Manager waehlt die Verbraucher aus; in einem Verbraucher gibt es keine Manager-ID-Property.
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
@@ -40,5 +47,13 @@ Neue oder geaenderte Funktionen werden zusammen mit ihren PHPUnit-Tests eingeche
|
|||||||
| `beta` | Beta |
|
| `beta` | Beta |
|
||||||
| `develop` | Testing |
|
| `develop` | Testing |
|
||||||
|
|
||||||
Die ausfuehrliche Schnittstellenbeschreibung steht unter [docs/Schnittstelle.md](docs/Schnittstelle.md).
|
## Dokumentation
|
||||||
|
|
||||||
|
- [Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md)
|
||||||
|
- [Manager-Modul](docs/module/Manager/README.md)
|
||||||
|
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
||||||
|
- [Verbraucher-1-Stufig-Modul](docs/module/Verbraucher-1-Stufig/README.md)
|
||||||
|
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
||||||
|
- [Migration Verbraucher 1-Stufig](docs/migration/Verbraucher-1-Stufig.md)
|
||||||
|
- [Obere Anschlüsse des Managers](docs/Obere-Anschluesse.md)
|
||||||
|
- [Modulübersicht](docs/module/README.md)
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Manager und Zeitverhalten",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "PrioritaetPV",
|
||||||
|
"caption": "Prioritaet PV",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "PrioritaetPeak",
|
||||||
|
"caption": "Prioritaet Peak",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Meldeintervall",
|
||||||
|
"caption": "Meldeintervall",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "VorgabeTimeout",
|
||||||
|
"caption": "Vorgabe-Timeout",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Umschaltabstand",
|
||||||
|
"caption": "Zeit bis zum naechsten Lastwechsel",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Verbraucher",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Nennleistung",
|
||||||
|
"caption": "Nennleistung",
|
||||||
|
"suffix": " W",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "SchaltkontaktVariableID",
|
||||||
|
"caption": "Schaltkontakt",
|
||||||
|
"validVariableTypes": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "SchaltkontaktInvertiert",
|
||||||
|
"caption": "Schaltkontakt invertieren"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "RueckmeldungVariableID",
|
||||||
|
"caption": "Optionale Schaltzustands-Rueckmeldung",
|
||||||
|
"validVariableTypes": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Mindestlaufzeit",
|
||||||
|
"caption": "Taegliche Mindestlaufzeit",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 86400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Darstellung und Diagnose",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "EinstellungenInVisu",
|
||||||
|
"caption": "Lokale Einstellungen in der Visualisierung anzeigen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "DiagnosevariablenAnzeigen",
|
||||||
|
"caption": "Diagnosevariablen anzeigen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "LoggingEin",
|
||||||
|
"caption": "Diagnoseprotokoll aktivieren"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "{15879A4E-D0C2-4495-83DE-46E1E462591E}",
|
||||||
|
"name": "VerbraucherEinStufig",
|
||||||
|
"type": 3,
|
||||||
|
"vendor": "Enelix",
|
||||||
|
"aliases": [
|
||||||
|
"Verbraucher 1-Stufig"
|
||||||
|
],
|
||||||
|
"parentRequirements": [],
|
||||||
|
"childRequirements": [],
|
||||||
|
"implemented": [],
|
||||||
|
"prefix": "ENELIX",
|
||||||
|
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/VerbraucherEinStufig"
|
||||||
|
}
|
||||||
@@ -0,0 +1,861 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../libs/VerbraucherSchnittstelle.php';
|
||||||
|
require_once __DIR__ . '/../libs/VerbraucherBasisTrait.php';
|
||||||
|
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
|
||||||
|
require_once __DIR__ . '/../libs/EinStufigRegler.php';
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\EinStufigRegler;
|
||||||
|
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||||
|
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
||||||
|
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
|
||||||
|
|
||||||
|
class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
||||||
|
{
|
||||||
|
use VerbraucherBasisTrait;
|
||||||
|
|
||||||
|
private const MANAGER_MODULE_ID = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
||||||
|
private const STATUS_AKTIV = 102;
|
||||||
|
private const STATUS_KONFIGURATION_UNGUELTIG = 201;
|
||||||
|
private const STATUS_SCHALTFEHLER = 202;
|
||||||
|
private const VM_UPDATE = 10603;
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const DIAGNOSE_VARIABLEN = [
|
||||||
|
'Istleistung',
|
||||||
|
'Leistungsquelle',
|
||||||
|
'Sollleistung',
|
||||||
|
'SollwertGueltig',
|
||||||
|
'Verfuegbar',
|
||||||
|
'AenderungMoeglich',
|
||||||
|
'Stoerung',
|
||||||
|
'Stoertext',
|
||||||
|
'Rueckmeldefehler',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function Create(): void
|
||||||
|
{
|
||||||
|
parent::Create();
|
||||||
|
|
||||||
|
$this->registriereVerbraucherBasis();
|
||||||
|
$this->RegisterPropertyInteger('Nennleistung', 0);
|
||||||
|
$this->RegisterPropertyInteger('SchaltkontaktVariableID', 0);
|
||||||
|
$this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false);
|
||||||
|
$this->RegisterPropertyInteger('RueckmeldungVariableID', 0);
|
||||||
|
$this->RegisterPropertyInteger('Mindestlaufzeit', 0);
|
||||||
|
$this->RegisterPropertyInteger('Umschaltabstand', 5);
|
||||||
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||||
|
|
||||||
|
$this->RegisterVariableBoolean('Schaltzustand', 'Schaltzustand', '~Switch', 100);
|
||||||
|
$this->RegisterVariableInteger('Tageslaufzeit', 'Tageslaufzeit', '', 110);
|
||||||
|
|
||||||
|
$this->RegisterAttributeInteger('RegistrierterSchaltkontakt', 0);
|
||||||
|
$this->RegisterAttributeInteger('RegistrierteRueckmeldung', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
||||||
|
$this->RegisterAttributeInteger('GesperrtBis', 0);
|
||||||
|
$this->RegisterAttributeInteger('LaufzeitStandZeit', 0);
|
||||||
|
$this->RegisterAttributeString('LaufzeitTag', '');
|
||||||
|
$this->RegisterAttributeString('Leistungsangebot', '[0]');
|
||||||
|
$this->RegisterAttributeBoolean('Initialisiert', false);
|
||||||
|
$this->RegisterAttributeBoolean('SchaltvorgangLaeuft', false);
|
||||||
|
$this->RegisterAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->RegisterAttributeBoolean('Schaltziel', false);
|
||||||
|
$this->RegisterAttributeString('Schaltfehler', '');
|
||||||
|
|
||||||
|
$this->RegisterAttributeFloat('ZustandIstleistung', 0.0);
|
||||||
|
$this->RegisterAttributeInteger('ZustandSollleistung', 0);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandSollwertGueltig', false);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandVerfuegbar', false);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandStoerung', false);
|
||||||
|
$this->RegisterAttributeString('ZustandStoertext', '');
|
||||||
|
$this->RegisterAttributeBoolean('ZustandSchaltzustand', false);
|
||||||
|
$this->RegisterAttributeInteger('ZustandTageslaufzeit', 0);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandRueckmeldefehler', false);
|
||||||
|
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'Meldezyklus',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'RueckmeldungVerzoegert',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'Umschaltfreigabe',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Umschaltfreigabe', 0);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'VorgabeTimeout',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', 0);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'Tagesplanung',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Tagesplanung', 0);"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ApplyChanges(): void
|
||||||
|
{
|
||||||
|
parent::ApplyChanges();
|
||||||
|
|
||||||
|
$this->aktualisiereVariablen();
|
||||||
|
$this->registriereMeldungen();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pruefeKonfiguration();
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->deaktiviereTimer();
|
||||||
|
$this->setzeZustand('Verfuegbar', false);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', false);
|
||||||
|
$this->setzeStoerung($fehler->getMessage(), false);
|
||||||
|
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||||
|
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000);
|
||||||
|
$this->initialisiereZustand();
|
||||||
|
$this->aktualisiere(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
|
||||||
|
{
|
||||||
|
if ((int) $nachricht !== self::VM_UPDATE || $this->ReadAttributeBoolean('SchaltvorgangLaeuft')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relevanteIDs = array_filter([
|
||||||
|
$this->ReadPropertyInteger('SchaltkontaktVariableID'),
|
||||||
|
$this->ReadPropertyInteger('RueckmeldungVariableID'),
|
||||||
|
]);
|
||||||
|
if (in_array((int) $senderID, $relevanteIDs, true)) {
|
||||||
|
$this->aktualisiere(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RequestAction($ident, $wert): void
|
||||||
|
{
|
||||||
|
switch ($ident) {
|
||||||
|
case 'Aktiv':
|
||||||
|
$this->SetValue('Aktiv', (bool) $wert);
|
||||||
|
if (!(bool) $wert) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->SetTimerInterval('VorgabeTimeout', 0);
|
||||||
|
}
|
||||||
|
$this->aktualisiere(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'Melden':
|
||||||
|
if ((bool) $wert) {
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||||
|
}
|
||||||
|
$this->aktualisiere(false);
|
||||||
|
$this->sendeVerbraucherdaten();
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'Umschaltfreigabe':
|
||||||
|
case 'VorgabeTimeout':
|
||||||
|
case 'Tagesplanung':
|
||||||
|
$this->SetTimerInterval((string) $ident, 0);
|
||||||
|
$this->aktualisiere(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'ManagerdatenEmpfangen':
|
||||||
|
if (!is_string($wert)) {
|
||||||
|
throw new InvalidArgumentException('Managerdaten muessen als JSON uebergeben werden.');
|
||||||
|
}
|
||||||
|
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($daten)) {
|
||||||
|
throw new InvalidArgumentException('Managerdaten muessen ein JSON-Objekt sein.');
|
||||||
|
}
|
||||||
|
$this->ManagerdatenEmpfangen($daten);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $daten */
|
||||||
|
public function ManagerdatenEmpfangen(array $daten): void
|
||||||
|
{
|
||||||
|
Nachrichtenvertrag::pruefeManagerdaten($daten);
|
||||||
|
if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) {
|
||||||
|
throw new InvalidArgumentException('Managerdaten sind an eine andere Instanz adressiert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$managerID = $daten['Kopf']['AbsenderID'];
|
||||||
|
if (!in_array($managerID, $this->zugeordneteManagerIDs(), true)) {
|
||||||
|
throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->aktualisiere(false);
|
||||||
|
$sollleistung = $daten['Sollleistung_W'];
|
||||||
|
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
|
||||||
|
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
|
||||||
|
$this->setzeZustand('Sollleistung', $sollleistung);
|
||||||
|
$this->setzeZustand('SollwertGueltig', true);
|
||||||
|
$this->planeVorgabeTimeout(time());
|
||||||
|
$this->aktualisiere(false);
|
||||||
|
$this->planeMeldung();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiere(bool $meldungPlanen): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$jetzt = time();
|
||||||
|
$this->aktualisiereTageslaufzeit($jetzt);
|
||||||
|
$vorherigerSchaltzustand = (bool) $this->leseZustand('Schaltzustand');
|
||||||
|
$schaltzustand = $this->leseSchaltzustand();
|
||||||
|
|
||||||
|
if ($schaltzustand !== $vorherigerSchaltzustand) {
|
||||||
|
$this->setzeSchaltzustand($schaltzustand);
|
||||||
|
if ($this->ReadAttributeBoolean('Initialisiert')
|
||||||
|
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
||||||
|
) {
|
||||||
|
$this->starteUmschaltsperre($jetzt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
|
||||||
|
$this->pruefeVorgabeTimeout($jetzt);
|
||||||
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||||
|
&& !in_array(
|
||||||
|
(int) $this->leseZustand('Sollleistung'),
|
||||||
|
[0, $this->ReadPropertyInteger('Nennleistung')],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aktiv = (bool) $this->GetValue('Aktiv');
|
||||||
|
$fehlerfrei = $this->ReadAttributeString('Schaltfehler') === ''
|
||||||
|
&& !(bool) $this->leseZustand('Rueckmeldefehler');
|
||||||
|
$verfuegbar = $aktiv && $fehlerfrei;
|
||||||
|
$mindestlaufzeitErzwungen = $verfuegbar
|
||||||
|
&& $this->mussMindestlaufzeitErzwingen($jetzt);
|
||||||
|
$gesperrt = $jetzt < $this->ReadAttributeInteger('GesperrtBis');
|
||||||
|
|
||||||
|
$ziel = 0;
|
||||||
|
if ($verfuegbar) {
|
||||||
|
if ($mindestlaufzeitErzwungen) {
|
||||||
|
$ziel = $this->ReadPropertyInteger('Nennleistung');
|
||||||
|
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
||||||
|
$ziel = (int) $this->leseZustand('Sollleistung');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
||||||
|
if ($ziel !== $istleistung && (!$aktiv || (!$gesperrt && $fehlerfrei))) {
|
||||||
|
$this->schalte($ziel > 0, $jetzt);
|
||||||
|
$schaltzustand = $this->leseSchaltzustand();
|
||||||
|
$this->setzeSchaltzustand($schaltzustand);
|
||||||
|
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
||||||
|
$gesperrt = $jetzt < $this->ReadAttributeInteger('GesperrtBis');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setzeZustand('Sollleistung', $ziel);
|
||||||
|
$this->setzeZustand('Verfuegbar', $verfuegbar);
|
||||||
|
$aenderungMoeglich = $verfuegbar
|
||||||
|
&& !$mindestlaufzeitErzwungen
|
||||||
|
&& !$gesperrt
|
||||||
|
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
||||||
|
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
|
||||||
|
|
||||||
|
$angebot = EinStufigRegler::leistungsangebot(
|
||||||
|
$this->ReadPropertyInteger('Nennleistung'),
|
||||||
|
$istleistung,
|
||||||
|
$verfuegbar,
|
||||||
|
$aenderungMoeglich,
|
||||||
|
$mindestlaufzeitErzwungen
|
||||||
|
);
|
||||||
|
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
||||||
|
$this->aktualisiereStoerung();
|
||||||
|
$this->planeTimer($jetzt);
|
||||||
|
$this->SetStatus(
|
||||||
|
$this->ReadAttributeString('Schaltfehler') === ''
|
||||||
|
&& !(bool) $this->leseZustand('Rueckmeldefehler')
|
||||||
|
? self::STATUS_AKTIV
|
||||||
|
: self::STATUS_SCHALTFEHLER
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($meldungPlanen) {
|
||||||
|
$this->planeMeldung();
|
||||||
|
}
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->WriteAttributeString('Schaltfehler', $fehler->getMessage());
|
||||||
|
$this->setzeZustand('Verfuegbar', false);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', false);
|
||||||
|
$this->setzeStoerung($fehler->getMessage(), false);
|
||||||
|
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||||
|
$this->protokolliere('Aktualisierung', $fehler->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function schalte(bool $einschalten, int $jetzt): void
|
||||||
|
{
|
||||||
|
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
|
||||||
|
$aktorwert = $einschalten !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
|
||||||
|
|
||||||
|
$this->WriteAttributeBoolean('Schaltziel', $einschalten);
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', true);
|
||||||
|
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', true);
|
||||||
|
try {
|
||||||
|
RequestAction($kontaktID, $aktorwert);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage());
|
||||||
|
} finally {
|
||||||
|
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->starteUmschaltsperre($jetzt);
|
||||||
|
$istzustand = $this->leseSchaltzustand();
|
||||||
|
if ($istzustand === $einschalten) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', false);
|
||||||
|
} elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.');
|
||||||
|
} elseif ($this->ReadPropertyInteger('Umschaltabstand') === 0) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeString('Schaltfehler', '');
|
||||||
|
$this->protokolliere('Lastwechsel', ['Eingeschaltet' => $einschalten]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void
|
||||||
|
{
|
||||||
|
if (!$this->ReadAttributeBoolean('SchaltpruefungAusstehend')) {
|
||||||
|
if ((bool) $this->leseZustand('Rueckmeldefehler')
|
||||||
|
&& $schaltzustand === $this->ReadAttributeBoolean('Schaltziel')
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($schaltzustand === $this->ReadAttributeBoolean('Schaltziel')) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($jetzt >= $this->ReadAttributeInteger('GesperrtBis')) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function starteUmschaltsperre(int $jetzt): void
|
||||||
|
{
|
||||||
|
$gesperrtBis = $jetzt + $this->ReadPropertyInteger('Umschaltabstand');
|
||||||
|
$this->WriteAttributeInteger('GesperrtBis', $gesperrtBis);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiereTageslaufzeit(int $jetzt): void
|
||||||
|
{
|
||||||
|
$werte = EinStufigRegler::aktualisiereTageslaufzeit(
|
||||||
|
$this->ReadAttributeString('LaufzeitTag'),
|
||||||
|
(int) $this->leseZustand('Tageslaufzeit'),
|
||||||
|
$this->ReadAttributeInteger('LaufzeitStandZeit'),
|
||||||
|
$jetzt,
|
||||||
|
(bool) $this->leseZustand('Schaltzustand'),
|
||||||
|
$this->zeitzone()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->WriteAttributeString('LaufzeitTag', $werte['Tag']);
|
||||||
|
$this->WriteAttributeInteger('LaufzeitStandZeit', $werte['StandZeit']);
|
||||||
|
$this->setzeZustand('Tageslaufzeit', $werte['Sekunden']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mussMindestlaufzeitErzwingen(int $jetzt): bool
|
||||||
|
{
|
||||||
|
return EinStufigRegler::mussMindestlaufzeitErzwingen(
|
||||||
|
(int) $this->leseZustand('Tageslaufzeit'),
|
||||||
|
$this->ReadPropertyInteger('Mindestlaufzeit'),
|
||||||
|
EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pruefeVorgabeTimeout(int $jetzt): void
|
||||||
|
{
|
||||||
|
if (!(bool) $this->leseZustand('SollwertGueltig')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit');
|
||||||
|
if ($letzteVorgabe <= 0
|
||||||
|
|| $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout')
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->SetTimerInterval('VorgabeTimeout', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function planeTimer(int $jetzt): void
|
||||||
|
{
|
||||||
|
$gesperrtBis = $this->ReadAttributeInteger('GesperrtBis');
|
||||||
|
$this->SetTimerInterval(
|
||||||
|
'Umschaltfreigabe',
|
||||||
|
$gesperrtBis > $jetzt ? max(1, ($gesperrtBis - $jetzt) * 1000) : 0
|
||||||
|
);
|
||||||
|
$this->planeVorgabeTimeout($jetzt);
|
||||||
|
|
||||||
|
$bisTagesende = EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone());
|
||||||
|
$naechstePruefung = EinStufigRegler::sekundenBisNaechsteTagespruefung(
|
||||||
|
(int) $this->leseZustand('Tageslaufzeit'),
|
||||||
|
$this->ReadPropertyInteger('Mindestlaufzeit'),
|
||||||
|
$bisTagesende,
|
||||||
|
(bool) $this->leseZustand('Schaltzustand')
|
||||||
|
);
|
||||||
|
$this->SetTimerInterval('Tagesplanung', $naechstePruefung * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function planeVorgabeTimeout(int $jetzt): void
|
||||||
|
{
|
||||||
|
if (!(bool) $this->leseZustand('SollwertGueltig')) {
|
||||||
|
$this->SetTimerInterval('VorgabeTimeout', 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit')
|
||||||
|
+ $this->ReadPropertyInteger('VorgabeTimeout');
|
||||||
|
$this->SetTimerInterval('VorgabeTimeout', max(1, ($ablauf - $jetzt) * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function planeMeldung(): void
|
||||||
|
{
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initialisiereZustand(): void
|
||||||
|
{
|
||||||
|
$jetzt = time();
|
||||||
|
$schaltzustand = $this->leseSchaltzustand();
|
||||||
|
|
||||||
|
if (!$this->ReadAttributeBoolean('Initialisiert')) {
|
||||||
|
$this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt));
|
||||||
|
$this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt);
|
||||||
|
$this->setzeZustand('Tageslaufzeit', 0);
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', false);
|
||||||
|
$this->WriteAttributeBoolean('Initialisiert', true);
|
||||||
|
}
|
||||||
|
$this->setzeSchaltzustand($schaltzustand);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setzeSchaltzustand(bool $eingeschaltet): void
|
||||||
|
{
|
||||||
|
$this->setzeZustand('Schaltzustand', $eingeschaltet);
|
||||||
|
$this->setzeZustand('Istleistung', (float) $this->leistungFuerSchaltzustand($eingeschaltet));
|
||||||
|
$this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function leseSchaltzustand(): bool
|
||||||
|
{
|
||||||
|
$rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID');
|
||||||
|
if ($rueckmeldungID > 0) {
|
||||||
|
return (bool) GetValue($rueckmeldungID);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kontaktwert = (bool) GetValue($this->ReadPropertyInteger('SchaltkontaktVariableID'));
|
||||||
|
|
||||||
|
return $kontaktwert !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function leistungFuerSchaltzustand(bool $eingeschaltet): int
|
||||||
|
{
|
||||||
|
return $eingeschaltet ? $this->ReadPropertyInteger('Nennleistung') : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiereStoerung(): void
|
||||||
|
{
|
||||||
|
$texte = [];
|
||||||
|
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||||
|
$texte[] = $this->ReadAttributeString('Schaltfehler');
|
||||||
|
}
|
||||||
|
if ((bool) $this->leseZustand('Rueckmeldefehler')) {
|
||||||
|
$texte[] = 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.';
|
||||||
|
}
|
||||||
|
$this->setzeStoerung(implode("\n", $texte), $texte === []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setzeStoerung(string $text, bool $fehlerfrei): void
|
||||||
|
{
|
||||||
|
$this->setzeZustand('Stoerung', !$fehlerfrei);
|
||||||
|
$this->setzeZustand('Stoertext', $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendeVerbraucherdaten(): void
|
||||||
|
{
|
||||||
|
foreach ($this->zugeordneteManagerIDs() as $managerID) {
|
||||||
|
$daten = $this->baueVerbraucherdaten($managerID);
|
||||||
|
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
|
||||||
|
try {
|
||||||
|
IPS_RequestAction(
|
||||||
|
$managerID,
|
||||||
|
'VerbraucherdatenEmpfangen',
|
||||||
|
json_encode($daten, JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->protokolliere('Managerkommunikation', $fehler->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function baueVerbraucherdaten(int $managerID): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Kopf' => [
|
||||||
|
'Version' => Nachrichtenvertrag::VERSION,
|
||||||
|
'AbsenderID' => $this->InstanceID,
|
||||||
|
'EmpfaengerID' => $managerID,
|
||||||
|
'Zeitpunkt' => time(),
|
||||||
|
],
|
||||||
|
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
||||||
|
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
||||||
|
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
||||||
|
'AenderungMoeglich' => (bool) $this->leseZustand('AenderungMoeglich'),
|
||||||
|
'Verfuegbar' => (bool) $this->leseZustand('Verfuegbar'),
|
||||||
|
'Istleistung_W' => (float) $this->leseZustand('Istleistung'),
|
||||||
|
'Leistungsquelle' => Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET,
|
||||||
|
'Zustand' => [
|
||||||
|
[
|
||||||
|
'Kennung' => 'Sollleistung_W',
|
||||||
|
'Art' => 'Sollwert',
|
||||||
|
'Wert' => (bool) $this->leseZustand('SollwertGueltig')
|
||||||
|
? (int) $this->leseZustand('Sollleistung')
|
||||||
|
: null,
|
||||||
|
'Einheit' => 'W',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Schaltzustand',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => (bool) $this->leseZustand('Schaltzustand'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Tageslaufzeit_s',
|
||||||
|
'Art' => 'Istwert',
|
||||||
|
'Wert' => (int) $this->leseZustand('Tageslaufzeit'),
|
||||||
|
'Einheit' => 's',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Rueckmeldefehler',
|
||||||
|
'Art' => 'Stoerung',
|
||||||
|
'Wert' => (bool) $this->leseZustand('Rueckmeldefehler'),
|
||||||
|
'Einheit' => '',
|
||||||
|
'Text' => (bool) $this->leseZustand('Rueckmeldefehler')
|
||||||
|
? 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.'
|
||||||
|
: '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
private function zugeordneteManagerIDs(): array
|
||||||
|
{
|
||||||
|
$ergebnis = [];
|
||||||
|
foreach (IPS_GetInstanceListByModuleID(self::MANAGER_MODULE_ID) as $managerID) {
|
||||||
|
try {
|
||||||
|
$automatisch = (bool) IPS_GetProperty($managerID, 'AutomatischeSuche');
|
||||||
|
$property = $automatisch
|
||||||
|
? 'AutomatischeVerbraucherZuordnung'
|
||||||
|
: 'VerbraucherZuordnung';
|
||||||
|
$zuordnung = $this->leseManagerZuordnung($managerID, $property);
|
||||||
|
if ($automatisch && $zuordnung === []) {
|
||||||
|
$zuordnung = $this->leseManagerZuordnung($managerID, 'VerbraucherZuordnung');
|
||||||
|
}
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($zuordnung as $eintrag) {
|
||||||
|
if (!is_array($eintrag) || ($eintrag['Aktiv'] ?? false) !== true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null;
|
||||||
|
if ($instanzID === $this->InstanceID) {
|
||||||
|
$ergebnis[] = (int) $managerID;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($ergebnis));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<mixed> */
|
||||||
|
private function leseManagerZuordnung(int $managerID, string $property): array
|
||||||
|
{
|
||||||
|
$zuordnung = json_decode(
|
||||||
|
(string) IPS_GetProperty($managerID, $property),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return is_array($zuordnung) ? $zuordnung : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pruefeKonfiguration(): void
|
||||||
|
{
|
||||||
|
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
|
||||||
|
if ($this->ReadPropertyInteger($property) < 0) {
|
||||||
|
throw new InvalidArgumentException($property . ' muss mindestens 0 sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (['Meldeintervall', 'VorgabeTimeout'] as $property) {
|
||||||
|
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||||
|
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($this->ReadPropertyInteger('Nennleistung') <= 0) {
|
||||||
|
throw new InvalidArgumentException('Nennleistung muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
if ($this->ReadPropertyInteger('Mindestlaufzeit') < 0
|
||||||
|
|| $this->ReadPropertyInteger('Mindestlaufzeit') > 86400
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('Mindestlaufzeit muss zwischen 0 und 86400 Sekunden liegen.');
|
||||||
|
}
|
||||||
|
if ($this->ReadPropertyInteger('Umschaltabstand') < 0) {
|
||||||
|
throw new InvalidArgumentException('Umschaltabstand darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
|
||||||
|
$this->pruefeBooleanVariable($kontaktID, 'Schaltkontakt', true);
|
||||||
|
|
||||||
|
$rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID');
|
||||||
|
if ($rueckmeldungID > 0) {
|
||||||
|
$this->pruefeBooleanVariable($rueckmeldungID, 'Rueckmeldung', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pruefeBooleanVariable(int $variableID, string $bezeichnung, bool $aktionErforderlich): void
|
||||||
|
{
|
||||||
|
if ($variableID <= 0 || !IPS_VariableExists($variableID)) {
|
||||||
|
throw new InvalidArgumentException($bezeichnung . ' ist nicht eingerichtet.');
|
||||||
|
}
|
||||||
|
$variable = IPS_GetVariable($variableID);
|
||||||
|
if ((int) $variable['VariableType'] !== 0) {
|
||||||
|
throw new InvalidArgumentException($bezeichnung . ' muss eine Booleanvariable sein.');
|
||||||
|
}
|
||||||
|
if ($aktionErforderlich
|
||||||
|
&& (int) $variable['VariableAction'] <= 0
|
||||||
|
&& (int) $variable['VariableCustomAction'] <= 0
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException($bezeichnung . ' benoetigt eine Standard- oder benutzerdefinierte Aktion.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function registriereMeldungen(): void
|
||||||
|
{
|
||||||
|
$alteIDs = array_unique(array_filter([
|
||||||
|
$this->ReadAttributeInteger('RegistrierterSchaltkontakt'),
|
||||||
|
$this->ReadAttributeInteger('RegistrierteRueckmeldung'),
|
||||||
|
]));
|
||||||
|
$neueIDs = array_unique(array_filter([
|
||||||
|
$this->ReadPropertyInteger('SchaltkontaktVariableID'),
|
||||||
|
$this->ReadPropertyInteger('RueckmeldungVariableID'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
foreach ($alteIDs as $alteID) {
|
||||||
|
if (!in_array($alteID, $neueIDs, true)) {
|
||||||
|
$this->UnregisterMessage($alteID, self::VM_UPDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($neueIDs as $neueID) {
|
||||||
|
if (IPS_VariableExists($neueID)) {
|
||||||
|
$this->RegisterMessage($neueID, self::VM_UPDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'RegistrierterSchaltkontakt',
|
||||||
|
$this->ReadPropertyInteger('SchaltkontaktVariableID')
|
||||||
|
);
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'RegistrierteRueckmeldung',
|
||||||
|
$this->ReadPropertyInteger('RueckmeldungVariableID')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiereVariablen(): void
|
||||||
|
{
|
||||||
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
|
if ($ident === 'Leistungsquelle') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||||
|
$this->setzeZustand($ident, GetValue($variablenID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||||
|
$this->registriereVerbraucherDiagnose();
|
||||||
|
$this->RegisterVariableBoolean('Rueckmeldefehler', 'Rueckmeldefehler', '~Alert', 200);
|
||||||
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
|
$wert = $ident === 'Leistungsquelle'
|
||||||
|
? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET
|
||||||
|
: $this->leseZustand($ident);
|
||||||
|
$this->setzeZustand($ident, $wert);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||||
|
$this->UnregisterVariable($ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param bool|int|float|string $wert */
|
||||||
|
private function setzeZustand(string $ident, $wert): void
|
||||||
|
{
|
||||||
|
$booleanAttribute = [
|
||||||
|
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
||||||
|
'Verfuegbar' => 'ZustandVerfuegbar',
|
||||||
|
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
||||||
|
'Stoerung' => 'ZustandStoerung',
|
||||||
|
'Schaltzustand' => 'ZustandSchaltzustand',
|
||||||
|
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
|
||||||
|
];
|
||||||
|
$integerAttribute = [
|
||||||
|
'Sollleistung' => 'ZustandSollleistung',
|
||||||
|
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
||||||
|
];
|
||||||
|
$floatAttribute = [
|
||||||
|
'Istleistung' => 'ZustandIstleistung',
|
||||||
|
];
|
||||||
|
$stringAttribute = [
|
||||||
|
'Stoertext' => 'ZustandStoertext',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($booleanAttribute[$ident])) {
|
||||||
|
$wert = (bool) $wert;
|
||||||
|
$this->WriteAttributeBoolean($booleanAttribute[$ident], $wert);
|
||||||
|
} elseif (isset($integerAttribute[$ident])) {
|
||||||
|
$wert = (int) $wert;
|
||||||
|
$this->WriteAttributeInteger($integerAttribute[$ident], $wert);
|
||||||
|
} elseif (isset($floatAttribute[$ident])) {
|
||||||
|
$wert = (float) $wert;
|
||||||
|
$this->WriteAttributeFloat($floatAttribute[$ident], $wert);
|
||||||
|
} elseif (isset($stringAttribute[$ident])) {
|
||||||
|
$wert = (string) $wert;
|
||||||
|
$this->WriteAttributeString($stringAttribute[$ident], $wert);
|
||||||
|
} elseif ($ident !== 'Leistungsquelle') {
|
||||||
|
throw new LogicException('Unbekannter interner Zustand: ' . $ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
||||||
|
SetValue($variablenID, $wert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return bool|int|float|string */
|
||||||
|
private function leseZustand(string $ident)
|
||||||
|
{
|
||||||
|
$booleanAttribute = [
|
||||||
|
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
||||||
|
'Verfuegbar' => 'ZustandVerfuegbar',
|
||||||
|
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
||||||
|
'Stoerung' => 'ZustandStoerung',
|
||||||
|
'Schaltzustand' => 'ZustandSchaltzustand',
|
||||||
|
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
|
||||||
|
];
|
||||||
|
$integerAttribute = [
|
||||||
|
'Sollleistung' => 'ZustandSollleistung',
|
||||||
|
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
||||||
|
];
|
||||||
|
$floatAttribute = [
|
||||||
|
'Istleistung' => 'ZustandIstleistung',
|
||||||
|
];
|
||||||
|
$stringAttribute = [
|
||||||
|
'Stoertext' => 'ZustandStoertext',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($booleanAttribute[$ident])) {
|
||||||
|
return $this->ReadAttributeBoolean($booleanAttribute[$ident]);
|
||||||
|
}
|
||||||
|
if (isset($integerAttribute[$ident])) {
|
||||||
|
return $this->ReadAttributeInteger($integerAttribute[$ident]);
|
||||||
|
}
|
||||||
|
if (isset($floatAttribute[$ident])) {
|
||||||
|
return $this->ReadAttributeFloat($floatAttribute[$ident]);
|
||||||
|
}
|
||||||
|
if (isset($stringAttribute[$ident])) {
|
||||||
|
return $this->ReadAttributeString($stringAttribute[$ident]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException('Unbekannter interner Zustand: ' . $ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
private function leseLeistungsangebot(): array
|
||||||
|
{
|
||||||
|
$angebot = json_decode(
|
||||||
|
$this->ReadAttributeString('Leistungsangebot'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
if (!is_array($angebot)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_map('intval', $angebot));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function zeitzone(): DateTimeZone
|
||||||
|
{
|
||||||
|
return new DateTimeZone(date_default_timezone_get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deaktiviereTimer(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
'Meldezyklus',
|
||||||
|
'RueckmeldungVerzoegert',
|
||||||
|
'Umschaltfreigabe',
|
||||||
|
'VorgabeTimeout',
|
||||||
|
'Tagesplanung',
|
||||||
|
] as $timer) {
|
||||||
|
$this->SetTimerInterval($timer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param mixed $daten */
|
||||||
|
private function protokolliere(string $bezeichnung, $daten): void
|
||||||
|
{
|
||||||
|
if (!$this->ReadPropertyBoolean('LoggingEin')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->SendDebug(
|
||||||
|
$bezeichnung,
|
||||||
|
is_string($daten) ? $daten : json_encode($daten, JSON_THROW_ON_ERROR),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
{
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Manager und Zeitverhalten",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "PrioritaetPV",
|
||||||
|
"caption": "Priorität PV",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "PrioritaetPeak",
|
||||||
|
"caption": "Priorität Peak",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Meldeintervall",
|
||||||
|
"caption": "Meldeintervall",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "VorgabeTimeout",
|
||||||
|
"caption": "Vorgabe-Timeout",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "LastwechselSperrzeit",
|
||||||
|
"caption": "Mindestzeit zwischen Lastwechseln",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Speichereinstellungen",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "List",
|
||||||
|
"name": "LeistungsStufen",
|
||||||
|
"caption": "Exklusive Leistungsstufen und Schaltkontakte",
|
||||||
|
"add": true,
|
||||||
|
"delete": true,
|
||||||
|
"sortable": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"caption": "Stufe",
|
||||||
|
"name": "Stufe",
|
||||||
|
"width": "100px",
|
||||||
|
"add": 1,
|
||||||
|
"edit": {
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "Leistung",
|
||||||
|
"name": "Leistung",
|
||||||
|
"width": "180px",
|
||||||
|
"add": 0,
|
||||||
|
"edit": {
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"minimum": 1,
|
||||||
|
"suffix": " W"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "Schaltkontakt",
|
||||||
|
"name": "Schaltkontakt_Stufe",
|
||||||
|
"width": "auto",
|
||||||
|
"add": 0,
|
||||||
|
"edit": {
|
||||||
|
"type": "SelectVariable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "Boilerfuehler_PT1",
|
||||||
|
"caption": "Boilerfühler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "LegionellenfunktionAktiv",
|
||||||
|
"caption": "Legionellenschaltung aktiv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "LegionellenMinimalintervallTage",
|
||||||
|
"caption": "Frühestens nach",
|
||||||
|
"suffix": " Tagen",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "LegionellenMaximalintervallTage",
|
||||||
|
"caption": "Spätestens nach",
|
||||||
|
"suffix": " Tagen",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Erweiterte Speichereinstellungen",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Boilervolumen",
|
||||||
|
"caption": "Boilervolumen",
|
||||||
|
"suffix": " l",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Hysterese",
|
||||||
|
"caption": "Temperaturhysterese",
|
||||||
|
"suffix": " K",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "List",
|
||||||
|
"name": "Zeitplan",
|
||||||
|
"caption": "Zeitplan für Solltemperaturen",
|
||||||
|
"add": true,
|
||||||
|
"delete": true,
|
||||||
|
"sortable": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"caption": "Uhrzeit",
|
||||||
|
"name": "Uhrzeit",
|
||||||
|
"width": "150px",
|
||||||
|
"add": "00:00",
|
||||||
|
"edit": {
|
||||||
|
"type": "ValidationTextBox"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "Solltemperatur",
|
||||||
|
"name": "Solltemperatur",
|
||||||
|
"width": "180px",
|
||||||
|
"add": 50,
|
||||||
|
"edit": {
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 100,
|
||||||
|
"digits": 1,
|
||||||
|
"suffix": " °C"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Erweiterte sonstige Einstellungen",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "TemperaturMaxAlter",
|
||||||
|
"caption": "Maximales Messwertalter",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "BoilertemperaturGlaetten",
|
||||||
|
"caption": "Boilertemperatur mit PT1 glätten"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "ZeitKonstante",
|
||||||
|
"caption": "PT1-Zeitkonstante",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "EinstellungenInVisu",
|
||||||
|
"caption": "Temperatursollwerte in der Visualisierung anzeigen und bedienen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "DiagnosevariablenAnzeigen",
|
||||||
|
"caption": "Diagnosevariablen anlegen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "LoggingEin",
|
||||||
|
"caption": "Laufendes Debug-Logging aktivieren"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
{
|
||||||
|
"code": 102,
|
||||||
|
"icon": "active",
|
||||||
|
"caption": "Aktiv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 201,
|
||||||
|
"icon": "inactive",
|
||||||
|
"caption": "Temperaturmessung fehlt oder ist veraltet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 202,
|
||||||
|
"icon": "error",
|
||||||
|
"caption": "Konfiguration ungültig"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 203,
|
||||||
|
"icon": "error",
|
||||||
|
"caption": "Schaltfehler"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "{B7C54AF4-AD7D-4FE4-B75D-203693906251}",
|
||||||
|
"name": "VerbraucherWarmwassererwaermer",
|
||||||
|
"type": 3,
|
||||||
|
"vendor": "Enelix",
|
||||||
|
"aliases": [
|
||||||
|
"Wassererwärmer"
|
||||||
|
],
|
||||||
|
"parentRequirements": [],
|
||||||
|
"childRequirements": [],
|
||||||
|
"implemented": [],
|
||||||
|
"prefix": "ENELIX",
|
||||||
|
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Warmwassererwaermer"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
# Obere Anschlüsse des Managers
|
||||||
|
|
||||||
|
> Status: Diskussionsentwurf. Anbieterformate und konkrete Gegenstellen sind
|
||||||
|
> noch nicht festgelegt.
|
||||||
|
|
||||||
|
Diese Anschlüsse sind optional. Sie werden im Manager konfiguriert und erzeugen
|
||||||
|
keine Abhängigkeit zwischen Enelix EMS und Enelix Utils.
|
||||||
|
|
||||||
|
| Anschluss | Aufgabe | Gegenstelle |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| SDL/VGT | Zeitlich gültige Leistungsaufträge und Rückmeldungen | Optional das unabhängige Utils-Modul VGT-Schnittstelle |
|
||||||
|
| Prognose / Forecast | Vorhersage für Produktion und bei Bedarf Verbrauch | Externer Anbieter; kein Repo-Modul |
|
||||||
|
| Lizenzierung | Prüfung freigeschalteter Funktionen | Externer Lizenzdienst; kein Repo-Modul |
|
||||||
|
| Störüberwachung | Weitergabe von Zuständen und Störungen | Externer Empfänger; kein Repo-Modul |
|
||||||
|
|
||||||
|
## Gemeinsame Grundsätze
|
||||||
|
|
||||||
|
- Die Auswahl der Gegenstelle erfolgt ausschliesslich im Manager.
|
||||||
|
- Anbieterformate werden an diesem Anschluss übersetzt und gelangen nicht in
|
||||||
|
die Verbraucher-Schnittstelle.
|
||||||
|
- Jeder Anschluss meldet im Manager `NichtVerwendet`, `Verbunden` oder `Fehler`.
|
||||||
|
- Fehlende optionale Anschlüsse dürfen die lokale EMS-Grundfunktion nicht
|
||||||
|
blockieren.
|
||||||
|
|
||||||
|
## Noch festzulegen
|
||||||
|
|
||||||
|
- Anbieter und Datenformat für Prognose, Lizenzierung und Störüberwachung
|
||||||
|
- Aufbau, Gültigkeitszeitraum und Rückmeldung eines SDL-Auftrags
|
||||||
|
- Authentisierung und Verhalten bei Verbindungsunterbruch
|
||||||
|
- Welche Lizenzfehler nur Funktionen sperren und welche den Manager stoppen
|
||||||
@@ -81,3 +81,60 @@ public function VerbraucherdatenEmpfangen(array $daten): void;
|
|||||||
- Eine aktive Stoerung wird mit `Art=Stoerung` und `Wert=true` gemeldet; eine behobene mit `false`.
|
- Eine aktive Stoerung wird mit `Art=Stoerung` und `Wert=true` gemeldet; eine behobene mit `false`.
|
||||||
- Verbraucher werden ausschliesslich im Manager zugeordnet. Der Verbraucher besitzt keine Manager-ID-Property.
|
- Verbraucher werden ausschliesslich im Manager zugeordnet. Der Verbraucher besitzt keine Manager-ID-Property.
|
||||||
|
|
||||||
|
## Technische Umsetzung in IP-Symcon
|
||||||
|
|
||||||
|
Der Transport erfolgt über `IPS_RequestAction` mit JSON. `MessageSink` erkennt
|
||||||
|
registrierte Änderungen; der empfangende Action-Handler prüft das Paket und
|
||||||
|
ruft danach genau eine fachliche Empfangsmethode auf. Empfang und Neuberechnung
|
||||||
|
werden intern entkoppelt, damit keine gegenseitige Endlosschleife entsteht.
|
||||||
|
|
||||||
|
Das PHP-Interface legt nur die Methode fest. Die folgenden gemeinsamen
|
||||||
|
Symcon-Datenpunkte werden durch `VerbraucherBasisTrait` registriert.
|
||||||
|
|
||||||
|
### Gemeinsame Properties aller Verbraucher
|
||||||
|
|
||||||
|
| Ident | Typ | Standard | Beschreibung |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `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 | `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. |
|
||||||
|
|
||||||
|
### Gemeinsame Variablen aller Verbraucher
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Start `false`. |
|
||||||
|
| `Istleistung` | Float / Anzeige | Aktuelle Leistung in W; nur zusammen mit `Leistungsquelle` gültig. |
|
||||||
|
| `Leistungsquelle` | Integer / Anzeige | `0` nicht vorhanden, `1` berechnet, `2` gemessen. |
|
||||||
|
| `Sollleistung` | Integer / Anzeige | Tatsächlich angenommene Vorgabe in W. |
|
||||||
|
| `SollwertGueltig` | Boolean / Anzeige | Kennzeichnet eine aktuell gültige Vorgabe. |
|
||||||
|
| `Verfuegbar` | Boolean / Anzeige | Verbraucher grundsätzlich für EMS-Steuerung verfügbar. |
|
||||||
|
| `AenderungMoeglich` | Boolean / Anzeige | Neue Vorgabe darf momentan übernommen werden. |
|
||||||
|
| `Stoerung` | Boolean / Anzeige | Mindestens ein Zustandseintrag der Art `Stoerung` ist aktiv. |
|
||||||
|
| `Stoertext` | String / Anzeige | Zusammengefasste lesbare Störbeschreibung. |
|
||||||
|
|
||||||
|
`Leistungswerte_W` und `Zustand` werden intern gehalten und direkt in die
|
||||||
|
Nachricht geschrieben. Dafür werden keine zusätzlichen Symcon-Variablen
|
||||||
|
angelegt. Der gemeinsame Zustandseintrag `Sollleistung_W` wird durch die Basis
|
||||||
|
ergänzt. Während einer Lastwechselsperre meldet ein Verbraucher
|
||||||
|
`AenderungMoeglich` als `false` und beschränkt `Leistungswerte_W` auf die aktuell
|
||||||
|
gehaltene Leistung. Separate Felder wie `Idle` oder `IdleCounter` sind nicht
|
||||||
|
Bestandteil des Vertrags.
|
||||||
|
|
||||||
|
## Zeitverhalten
|
||||||
|
|
||||||
|
- Rückmeldung nach dem Start, bei relevanten Änderungen und zusätzlich alle
|
||||||
|
`Meldeintervall` Sekunden.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Obere Anschlüsse des Managers
|
||||||
|
|
||||||
|
SDL/VGT, Prognose, Lizenzierung und Störüberwachung verändern den
|
||||||
|
Manager-Verbraucher-Vertrag nicht. Ihr aktueller Diskussionsstand ist in
|
||||||
|
[Obere-Anschluesse.md](Obere-Anschluesse.md) beschrieben.
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# ADR 0001: Warmwassererwaermer verwendet ausschliesslich Vertrag 3.0
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Akzeptiert.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
Der neue `VerbraucherWarmwassererwaermer` implementiert die bestehende
|
||||||
|
`VerbraucherSchnittstelle` und den `Nachrichtenvertrag` in Version `3.0`.
|
||||||
|
Die Enelix-1-Variablen `Power`, `PowerSteps`, `PV_Prio`, `Sperre_Prio`,
|
||||||
|
`Is_Peak_Shaving` und `Leistung_Delta` werden nicht als zweite parallele
|
||||||
|
Manager-Schnittstelle weitergefuehrt.
|
||||||
|
|
||||||
|
Der Manager bleibt allein fuer die Betriebsart und Leistungsverteilung
|
||||||
|
verantwortlich. Lokale Temperatur- und Hygienesicherheit wird als eingeschraenktes
|
||||||
|
Leistungsangebot mit `AenderungMoeglich=false` gemeldet.
|
||||||
|
|
||||||
|
## Folgen
|
||||||
|
|
||||||
|
- Es gibt genau eine fachliche Empfangsmethode pro Richtung.
|
||||||
|
- Der Verbraucher hat keine Manager-ID-Property.
|
||||||
|
- Alte Instanzen werden anhand der Migrationstabelle neu konfiguriert.
|
||||||
|
- Manager und Verbraucher koennen unabhaengig getestet werden.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# ADR 0002: Verbraucher 1-Stufig arbeitet ereignisbasiert
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Akzeptiert.
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Das Enelix-1-Modul berechnete seinen Zustand in einem festen Intervall und
|
||||||
|
bildete die Lastwechselsperre ueber `Interval`, `IdleCounterMax` und weitere
|
||||||
|
zyklusabhaengige Zaehler ab. Dieses Verhalten passt nicht zur
|
||||||
|
ereignisbasierten Enelix-2-Kommunikation und machte reale Zeitabstaende von
|
||||||
|
mehreren Properties abhaengig.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
Der neue `VerbraucherEinStufig` verwendet keine zyklische Regelberechnung.
|
||||||
|
Schaltkontakt, Rueckmeldung, Manager-Vorgabe, Freigabe, Vorgabeablauf und
|
||||||
|
Tagesplanung loesen die Regelung direkt aus.
|
||||||
|
|
||||||
|
Als einziger modulspezifischer Wert fuer die Lastwechselbegrenzung wird
|
||||||
|
`Umschaltabstand` in Sekunden registriert. Der Standardwert ist 5 Sekunden.
|
||||||
|
Das Ende der Sperre wird durch einen einmaligen Timer ausgeloest.
|
||||||
|
|
||||||
|
Das gemeinsame `Meldeintervall` bleibt erhalten, weil Vertrag `3.0`
|
||||||
|
zusaetzlich zu Ereignismeldungen eine periodische Vollmeldung fordert. Dieser
|
||||||
|
Timer ist kein Regelzyklus.
|
||||||
|
|
||||||
|
Die Tagesmindestlaufzeit wird in realen Sekunden und lokaler Symcon-Zeitzone
|
||||||
|
gezaehlt. Das Modul erzwingt den Betrieb erst zum spaetestmoeglichen Zeitpunkt,
|
||||||
|
an dem die fehlende Laufzeit vor Mitternacht noch erreicht werden kann.
|
||||||
|
|
||||||
|
## Alternativen
|
||||||
|
|
||||||
|
- Ein fester Regelzyklus wurde verworfen, weil Reaktionszeit und Sperrzeit
|
||||||
|
wieder voneinander abhaengen wuerden.
|
||||||
|
- Getrennte Ein- und Ausschaltzeiten wurden verworfen, weil sie fuer diese
|
||||||
|
Adaption nicht freigegeben sind.
|
||||||
|
- Die feste Nachtphase aus Enelix 1 wurde verworfen, weil sie weder
|
||||||
|
konfigurierbar noch Teil der Enelix-2-Spezifikation ist.
|
||||||
|
|
||||||
|
## Folgen
|
||||||
|
|
||||||
|
- Lastwechsel reagieren ohne Polling auf relevante Ereignisse.
|
||||||
|
- `Interval` und `IdleCounterMax` entfallen.
|
||||||
|
- Der Manager sieht waehrend einer Sperre nur die aktuelle Leistung und
|
||||||
|
`AenderungMoeglich=false`.
|
||||||
|
- Ein optionaler Rueckmeldekontakt muss innerhalb des Umschaltabstands folgen.
|
||||||
|
- Tageslaufzeit und Mindestlaufzeit sind unabhaengig von einer Zyklusdauer.
|
||||||
|
- Das Modul bleibt vollstaendig im Repository Enelix EMS; Enelix Utils erhaelt
|
||||||
|
keine geraetespezifische Logik.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Eine spaetere sperrbare Variante wird separat spezifiziert.
|
||||||
|
- Unterschiedliche Ein- und Ausschaltabstaende benoetigen eine neue
|
||||||
|
Architekturentscheidung.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Migration von Boiler_x_Stufig
|
||||||
|
|
||||||
|
## Eigenschaften
|
||||||
|
|
||||||
|
| Enelix 1 | Enelix 2 | Hinweis |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `LeistungsStufen` | `LeistungsStufen` | Felder `Stufe`, `Leistung`, `Schaltkontakt_Stufe` bleiben erhalten. |
|
||||||
|
| `ZeitKonstante` | `ZeitKonstante` | PT1-Berechnung verwendet die tatsaechlich verstrichene Zeit. |
|
||||||
|
| `Boilerfuehler_PT1` | `Boilerfuehler_PT1` | Alter und Datentyp werden geprueft. |
|
||||||
|
| `Boilertemperatur_glätten` | `BoilertemperaturGlaetten` | ASCII-sicherer Ident. |
|
||||||
|
| `Boilervolumen` | `Boilervolumen` | Wird fuer die Zeitplanprognose verwendet. |
|
||||||
|
| `Zeitplan` | `Zeitplan` | JSON-Liste mit `Uhrzeit` und `Solltemperatur`. |
|
||||||
|
| `Interval` | entfaellt | Die Regelung wird ereignisbasiert ausgeloest. |
|
||||||
|
| `IdleCounterMax` | `LastwechselSperrzeit` | Zykluszaehler wird durch eine Sperrzeit in Sekunden ersetzt; Standard `5`. |
|
||||||
|
|
||||||
|
Die neuen Properties `TemperaturMaxAlter`, `Hysterese`, die Parameter der
|
||||||
|
Legionellenfunktion und `DiagnosevariablenAnzeigen` ersetzen feste Werte und
|
||||||
|
nicht pruefbare Annahmen des Altmoduls.
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
|
| Enelix 1 | Enelix 2 |
|
||||||
|
| --- | --- |
|
||||||
|
| `Mindesttemperatur` | `Mindesttemperatur` |
|
||||||
|
| `Maximaltemperatur` | `Maximaltemperatur` |
|
||||||
|
| `Legionellentemperatur` | `Legionellentemperatur` |
|
||||||
|
| `Boilertemperatur` | `Boilertemperatur` |
|
||||||
|
| `LegioCounter` | `LegioCounter` |
|
||||||
|
| `Idle` | entfaellt; Freigabe ueber `AenderungMoeglich` |
|
||||||
|
| `IdleCounter` | entfaellt; Sperre ueber `Leistungswerte_W` und `AenderungMoeglich` |
|
||||||
|
| `Aktuelle_Leistung` | gemeinsames `Istleistung` |
|
||||||
|
| `Bezogene_Energie` | `BezogeneEnergie` in kWh |
|
||||||
|
| `PV_Prio` | Property `PrioritaetPV` |
|
||||||
|
| `Sperre_Prio` | Property `PrioritaetPeak` |
|
||||||
|
| `Power` | gemeinsames `Sollleistung` |
|
||||||
|
| `PowerSteps` | Vertragsfeld `Leistungswerte_W`, keine Variable |
|
||||||
|
| `Is_Peak_Shaving` | entfaellt; Betriebsart liegt im Manager |
|
||||||
|
| `Leistung_Delta` | entfaellt; Leistungsverteilung liegt im Manager |
|
||||||
|
|
||||||
|
## Inbetriebnahme
|
||||||
|
|
||||||
|
1. Neue Instanz `VerbraucherWarmwassererwaermer` anlegen.
|
||||||
|
2. Fuehler, Leistungsstufen und Schaltkontakte uebertragen.
|
||||||
|
3. Temperaturvariablen einmalig mit den bisherigen Sollwerten setzen.
|
||||||
|
4. Die Instanz im Enelix-2-Manager unter `VerbraucherZuordnung` aktiv zuordnen.
|
||||||
|
5. Erst danach `Aktiv` einschalten und jede Stufe beaufsichtigt pruefen.
|
||||||
|
|
||||||
|
Das Modul verwendet eine neue Modul-ID. Eine automatische Umwandlung der alten
|
||||||
|
Instanz findet deshalb nicht statt.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Migration von Verbraucher_1_Stufig
|
||||||
|
|
||||||
|
Das Enelix-1-Modul wird nicht in-place aktualisiert. Das neue Modul verwendet
|
||||||
|
eine neue Modul-ID, den Vertrag `3.0` und eine ereignisbasierte Regelung.
|
||||||
|
|
||||||
|
## Eigenschaften
|
||||||
|
|
||||||
|
| Enelix 1 | Enelix 2 | Migration |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BoilerLeistung` | `Nennleistung` | Wert in ganzen Watt uebernehmen. |
|
||||||
|
| `Schaltkontakt1` | `SchaltkontaktVariableID` | Boolean-Aktor uebernehmen und Aktion pruefen. |
|
||||||
|
| nicht vorhanden | `SchaltkontaktInvertiert` | Nur bei umgekehrter Aktorlogik aktivieren. |
|
||||||
|
| nicht vorhanden | `RueckmeldungVariableID` | Optional eine echte Boolean-Rueckmeldung zuordnen. |
|
||||||
|
| `Mindesttlaufzeit` | `Mindestlaufzeit` | Alter Wert war in Minuten gedacht; fuer gleiches Verhalten mit 60 multiplizieren und als Sekunden eintragen. |
|
||||||
|
| `Zeit_Zwischen_Zustandswechseln` | `Umschaltabstand` | Alte Minuten nicht uebernehmen. Neuen Sekundenwert bewusst festlegen; Standard ist 5 s. |
|
||||||
|
| `Interval` | entfaellt | Keine zyklische Regelberechnung mehr. |
|
||||||
|
| `IdleCounterMax` | entfaellt | Keine zyklusbasierte Idle-Erkennung mehr. |
|
||||||
|
|
||||||
|
## Variablen und Verhalten
|
||||||
|
|
||||||
|
| Enelix 1 | Enelix 2 |
|
||||||
|
| --- | --- |
|
||||||
|
| `DailyOnTime` | `Tageslaufzeit` in echten Sekunden |
|
||||||
|
| `IstNacht` | entfaellt; Mindestlaufzeit wird am spaetestmoeglichen Start des lokalen Kalendertags erzwungen |
|
||||||
|
| `IsTimerActive` | entfaellt; interne einmalige Timer |
|
||||||
|
| `Aktuelle_Leistung` | gemeinsames `Istleistung` |
|
||||||
|
| `Power` | gemeinsames `Sollleistung` |
|
||||||
|
| `PowerSteps` | Vertragsfeld `Leistungswerte_W` |
|
||||||
|
| `PV_Prio` | Property `PrioritaetPV` |
|
||||||
|
| `Sperre_Prio` | Property `PrioritaetPeak` |
|
||||||
|
| `Is_Peak_Shaving` | entfaellt; Betriebsart liegt im Manager |
|
||||||
|
| `Leistung_Delta` | entfaellt; Leistungsverteilung liegt im Manager |
|
||||||
|
| `Idle`, `IdleCounter` | entfallen |
|
||||||
|
|
||||||
|
Die feste Enelix-1-Nachtzeit von 22:00 bis 07:00 wird nicht uebernommen.
|
||||||
|
Enelix 2 plant nur den Zeitpunkt, ab dem die verbleibende Mindestlaufzeit vor
|
||||||
|
dem lokalen Tagesende zwingend eingeschaltet werden muss.
|
||||||
|
|
||||||
|
## Inbetriebnahme
|
||||||
|
|
||||||
|
1. Bestehende Instanzkonfiguration und aktuelle Aktorlogik dokumentieren.
|
||||||
|
2. Neue Instanz **Verbraucher 1-Stufig** anlegen.
|
||||||
|
3. Leistung, Aktor, Invertierung und optional Rueckmeldung konfigurieren.
|
||||||
|
4. Mindestlaufzeit von Minuten in Sekunden umrechnen.
|
||||||
|
5. `Umschaltabstand` neu in Sekunden festlegen; nicht den alten Minutenwert
|
||||||
|
kopieren.
|
||||||
|
6. Neue Instanz im Enelix-2-Manager aktiv zuordnen.
|
||||||
|
7. Bei ausgeschalteter lokaler EMS-Freigabe Ein/Aus und Rueckmeldung
|
||||||
|
beaufsichtigt pruefen.
|
||||||
|
8. Alte Instanz erst deaktivieren, danach `Aktiv` an der neuen Instanz
|
||||||
|
einschalten.
|
||||||
|
9. Mindestens einen Timeout, einen verhinderten Lastwechsel und einen
|
||||||
|
Tageswechsel kontrollieren.
|
||||||
|
|
||||||
|
## Rueckkehr
|
||||||
|
|
||||||
|
Solange die alte Instanz nicht geloescht wurde, kann zurueckgekehrt werden,
|
||||||
|
indem die neue Instanz im Manager deaktiviert und ausgeschaltet wird. Danach
|
||||||
|
darf die alte Instanz wieder aktiviert werden. Beide Module duerfen nie
|
||||||
|
gleichzeitig denselben Aktor steuern.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Batterie
|
||||||
|
|
||||||
|
> Status: Diskussionsentwurf. Positiv bedeutet Laden, negativ Entladen.
|
||||||
|
|
||||||
|
Die Auswahl des Batterietyps bestimmt den Adapter und die sichtbaren
|
||||||
|
Anschlussfelder. Ohne Logging bleibt von den zusätzlichen Variablen nur
|
||||||
|
`Ladestatus` sichtbar.
|
||||||
|
|
||||||
|
## Zusätzliche Variablen
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Ladestatus` | Integer / Anzeige | `0` unbekannt, `1` ruhend, `2` laden, `3` entladen. |
|
||||||
|
| `Ladezustand` | Float / Logging | Physischer Ladezustand in %. |
|
||||||
|
| `LadezustandGueltig` | Boolean / Logging | Gültigkeit unabhängig von der Leistungsmessung. |
|
||||||
|
| `Steuerungsmodus` | Integer / lokal bedienbar | `0` Batterie, `1` Manager, `2` Virtuelle Batterie führt. |
|
||||||
|
| `NotnachladungAktiv` | Boolean / Logging | Aus Ladezustand ermittelter lokaler Bedarf. |
|
||||||
|
| `LadeleistungVerfuegbar` | Float / Logging | Aktuell nutzbare Ladeleistung in W. |
|
||||||
|
| `EntladeleistungVerfuegbar` | Float / Logging | Aktuell nutzbare Entladeleistung als positiver Betrag in W. |
|
||||||
|
| `MaxLadeleistung` | Float / lokal bedienbar | Wirksamer Parameter in W. |
|
||||||
|
| `MaxEntladeleistung` | Float / lokal bedienbar | Wirksamer Parameter in W. |
|
||||||
|
| `Mindestladezustand` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
||||||
|
| `Maximalladezustand` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
||||||
|
| `Nachladeschwelle` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
||||||
|
| `Nachladehysterese` | Float / lokal bedienbar | Wirksamer Parameter in %. |
|
||||||
|
| `Notnachladeleistung` | Float / lokal bedienbar | `0` deaktiviert; sonst Leistung in W. |
|
||||||
|
|
||||||
|
## Zusätzliche Properties
|
||||||
|
|
||||||
|
| Ident | Typ | Standard / Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `MaxLadeleistung` | Float | `0*` W; einzige einstellbare obere Ladegrenze. |
|
||||||
|
| `MaxEntladeleistung` | Float | `0` W; einzige einstellbare obere Entladegrenze. |
|
||||||
|
| `Mindestladezustand` | Float | `0*` %; Bereich 0–100. |
|
||||||
|
| `Maximalladezustand` | Float | `100` %; grösser als Mindestladezustand. |
|
||||||
|
| `Nachladeschwelle` | Float | `0*` %; zwischen Mindest- und Maximalladezustand. |
|
||||||
|
| `Nachladehysterese` | Float | `2` %; Schwelle plus Hysterese höchstens Maximalladezustand. |
|
||||||
|
| `Notnachladeleistung` | Float | `0` W; sonst höchstens MaxLadeleistung. |
|
||||||
|
| `Batterietyp` | Auswahl | `Unkonfiguriert`, `Herstellerunabhaengig`, `Goodwe`, `SolarEdge`, `SigEnergy`. |
|
||||||
|
| `BatterieInstanzID` | Integer | `0`; für kompatible Herstelleradapter. |
|
||||||
|
| `LadezustandVariableID` | Integer | `0`; nur herstellerunabhängig. |
|
||||||
|
| `IstleistungVariableID` | Integer | `0`; nur herstellerunabhängig. |
|
||||||
|
| `SollleistungVariableID` | Integer | `0`; nur herstellerunabhängig, vorzeichenbehaftet in W. |
|
||||||
|
| `LadezustandMaxAlter` | Integer | `60` s; Aktualität des Ladezustands. |
|
||||||
|
| `VirtuelleBatterieID` | Integer | `0`; Pflicht bei Steuerungsmodus 2. |
|
||||||
|
|
||||||
|
Anschlussart, Batteriespannung, Gerätezuordnung, Leistungsschritt,
|
||||||
|
Sollwertquelle und externe Sollwerte entfallen.
|
||||||
|
|
||||||
|
## Zustand
|
||||||
|
|
||||||
|
`Ladezustand_Prozent` als Istwert; `Steuerungsmodus` und
|
||||||
|
`NotnachladungAktiv` als Status; `LadezustandFehlt` und `Geraetefehler` als
|
||||||
|
Störungen.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Michis konkrete Fehlerliste nachliefern und gegen den Entwurf prüfen.
|
||||||
|
- Datenpunkte und Leistungsgrenzen der drei Herstelleradapter verifizieren.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Easee Gateway
|
||||||
|
|
||||||
|
> Status: Diskussionsentwurf. Kommunikationsmodul, kein Verbraucher und keine
|
||||||
|
> Verwendung der Verbraucherbasis.
|
||||||
|
|
||||||
|
Das bestehende Gateway wird übernommen. Anzeigen werden deutsch; technische
|
||||||
|
Alt-Idents bleiben zur Kompatibilität erhalten. Eine passende vorhandene
|
||||||
|
Verbindung soll bei der Instanziierung wiederverwendet werden.
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
|
| Technischer Ident / Anzeige | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Connected` / Verbunden | Boolean / Anzeige | SignalR-Verbindungsstatus. |
|
||||||
|
| `SubscriptionCount` / Angemeldete Ladestationen | Integer / Anzeige | Anzahl registrierter Geräte. |
|
||||||
|
| `LastError` / Letzter Fehler | String / Anzeige | Diagnose ohne Zugangsdaten. |
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Technischer Ident / Anzeige | Typ | Standard / Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Active` / Aktiv | Boolean | `true`; Verbindungsbetrieb, keine Ladefreigabe. |
|
||||||
|
| `Username` / Benutzername | String | leer; Easee-Konto. |
|
||||||
|
| `Password` / Passwort | String | leer; vertraulich. |
|
||||||
|
| `VerifyCertificate` / Zertifikat prüfen | Boolean | `true`; TLS-Prüfung. |
|
||||||
|
|
||||||
|
## Verhalten
|
||||||
|
|
||||||
|
Nach einer Wiederverbindung werden Gerätezustände erst nach neuer gültiger
|
||||||
|
Rückmeldung verwendet. Das Gateway sendet keine EMS-Verbrauchermeldung.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Instanziierung und Wiederverwendung bestehender Verbindungen testen.
|
||||||
|
- Aktuelle Easee-Endpunkte und Ereignisfelder vor Übernahme verifizieren.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Ladestation Gateway
|
||||||
|
|
||||||
|
> Status: Diskussionsentwurf. Eigenständiges Verbrauchermodul mit zugeordnetem
|
||||||
|
> Easee Gateway; keine gemeinsame Ladestations-Basisklasse.
|
||||||
|
|
||||||
|
## Zusätzliche Variablen
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `FahrzeugVerbunden` | Boolean / Anzeige | Nur bei gültigem Gatewaystatus aussagekräftig. |
|
||||||
|
| `Fahrzeugstatus` | Integer / Anzeige | `0` unbekannt, `1` nicht verbunden, `2` bereit, `3` lädt, `4` voll, `5` Fehler. |
|
||||||
|
| `Ladestrom` | Float / Anzeige | Aktueller Ladestrom in A. |
|
||||||
|
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig, `3` dreiphasig. |
|
||||||
|
| `Ladefreigabe` | Boolean / lokal bedienbar | Lokale Ladeerlaubnis zusätzlich zu `Aktiv`. |
|
||||||
|
| `Solarladen` | Boolean / lokal bedienbar | Überschussorientiertes Laden ein/aus. |
|
||||||
|
|
||||||
|
## Zusätzliche Properties
|
||||||
|
|
||||||
|
| Ident | Typ | Standard / Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Ladefreigabe` | Boolean | `false`. |
|
||||||
|
| `Solarladen` | Boolean | `true`. |
|
||||||
|
| `MaximalerLadestrom` | Float | `0*` A; Installations- und Gerätegrenze. |
|
||||||
|
| `Ladestromschritt` | Float | `1` A; muss von Easee unterstützt sein. |
|
||||||
|
| `MindestEinzeit` | Integer | `0` s. |
|
||||||
|
| `MindestAuszeit` | Integer | `0` s. |
|
||||||
|
| `GatewayID` | Integer | `0`; kompatibles Easee Gateway, erforderlich. |
|
||||||
|
| `Ladestationskennung` | String | leer; eindeutige Station/Seriennummer im Gateway. |
|
||||||
|
|
||||||
|
Es gibt hier keine Zugangsdaten; sie liegen ausschliesslich im Easee Gateway.
|
||||||
|
Ladefreigabe und Solarladen werden über `EinstellungenInVisu` eingeblendet.
|
||||||
|
|
||||||
|
## Zustand
|
||||||
|
|
||||||
|
`Fahrzeugstatus`, `FahrzeugVerbunden`, `Ladefreigabe`, `Solarladen`,
|
||||||
|
`Ladestrom_A`, `Ladefehler` und `Gatewayfehler`.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Reaktion aller zugeordneten Stationen bei Gatewayausfall im Praxistest festlegen.
|
||||||
|
- Lokalen Mindestladebedarf bei deaktiviertem Solarladen genau festlegen.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Ladestation Stand-Alone
|
||||||
|
|
||||||
|
> Status: Diskussionsentwurf. Eigenständiges Verbrauchermodul mit direkter
|
||||||
|
> Geräteanbindung; keine Beziehung zur Ladestation Gateway.
|
||||||
|
|
||||||
|
## Zusätzliche Variablen
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `FahrzeugVerbunden` | Boolean / Anzeige | Nur bei gültigem Gerätestatus aussagekräftig. |
|
||||||
|
| `Fahrzeugstatus` | Integer / Anzeige | `0` unbekannt, `1` nicht verbunden, `2` bereit, `3` lädt, `4` voll, `5` Fehler. |
|
||||||
|
| `Ladestrom` | Float / Anzeige | Aktueller Ladestrom in A. |
|
||||||
|
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig, `3` dreiphasig. |
|
||||||
|
| `Ladefreigabe` | Boolean / lokal bedienbar | Lokale Ladeerlaubnis zusätzlich zu `Aktiv`. |
|
||||||
|
| `Solarladen` | Boolean / lokal bedienbar | Überschussorientiertes Laden ein/aus. |
|
||||||
|
|
||||||
|
## Zusätzliche Properties
|
||||||
|
|
||||||
|
| Ident | Typ | Standard / Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Ladefreigabe` | Boolean | `false`. |
|
||||||
|
| `Solarladen` | Boolean | `true`. |
|
||||||
|
| `MaximalerLadestrom` | Float | `0*` A; Installations- und Gerätegrenze. |
|
||||||
|
| `Ladestromschritt` | Float | `1` A; muss vom Gerät unterstützt sein. |
|
||||||
|
| `MindestEinzeit` | Integer | `0` s. |
|
||||||
|
| `MindestAuszeit` | Integer | `0` s. |
|
||||||
|
| `Geraetetyp` | Auswahl | `Unkonfiguriert`, `GoEAlt`, `GoEGemini` oder `SmartMePico`. |
|
||||||
|
| `Geraeteadresse` | String | leer; nur für lokale Adapter. |
|
||||||
|
| `GeraeteID` | String | leer; nur falls der Adapter eine ID benötigt. |
|
||||||
|
| `Seriennummer` | String | leer; nur falls benötigt. |
|
||||||
|
| `Benutzername` | String | leer; nur falls Anmeldung benötigt. |
|
||||||
|
| `Passwort` | String | leer; vertraulich und nie als Variable/Diagnose anzeigen. |
|
||||||
|
|
||||||
|
Minimaler Ladestrom, frühere Umschaltabstände, Benutzervariable und externe
|
||||||
|
Sollwert-Variable entfallen. Ladefreigabe und Solarladen erscheinen nur bei
|
||||||
|
aktivem `EinstellungenInVisu` als bedienbare Variablen.
|
||||||
|
|
||||||
|
## Zustand
|
||||||
|
|
||||||
|
`Fahrzeugstatus`, `FahrzeugVerbunden`, `Ladefreigabe`, `Solarladen` und
|
||||||
|
`Ladestrom_A` sowie `Ladefehler` und gerätespezifische Fehler.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Direkte Adapter und ihre noch gültigen API-Routen bei der Implementierung prüfen.
|
||||||
|
- Lokalen Mindestladebedarf bei deaktiviertem Solarladen genau festlegen.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Manager
|
||||||
|
|
||||||
|
> Status: Implementiert. Führt Hauptmanager und Peakshaving zusammen.
|
||||||
|
|
||||||
|
Der Manager liest die Netzleistung, verwaltet ausschliesslich die ausgewählten
|
||||||
|
Verbraucher und verteilt Leistung im PV- oder Peak-Betrieb. Die Prioritäten
|
||||||
|
kommen aus den Verbrauchermeldungen.
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
|
Ohne angelegte Diagnosevariablen existieren nur `Aktiv`, `Betriebsart` und `Netzleistung`.
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Aktiv` | Boolean / bedienbar | Regelung ein/aus; Start `false`. |
|
||||||
|
| `Betriebsart` | String / Anzeige | `Inaktiv`, `PV` oder `Peak`. |
|
||||||
|
| `Netzleistung` | Float / Anzeige | Aktuelle Netzleistung in W; positiv Bezug, negativ Einspeisung. |
|
||||||
|
| `NetzleistungGueltig` | Boolean / Logging | Messquelle vorhanden und aktuell. |
|
||||||
|
| `WirksameLastspitzengrenze` | Float / Logging | Aktuelle feste oder monatliche Grenze in W. |
|
||||||
|
| `Verteilbudget` | Float / Logging | Aktuell verfügbares Budget in W. |
|
||||||
|
| `VerbraucherAnzahl` | Integer / Logging | Anzahl zugeordneter Verbraucher. |
|
||||||
|
| `VerbraucherVerfuegbar` | Integer / Logging | Anzahl aktuell verfügbarer Verbraucher. |
|
||||||
|
| `Verbraucherstatus` | String/JSON / Logging | Letzte vollständige Meldungen. |
|
||||||
|
| `Prognosestatus` | String / Logging | `NichtVerwendet`, `Verbunden` oder `Fehler`. |
|
||||||
|
| `SDLStatus` | String / Logging | Status des SDL/VGT-Anschlusses. |
|
||||||
|
| `Lizenzstatus` | String / Logging | Status der Lizenzprüfung. |
|
||||||
|
| `Stoerueberwachungsstatus` | String / Logging | Status der externen Störüberwachung. |
|
||||||
|
| `Sammelstoerung` | Boolean / Logging | Eigene und weitergeleitete Störungen. |
|
||||||
|
| `Stoertext` | String / Logging | Lesbare Sammelmeldung. |
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Ident | Typ | Standard / Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `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 | `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 | Editierbare Liste mit zwölf Monatswerten in W. |
|
||||||
|
| `SollwertSolarladen` | Float | `0` W; gewünschte Netzleistung im Solarladebetrieb. |
|
||||||
|
| `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. |
|
||||||
|
| `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. |
|
||||||
|
| `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. |
|
||||||
|
| `LoggingEin` | Boolean | `false`; laufende Meldungen im Debug-Fenster ausgeben. |
|
||||||
|
|
||||||
|
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 automatische Verbrauchersuche kann dort erneut
|
||||||
|
ausgeführt werden, ohne andere ungespeicherte Formulareingaben zu verlieren.
|
||||||
|
Die vier optionalen oberen Anschlüsse besitzen jeweils einen eigenen Bereich;
|
||||||
|
die Lizenzierung steht an erster Stelle des Formulars.
|
||||||
|
|
||||||
|
## Verteilalgorithmus
|
||||||
|
|
||||||
|
Der Manager bildet das Verteilbudget aus der aktuellen Netzleistung, dem Ziel
|
||||||
|
der aktiven Betriebsart und der aktuellen Leistung aller frisch gemeldeten
|
||||||
|
Verbraucher. Eine gültige Istleistung wird bevorzugt; fehlt sie, bleibt der im
|
||||||
|
Zustand bestätigte Sollwert konservativ reserviert.
|
||||||
|
|
||||||
|
Nicht verfügbare, nicht änderbare oder aktuell angebotlose Verbraucher behalten
|
||||||
|
ihre Leistung und erhalten keine neue Vorgabe. Die übrigen Verbraucher werden
|
||||||
|
nach der gemeldeten PV- beziehungsweise Peak-Priorität und danach stabil nach
|
||||||
|
Instanz-ID sortiert. Von ihrem jeweils kleinsten erlaubten Leistungswert aus
|
||||||
|
wird das Budget in dieser Reihenfolge aufgefüllt. Einzelwerte und ganzzahlige
|
||||||
|
Bereiche werden direkt verarbeitet; Lücken werden nie durch unzulässige Werte
|
||||||
|
geschlossen. Eine verbleibende Abweichung wird im `Verbraucherstatus`
|
||||||
|
dokumentiert.
|
||||||
|
|
||||||
|
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 Verbrauchermeldungen lösen die Berechnung aus.
|
||||||
|
- Identische Sollwerte werden nur beim Keep-alive erneut gesendet.
|
||||||
|
- Diagnosevariablen und laufendes Debug-Logging werden getrennt aktiviert.
|
||||||
|
- Verbraucherpakete werden zentral geprüft und nur von aktiv zugeordneten
|
||||||
|
Absendern angenommen.
|
||||||
|
- Veraltete oder noch fehlende Verbrauchermeldungen werden nicht verteilt und
|
||||||
|
als Sammelstörung ausgewiesen.
|
||||||
|
- Bei fehlender oder veralteter Netzleistung bleibt die Regelung `Inaktiv` und
|
||||||
|
sendet keine neuen Vorgaben.
|
||||||
|
- Die oberen Anschlüsse bleiben optional. Solange kein konkreter Adapter
|
||||||
|
implementiert ist, meldet ein aktivierter Anschluss den Status `Fehler`, ohne
|
||||||
|
die lokale EMS-Regelung zu blockieren.
|
||||||
|
|
||||||
|
## Schnittstellen
|
||||||
|
|
||||||
|
- Empfängt genau `VerbraucherdatenEmpfangen(array $daten)`.
|
||||||
|
- Sendet an jeden Verbraucher nur Kopf und `Sollleistung_W`.
|
||||||
|
- Obere Anschlüsse: siehe [Obere Anschlüsse](../../Obere-Anschluesse.md).
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Monatliche Batteriereserve aus dem alten Peakshaving-Modul übernehmen?
|
||||||
|
- Anbieterformate für Prognose, Lizenzierung und Störüberwachung festlegen.
|
||||||
|
- Verhalten und Messabgrenzung bei Untermanagern im Anlagentest bestätigen.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Pufferspeicher
|
||||||
|
|
||||||
|
> Status: Diskussionsentwurf.
|
||||||
|
|
||||||
|
Regelt elektrische Heizstufen nach Pufferbedarf. Die Heizkurve verwendet einen
|
||||||
|
Fusspunkt bei 20 °C Aussentemperatur und eine Steigung:
|
||||||
|
|
||||||
|
`Solltemperatur = FusspunktVorlauftemperatur + HeizkurvenSteigung × (20 - Aussentemperatur)`
|
||||||
|
|
||||||
|
Anschliessend wird auf Mindest- und Maximaltemperatur begrenzt.
|
||||||
|
|
||||||
|
## Zusätzliche Variablen
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Puffertemperatur` | Float / Anzeige | Aktuelle Temperatur in °C. |
|
||||||
|
| `Aussentemperatur` | Float / Anzeige | Heizkurveneingang in °C. |
|
||||||
|
| `TemperaturenGueltig` | Boolean / Anzeige | Beide Quellen sind gültig. |
|
||||||
|
| `Solltemperatur` | Float / Anzeige | Berechneter und begrenzter Sollwert in °C. |
|
||||||
|
| `Heizbedarf` | Boolean / Anzeige | Bedarf unter Berücksichtigung der Hysterese. |
|
||||||
|
| `AktiveStufe` | Integer / Anzeige | Stufenkennung; `0` bedeutet aus. |
|
||||||
|
| `FusspunktVorlauftemperatur` | Float / lokal bedienbar | Wirksamer Parameter in °C. |
|
||||||
|
| `HeizkurvenSteigung` | Float / lokal bedienbar | Anhebung pro K fallender Aussentemperatur. |
|
||||||
|
| `Mindesttemperatur` | Float / lokal bedienbar | Untere Begrenzung in °C. |
|
||||||
|
| `Maximaltemperatur` | Float / lokal bedienbar | Obere Begrenzung in °C. |
|
||||||
|
| `Hysterese` | Float / lokal bedienbar | Ein unter Soll minus Hysterese, aus bei Soll. |
|
||||||
|
|
||||||
|
## Zusätzliche Properties
|
||||||
|
|
||||||
|
| Ident | Typ | Standard / Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `FusspunktVorlauftemperatur` | Float | `0*` °C; Vorlauf bei 20 °C aussen. |
|
||||||
|
| `HeizkurvenSteigung` | Float | `0*`; mindestens 0. |
|
||||||
|
| `Mindesttemperatur` | Float | `0*` °C. |
|
||||||
|
| `Maximaltemperatur` | Float | `0*` °C. |
|
||||||
|
| `Hysterese` | Float | `1` K; grösser 0. |
|
||||||
|
| `Leistungsstufen` | String/JSON | `[]`; Stufenkennung, Leistung in W und Ausgangsbelegung. |
|
||||||
|
| `PuffertemperaturVariableID` | Integer | `0`; Temperaturquelle. |
|
||||||
|
| `AussentemperaturVariableID` | Integer | `0`; Aussentemperaturquelle. |
|
||||||
|
| `Mindestlaufzeit` | Integer | `0` s. |
|
||||||
|
| `Mindeststillstand` | Integer | `0` s. |
|
||||||
|
|
||||||
|
Vier bisherige Heizkurventemperaturen, beide Heizkurvenquellen, Übernahmeaktion,
|
||||||
|
Zeitplan und Zeitzone entfallen.
|
||||||
|
|
||||||
|
## Zustand
|
||||||
|
|
||||||
|
`Puffertemperatur_C` und `Aussentemperatur_C` als Istwerte,
|
||||||
|
`Solltemperatur_C` als Sollwert, `Heizbedarf` als Status und `Fuehlerfehler` als
|
||||||
|
Störung.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Optionale Einbindung der Wärmepumpe beim Pufferheizen ausarbeiten.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# EMS-Module und Modulentwürfe
|
||||||
|
|
||||||
|
> Manager, Warmwassererwaermer und Verbraucher 1-Stufig sind als installierbare
|
||||||
|
> IP-Symcon-Module umgesetzt. Die weiteren Ordner enthalten Besprechungsgrundlagen.
|
||||||
|
|
||||||
|
Alle steuerbaren Verbraucher verwenden die gemeinsamen Datenpunkte aus der
|
||||||
|
[EMS-Schnittstelle](../Schnittstelle.md). In den Modul-READMEs stehen deshalb
|
||||||
|
nur zusätzliche Properties, Variablen, Zustände und offene Punkte.
|
||||||
|
|
||||||
|
| Modul | Rolle |
|
||||||
|
| --- | --- |
|
||||||
|
| [Manager](Manager/README.md) | Leistungsverteilung und Anschlüsse (implementiert) |
|
||||||
|
| [Batterie](Batterie/README.md) | Laden und Entladen eines Speichers |
|
||||||
|
| [Wassererwärmer](Wassererwaermer/README.md) | Mehrstufige elektrische Erwärmung (implementiert) |
|
||||||
|
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
||||||
|
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher (implementiert) |
|
||||||
|
| [Wärmepumpe](Waermepumpe/README.md) | Sperrkontakt oder SG Ready |
|
||||||
|
| [Ladestation Stand-Alone](Ladestation-Stand-Alone/README.md) | Direkte Geräteanbindung |
|
||||||
|
| [Ladestation Gateway](Ladestation-Gateway/README.md) | Ladestation am Easee Gateway |
|
||||||
|
| [Easee Gateway](Easee-Gateway/README.md) | Gemeinsame Easee-Kommunikation |
|
||||||
|
|
||||||
|
## Review-Regel
|
||||||
|
|
||||||
|
`0*` bezeichnet einen noch nicht eingerichteten Anlagenwert. Offene Punkte
|
||||||
|
werden nicht durch Annahmen ersetzt. Erst nach Freigabe wird aus einem Entwurf
|
||||||
|
ein Ordner mit `module.json`, `form.json`, `module.php` und Unit-Tests.
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Verbraucher 1-Stufig
|
||||||
|
|
||||||
|
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `3.0`.
|
||||||
|
|
||||||
|
Das Modul schaltet einen elektrischen Ein-/Aus-Verbraucher. Im freien Zustand
|
||||||
|
meldet es dem Manager das Leistungsangebot `[0, Nennleistung]`. Waerend der
|
||||||
|
Umschaltsperre und bei lokal erzwungener Mindestlaufzeit wird nur der aktuell
|
||||||
|
zulaessige Leistungswert angeboten.
|
||||||
|
|
||||||
|
## Architekturentscheidung
|
||||||
|
|
||||||
|
Die Regelung ist ereignisbasiert. Neuberechnungen werden ausgeloest durch:
|
||||||
|
|
||||||
|
- eine neue Manager-Vorgabe,
|
||||||
|
- eine Aenderung an Schaltkontakt oder optionaler Rueckmeldung,
|
||||||
|
- das Ein- oder Ausschalten der lokalen EMS-Freigabe,
|
||||||
|
- das Ende des Umschaltabstands,
|
||||||
|
- das Ablaufen einer Manager-Vorgabe,
|
||||||
|
- den naechsten relevanten Zeitpunkt der Tageslaufzeitplanung.
|
||||||
|
|
||||||
|
Es gibt keinen Regelzyklus, kein `Interval` und keinen `IdleCounterMax`.
|
||||||
|
`Meldeintervall` bleibt als vertraglich geforderte periodische Vollmeldung
|
||||||
|
bestehen und steuert keine Regelberechnung.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Zu den sechs gemeinsamen Verbraucher-Properties aus
|
||||||
|
[`Schnittstelle.md`](../../Schnittstelle.md) kommen sieben
|
||||||
|
Modul-Properties hinzu.
|
||||||
|
|
||||||
|
| Ident | Typ | Standard | Beschreibung |
|
||||||
|
| --- | --- | ---: | --- |
|
||||||
|
| `Nennleistung` | Integer | `0` | Positive elektrische Leistung in W. |
|
||||||
|
| `SchaltkontaktVariableID` | Integer | `0` | Boolean-Aktor mit Standard- oder benutzerdefinierter Aktion. |
|
||||||
|
| `SchaltkontaktInvertiert` | Boolean | `false` | Kehrt die Ein-/Aus-Semantik des Aktors um. |
|
||||||
|
| `RueckmeldungVariableID` | Integer | `0` | Optionale Boolean-Rueckmeldung; `true` bedeutet eingeschaltet. |
|
||||||
|
| `Mindestlaufzeit` | Integer | `0` | Geforderte Laufzeit pro lokalem Kalendertag in Sekunden, maximal 86400. |
|
||||||
|
| `Umschaltabstand` | Integer | `5` | Sekunden bis zum naechsten erlaubten Lastwechsel. |
|
||||||
|
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt gemeinsame Diagnosevariablen und `Rueckmeldefehler` an. |
|
||||||
|
|
||||||
|
`EinstellungenInVisu` ist Teil der gemeinsamen Verbraucherbasis. Das Modul
|
||||||
|
besitzt derzeit keine zusaetzlichen bedienbaren Einstellvariablen und wertet
|
||||||
|
diese Property deshalb noch nicht weiter aus.
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
|
Immer sichtbar:
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Startwert `false`. |
|
||||||
|
| `Schaltzustand` | Boolean / Anzeige | Rueckgemeldeter oder aus dem Aktor berechneter Zustand. |
|
||||||
|
| `Tageslaufzeit` | Integer / Anzeige | Laufzeit des lokalen Kalendertags in Sekunden. |
|
||||||
|
|
||||||
|
Mit `DiagnosevariablenAnzeigen` werden diese Variablen zusaetzlich angelegt:
|
||||||
|
|
||||||
|
- `Istleistung`
|
||||||
|
- `Leistungsquelle`
|
||||||
|
- `Sollleistung`
|
||||||
|
- `SollwertGueltig`
|
||||||
|
- `Verfuegbar`
|
||||||
|
- `AenderungMoeglich`
|
||||||
|
- `Stoerung`
|
||||||
|
- `Stoertext`
|
||||||
|
- `Rueckmeldefehler`
|
||||||
|
|
||||||
|
Die Regellogik arbeitet mit persistenten Attributen und ist nicht von der
|
||||||
|
Sichtbarkeit der Diagnosevariablen abhaengig.
|
||||||
|
|
||||||
|
## Zeit- und Schaltverhalten
|
||||||
|
|
||||||
|
Nach jedem Lastwechsel wird `AenderungMoeglich=false` gemeldet und das
|
||||||
|
Leistungsangebot auf den aktuellen Wert eingeschraenkt. Nach
|
||||||
|
`Umschaltabstand` Sekunden gibt ein einmaliger Timer den naechsten
|
||||||
|
Lastwechsel frei. Der Standardwert betraegt 5 Sekunden.
|
||||||
|
|
||||||
|
Die Tageslaufzeit wird mit der in IP-Symcon eingestellten lokalen Zeitzone
|
||||||
|
gefuehrt. Sommer- und Winterzeit sowie der Wechsel um Mitternacht werden
|
||||||
|
beruecksichtigt. Ist die konfigurierte Mindestlaufzeit noch nicht erreicht,
|
||||||
|
wird der Verbraucher erst zum spaetestmoeglichen Zeitpunkt lokal
|
||||||
|
eingeschaltet, an dem die Restlaufzeit bis Mitternacht noch erfuellt werden
|
||||||
|
kann. In dieser Phase meldet er `AenderungMoeglich=false`.
|
||||||
|
|
||||||
|
Bei Aktivierung sehr kurz vor Tagesende kann eine unmoeglich hohe Restlaufzeit
|
||||||
|
naturgemaess nicht mehr vollstaendig nachgeholt werden. Der Verbraucher wird
|
||||||
|
dann sofort eingeschaltet und die tatsaechliche Laufzeit erfasst.
|
||||||
|
|
||||||
|
## Rueckmeldung und Fehler
|
||||||
|
|
||||||
|
Ohne separate Rueckmeldung wird der Schaltzustand aus der Aktorvariable
|
||||||
|
berechnet. Mit `RueckmeldungVariableID` wartet das Modul bis zum Ende des
|
||||||
|
Umschaltabstands auf den angeforderten Zustand. Liegt er dann nicht an, werden
|
||||||
|
`Rueckmeldefehler=true`, `Stoerung=true` und `Verfuegbar=false` gemeldet.
|
||||||
|
|
||||||
|
Die Istleistung ist immer berechnet: `0` oder `Nennleistung`. Deshalb wird
|
||||||
|
`Leistungsquelle=1` gemeldet, auch wenn der Boolean-Schaltzustand ueber eine
|
||||||
|
physische Rueckmeldung erfasst wird.
|
||||||
|
|
||||||
|
## Managerkommunikation
|
||||||
|
|
||||||
|
Das Modul implementiert
|
||||||
|
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
|
||||||
|
ausschliesslich den Vertrag `3.0`. Der Verbraucher besitzt keine
|
||||||
|
Manager-ID-Property. Er akzeptiert nur Manager, in deren manueller oder
|
||||||
|
automatischer Verbraucherzuordnung seine Instanz aktiv eingetragen ist.
|
||||||
|
|
||||||
|
Rueckmeldungen erfolgen bei relevanten Ereignissen, kurz verzoegert nach einer
|
||||||
|
Manager-Vorgabe und zusaetzlich alle `Meldeintervall` Sekunden. Eine Vorgabe
|
||||||
|
wird nach `VorgabeTimeout` Sekunden ohne Erneuerung ungueltig.
|
||||||
|
|
||||||
|
## Installation und Inbetriebnahme
|
||||||
|
|
||||||
|
1. Im IP-Symcon Module Control den Testing-Branch `develop` der Bibliothek
|
||||||
|
`https://git.belevo.ch/ENELIX/Enelix-EMS.git` installieren oder
|
||||||
|
aktualisieren.
|
||||||
|
2. Unter **Instanz hinzufuegen** nach **Verbraucher 1-Stufig** suchen und eine
|
||||||
|
Instanz anlegen.
|
||||||
|
3. `Nennleistung` in ganzen Watt eintragen.
|
||||||
|
4. Als `Schaltkontakt` eine Booleanvariable mit funktionsfaehiger Aktion
|
||||||
|
auswaehlen. Bei umgekehrter Aktorlogik `SchaltkontaktInvertiert`
|
||||||
|
aktivieren.
|
||||||
|
5. Optional eine separate Boolean-Rueckmeldung auswaehlen. Dort muss
|
||||||
|
`true` dem physisch eingeschalteten Verbraucher entsprechen.
|
||||||
|
6. Prioritaeten, `Mindestlaufzeit`, `Umschaltabstand`,
|
||||||
|
`Meldeintervall` und `VorgabeTimeout` einstellen und die Konfiguration
|
||||||
|
uebernehmen.
|
||||||
|
7. Die neue Instanz im Manager manuell aktiv zuordnen oder bei automatischer
|
||||||
|
Suche in der gefundenen Liste aktivieren.
|
||||||
|
8. Fuer die Erstpruefung `DiagnosevariablenAnzeigen` und bei Bedarf
|
||||||
|
`LoggingEin` einschalten.
|
||||||
|
9. Unter Aufsicht `Aktiv` einschalten und ueber den Manager je eine Ein- und
|
||||||
|
Aus-Vorgabe pruefen.
|
||||||
|
|
||||||
|
## Abnahmecheckliste
|
||||||
|
|
||||||
|
- `Aktiv=false` schaltet den Ausgang aus.
|
||||||
|
- `Aktiv=true` und eine Manager-Vorgabe `Nennleistung` schalten den Ausgang
|
||||||
|
ein.
|
||||||
|
- Direkt nach einem Wechsel ist `AenderungMoeglich=false`; nach 5 Sekunden
|
||||||
|
wird es mit Standardkonfiguration wieder `true`.
|
||||||
|
- Eine Gegenanforderung waehrend der Sperre wird vom Leistungsangebot
|
||||||
|
ausgeschlossen.
|
||||||
|
- Die optionale Rueckmeldung folgt dem Ausgang innerhalb des
|
||||||
|
Umschaltabstands; andernfalls erscheint eine Stoerung.
|
||||||
|
- `Tageslaufzeit` steigt nur im eingeschalteten Zustand und beginnt am
|
||||||
|
lokalen Tageswechsel wieder bei 0.
|
||||||
|
- Nach Ablauf von `VorgabeTimeout` wird ohne neue Manager-Vorgabe
|
||||||
|
ausgeschaltet, sobald der Umschaltabstand dies zulaesst.
|
||||||
|
- Nach Neustart oder erneuter Konfiguration wird der reale Schaltzustand
|
||||||
|
eingelesen; alte Manager-Vorgaben werden nicht ungeprueft fortgesetzt.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Die Zuordnung der Enelix-1-Felder, Einheiten und bewusst entfallenen
|
||||||
|
Kommunikationsvariablen steht in
|
||||||
|
[`docs/migration/Verbraucher-1-Stufig.md`](../../migration/Verbraucher-1-Stufig.md).
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Wärmepumpe
|
||||||
|
|
||||||
|
> Status: Diskussionsentwurf.
|
||||||
|
|
||||||
|
Die Kontaktwirkung ist als Sperrkontakte oder SG Ready konfigurierbar. Die
|
||||||
|
elektrische Leistung kann fehlen, berechnet oder gemessen werden.
|
||||||
|
|
||||||
|
## Zusätzliche Variablen
|
||||||
|
|
||||||
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Waermepumpenzustand` | Integer / Anzeige | `0` unbekannt, `1` aus, `2` normal, `3` erhöht, `4` gesperrt. |
|
||||||
|
| `SperreAktiv` | Boolean / Anzeige | Aktuelle Sperranforderung. |
|
||||||
|
| `ErhoehungAktiv` | Boolean / Anzeige | Aktuelle Erhöhungsanforderung. |
|
||||||
|
| `SGReadyZustand` | Integer / Anzeige | Bei SG Ready Zustand 0–3. |
|
||||||
|
| `Laufzeit` | Integer / Anzeige | Kumulierte bestätigte Laufzeit in Sekunden. |
|
||||||
|
|
||||||
|
## Zusätzliche Properties
|
||||||
|
|
||||||
|
| Ident | Typ | Standard / Beschreibung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Kontaktwirkung` | Auswahl | `Sperrkontakte`; alternativ `SG Ready`. |
|
||||||
|
| `Kontakt1VariableID` | Integer | `0`; Sperre oder SG-Ready-Eingang 1. |
|
||||||
|
| `Kontakt2VariableID` | Integer | `0`; Erhöhung oder SG-Ready-Eingang 2. |
|
||||||
|
| `Kontakt1Invertiert` | Boolean | `false`. |
|
||||||
|
| `Kontakt2Invertiert` | Boolean | `false`. |
|
||||||
|
| `Leistungsermittlung` | Auswahl | `Nicht vorhanden`, `Berechnet` oder `Gemessen`; entspricht Quelle 0/1/2. |
|
||||||
|
| `LeistungsmessungVariableID` | Integer | `0`; nur bei gemessener Leistung. |
|
||||||
|
| `Zustandskennlinie` | String/JSON | `[]`; bei berechneter Leistung je Zustand Planleistung und Kontaktbelegung. |
|
||||||
|
| `BetriebsrueckmeldungVariableID` | Integer | `0`; optionale echte Betriebsrückmeldung. |
|
||||||
|
| `Mindestlaufzeit` | Integer | `0*` s; anlagengerecht festlegen. |
|
||||||
|
| `Mindeststillstand` | Integer | `0*` s; anlagengerecht festlegen. |
|
||||||
|
| `MaxSperrzeit` | Integer | `0` s; `0` deaktiviert die EMS-Sperrfunktion. |
|
||||||
|
|
||||||
|
Zeit- und wetterabhängiges Sperren beziehungsweise Erhöhen aus dem Altmodul
|
||||||
|
entfällt.
|
||||||
|
|
||||||
|
## Zustand
|
||||||
|
|
||||||
|
`Waermepumpenzustand`, `SperreAktiv`, `ErhoehungAktiv` beziehungsweise
|
||||||
|
`SGReadyZustand` als Status, `Laufzeit_s` als Istwert und
|
||||||
|
`Kommunikationsfehler` als Störung.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Der Formularbutton „Wassererwärmer über Wärmepumpe betreiben“ bleibt ohne
|
||||||
|
Regelwirkung, bis Anschluss und Verriegelung gemeinsam definiert sind.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Verbraucher Warmwassererwaermer
|
||||||
|
|
||||||
|
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `3.0`.
|
||||||
|
|
||||||
|
Das Modul adaptiert ausschliesslich das fruehere Modul `Boiler_x_Stufig`.
|
||||||
|
Mehrere elektrische Leistungsstufen werden exklusiv geschaltet. Der Manager
|
||||||
|
waehlt eine der aktuell angebotenen Leistungen; Mindesttemperatur, Zeitplan und
|
||||||
|
Legionellenprogramm koennen die lokale Warmwassersicherheit priorisieren.
|
||||||
|
|
||||||
|
## Sicherheit und Regelung
|
||||||
|
|
||||||
|
- Die Schaltkontakte werden mit Break-before-make geschaltet: zuerst alle aus,
|
||||||
|
danach hoechstens eine Stufe ein.
|
||||||
|
- Fehlende, veraltete oder nicht numerische Temperaturwerte schalten die
|
||||||
|
EMS-Verfuegbarkeit aus und fuehren zu einer Stoerung.
|
||||||
|
- Manager-Vorgaben werden nur angenommen, wenn sie im zuletzt gemeldeten
|
||||||
|
`Leistungswerte_W`-Angebot enthalten sind.
|
||||||
|
- Nach `VorgabeTimeout` wird eine nicht erneuerte Manager-Vorgabe ungueltig.
|
||||||
|
- Mindesttemperatur, vorausberechneter Zeitplan und Legionellenprogramm koennen
|
||||||
|
die maximale Stufe lokal erzwingen. Diese Phase wird mit
|
||||||
|
`AenderungMoeglich=false` gemeldet.
|
||||||
|
- Lastwechsel werden fuer `LastwechselSperrzeit` Sekunden gesperrt. Danach
|
||||||
|
erfolgt genau eine ereignisgesteuerte Neuberechnung; sicherheitsbedingtes
|
||||||
|
Ausschalten bleibt sofort moeglich.
|
||||||
|
- `BoilertemperaturGlaetten` aktiviert eine PT1-Glaettung mit konfigurierbarer
|
||||||
|
Zeitkonstante und der tatsaechlich verstrichenen Zeit.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Zu den sechs gemeinsamen Verbraucher-Properties aus
|
||||||
|
[`Schnittstelle.md`](../../Schnittstelle.md) kommen 13 Modul-Properties hinzu.
|
||||||
|
|
||||||
|
| Ident | Typ | Standard | Beschreibung |
|
||||||
|
| --- | --- | ---: | --- |
|
||||||
|
| `LeistungsStufen` | JSON-Liste | `[]` | Stufe, positive Leistung in W und Boolean-Schaltkontakt. |
|
||||||
|
| `ZeitKonstante` | Integer | `120` | PT1-Zeitkonstante in Sekunden. |
|
||||||
|
| `Boilerfuehler_PT1` | Integer | `0` | Temperaturvariable. |
|
||||||
|
| `BoilertemperaturGlaetten` | Boolean | `false` | PT1-Glaettung aktiv. |
|
||||||
|
| `Boilervolumen` | Integer | `300` | Volumen in Litern fuer die Zeitplanprognose. |
|
||||||
|
| `Zeitplan` | JSON-Liste | `[]` | Uhrzeit und Solltemperatur. |
|
||||||
|
| `LastwechselSperrzeit` | Integer | `5` | Mindestzeit zwischen zwei Lastwechseln in Sekunden. |
|
||||||
|
| `TemperaturMaxAlter` | Integer | `30` | Maximal zulaessiges Messwertalter in Sekunden. |
|
||||||
|
| `Hysterese` | Float | `5.0` | Temperaturhysterese in K. |
|
||||||
|
| `LegionellenfunktionAktiv` | Boolean | `true` | Zweistufige periodische Aufheizung aktiv. |
|
||||||
|
| `LegionellenMinimalintervallTage` | Integer | `4` | Ab diesem Intervall wird die Legionellentemperatur als Maximaltemperatur angeboten. |
|
||||||
|
| `LegionellenMaximalintervallTage` | Integer | `7` | Ab diesem Intervall wird die Legionellentemperatur zwingend nachgeladen. |
|
||||||
|
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt 16 Diagnosevariablen bei Bedarf an. |
|
||||||
|
|
||||||
|
`LoggingEin` aktiviert laufende Debug-Ausgaben.
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
|
Standardmaessig sichtbar sind nur `Aktiv` und `Boilertemperatur`.
|
||||||
|
|
||||||
|
Mit `EinstellungenInVisu` werden `Mindesttemperatur` und `Maximaltemperatur`
|
||||||
|
sichtbar und geprueft bedienbar. `Legionellentemperatur` wird nur angelegt,
|
||||||
|
wenn `LegionellenfunktionAktiv` eingeschaltet ist, und ist ebenfalls nur mit
|
||||||
|
`EinstellungenInVisu` sichtbar. Beim Abschalten der Legionellenfunktion wird
|
||||||
|
die Variable geloescht; ihr letzter Wert bleibt intern fuer ein spaeteres
|
||||||
|
Wiedereinschalten erhalten. Die Reihenfolge
|
||||||
|
`Mindesttemperatur < Maximaltemperatur <= Legionellentemperatur` ist zwingend.
|
||||||
|
|
||||||
|
Mit `DiagnosevariablenAnzeigen` werden diese 14 Variablen angelegt:
|
||||||
|
|
||||||
|
- `Istleistung`
|
||||||
|
- `Leistungsquelle`
|
||||||
|
- `Sollleistung`
|
||||||
|
- `SollwertGueltig`
|
||||||
|
- `Verfuegbar`
|
||||||
|
- `AenderungMoeglich`
|
||||||
|
- `Stoerung`
|
||||||
|
- `Stoertext`
|
||||||
|
- `AktiveStufe`
|
||||||
|
- `BezogeneEnergie`
|
||||||
|
- `TemperaturGueltig`
|
||||||
|
- `NachladenAktiv`
|
||||||
|
- `LegionellenbetriebAktiv`
|
||||||
|
- `LegioCounter`
|
||||||
|
|
||||||
|
Die Regelung und Managerkommunikation verwenden fuer diese Werte eine interne
|
||||||
|
Zustandsablage und funktionieren auch ohne angelegte Diagnosevariablen weiter.
|
||||||
|
Die Lastwechselsperre wird ausschliesslich ueber die Schnittstellenfelder
|
||||||
|
`AenderungMoeglich` und `Leistungswerte_W` abgebildet.
|
||||||
|
|
||||||
|
## Managerkommunikation
|
||||||
|
|
||||||
|
Das Modul implementiert `VerbraucherSchnittstelle::ManagerdatenEmpfangen()`.
|
||||||
|
Der Transport erfolgt wie beim Manager ueber `IPS_RequestAction` mit JSON. Der
|
||||||
|
Verbraucher besitzt keine Manager-ID-Property. Er ermittelt nur Manager, in
|
||||||
|
deren manueller oder automatischer Verbraucherzuordnung seine Instanz aktiv
|
||||||
|
eingetragen ist.
|
||||||
|
|
||||||
|
Die Rueckmeldung erfolgt ereignisbasiert und zusaetzlich alle
|
||||||
|
`Meldeintervall` Sekunden. Eine Rueckmeldung nach empfangener Vorgabe wird kurz
|
||||||
|
verzoegert, damit keine synchrone Manager-Verbraucher-Endlosschleife entsteht.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
`WarmwasserReglerTest.php` prueft Leistungsstufen, PT1, Energieberechnung,
|
||||||
|
Zeitplan, thermische Prognose und die zweistufigen Legionellengrenzen.
|
||||||
|
`WarmwassererwaermerModulstrukturTest.php` prueft Metadaten, Formular,
|
||||||
|
19 Properties, bedarfsgesteuerte Diagnosevariablen, Schnittstellenvertrag und
|
||||||
|
Break-before-make-Schaltung.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Die Feldzuordnung und die bewusst entfallenen Enelix-1-Kommunikationsvariablen
|
||||||
|
sind in [`docs/migration/Boiler-x-Stufig.md`](../../migration/Boiler-x-Stufig.md)
|
||||||
|
dokumentiert.
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "{E0185158-0DA9-4D44-BAA7-B06D3D786276}",
|
"id": "{E0185158-0DA9-4D44-BAA7-B06D3D786276}",
|
||||||
"name": "Enelix EMS",
|
"name": "Enelix EMS",
|
||||||
"author": "Belevo AG",
|
"author": "Enelix",
|
||||||
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS",
|
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"version": "8.0"
|
"version": "8.0"
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class EinStufigRegler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public static function leistungsangebot(
|
||||||
|
int $nennleistung,
|
||||||
|
int $istleistung,
|
||||||
|
bool $verfuegbar,
|
||||||
|
bool $aenderungMoeglich,
|
||||||
|
bool $mindestlaufzeitErzwungen
|
||||||
|
): array {
|
||||||
|
if ($nennleistung <= 0) {
|
||||||
|
throw new InvalidArgumentException('Die Nennleistung muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
if (!in_array($istleistung, [0, $nennleistung], true)) {
|
||||||
|
throw new InvalidArgumentException('Die Istleistung muss 0 oder der Nennleistung entsprechen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$verfuegbar || !$aenderungMoeglich || $mindestlaufzeitErzwungen) {
|
||||||
|
return [$istleistung];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [0, $nennleistung];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function mussMindestlaufzeitErzwingen(
|
||||||
|
int $tageslaufzeit,
|
||||||
|
int $mindestlaufzeit,
|
||||||
|
int $sekundenBisTagesende
|
||||||
|
): bool {
|
||||||
|
self::pruefeLaufzeiten($tageslaufzeit, $mindestlaufzeit, $sekundenBisTagesende);
|
||||||
|
|
||||||
|
$restlaufzeit = max(0, $mindestlaufzeit - $tageslaufzeit);
|
||||||
|
|
||||||
|
return $restlaufzeit > 0 && $sekundenBisTagesende <= $restlaufzeit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sekundenBisNaechsteTagespruefung(
|
||||||
|
int $tageslaufzeit,
|
||||||
|
int $mindestlaufzeit,
|
||||||
|
int $sekundenBisTagesende,
|
||||||
|
bool $eingeschaltet
|
||||||
|
): int {
|
||||||
|
self::pruefeLaufzeiten($tageslaufzeit, $mindestlaufzeit, $sekundenBisTagesende);
|
||||||
|
|
||||||
|
$restlaufzeit = max(0, $mindestlaufzeit - $tageslaufzeit);
|
||||||
|
if ($restlaufzeit === 0) {
|
||||||
|
return max(1, $sekundenBisTagesende);
|
||||||
|
}
|
||||||
|
if ($eingeschaltet) {
|
||||||
|
return max(1, min($restlaufzeit, $sekundenBisTagesende));
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(1, $sekundenBisTagesende - $restlaufzeit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{Tag: string, Sekunden: int, StandZeit: int}
|
||||||
|
*/
|
||||||
|
public static function aktualisiereTageslaufzeit(
|
||||||
|
string $gespeicherterTag,
|
||||||
|
int $gespeicherteSekunden,
|
||||||
|
int $standZeit,
|
||||||
|
int $jetzt,
|
||||||
|
bool $warEingeschaltet,
|
||||||
|
DateTimeZone $zeitzone
|
||||||
|
): array {
|
||||||
|
if ($gespeicherteSekunden < 0 || $standZeit < 0 || $jetzt < 0) {
|
||||||
|
throw new InvalidArgumentException('Zeitwerte duerfen nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$jetztDatum = self::datum($jetzt, $zeitzone);
|
||||||
|
$aktuellerTag = $jetztDatum->format('Y-m-d');
|
||||||
|
if ($standZeit <= 0 || $standZeit > $jetzt) {
|
||||||
|
return [
|
||||||
|
'Tag' => $aktuellerTag,
|
||||||
|
'Sekunden' => $gespeicherterTag === $aktuellerTag ? $gespeicherteSekunden : 0,
|
||||||
|
'StandZeit' => $jetzt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$standDatum = self::datum($standZeit, $zeitzone);
|
||||||
|
$sekunden = $gespeicherteSekunden;
|
||||||
|
if ($gespeicherterTag !== $aktuellerTag || $standDatum->format('Y-m-d') !== $aktuellerTag) {
|
||||||
|
$sekunden = 0;
|
||||||
|
if ($warEingeschaltet) {
|
||||||
|
$tagesbeginn = $jetztDatum->setTime(0, 0)->getTimestamp();
|
||||||
|
$sekunden = max(0, $jetzt - max($standZeit, $tagesbeginn));
|
||||||
|
}
|
||||||
|
} elseif ($warEingeschaltet) {
|
||||||
|
$sekunden += $jetzt - $standZeit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagesbeginn = $jetztDatum->setTime(0, 0);
|
||||||
|
$tagesdauer = $tagesbeginn->modify('+1 day')->getTimestamp() - $tagesbeginn->getTimestamp();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Tag' => $aktuellerTag,
|
||||||
|
'Sekunden' => min($sekunden, $tagesdauer),
|
||||||
|
'StandZeit' => $jetzt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sekundenBisTagesende(int $jetzt, DateTimeZone $zeitzone): int
|
||||||
|
{
|
||||||
|
if ($jetzt < 0) {
|
||||||
|
throw new InvalidArgumentException('Der Zeitpunkt darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$datum = self::datum($jetzt, $zeitzone);
|
||||||
|
$tagesende = $datum->setTime(0, 0)->modify('+1 day');
|
||||||
|
|
||||||
|
return max(1, $tagesende->getTimestamp() - $jetzt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pruefeLaufzeiten(
|
||||||
|
int $tageslaufzeit,
|
||||||
|
int $mindestlaufzeit,
|
||||||
|
int $sekundenBisTagesende
|
||||||
|
): void {
|
||||||
|
if ($tageslaufzeit < 0 || $mindestlaufzeit < 0 || $mindestlaufzeit > 86400) {
|
||||||
|
throw new InvalidArgumentException('Laufzeiten sind ausserhalb des gueltigen Bereichs.');
|
||||||
|
}
|
||||||
|
if ($sekundenBisTagesende <= 0) {
|
||||||
|
throw new InvalidArgumentException('Sekunden bis Tagesende muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function datum(int $zeitpunkt, DateTimeZone $zeitzone): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return (new DateTimeImmutable('@' . $zeitpunkt))->setTimezone($zeitzone);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reine Regel- und Verteilfunktion des Managers.
|
||||||
|
*
|
||||||
|
* Die Klasse kennt keine IP-Symcon-Funktionen und kann deshalb unabhaengig
|
||||||
|
* vom Laufzeitsystem getestet werden. Alle Leistungen verwenden die
|
||||||
|
* Vorzeichenkonvention des Vertrags: positiv Bezug, negativ Abgabe.
|
||||||
|
*/
|
||||||
|
final class ManagerRegler
|
||||||
|
{
|
||||||
|
public const BETRIEBSART_INAKTIV = 'Inaktiv';
|
||||||
|
public const BETRIEBSART_PV = 'PV';
|
||||||
|
public const BETRIEBSART_PEAK = 'Peak';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel
|
||||||
|
*/
|
||||||
|
public static function wirksameLastspitzengrenze(
|
||||||
|
int $modus,
|
||||||
|
float $konstanteGrenze,
|
||||||
|
array $monatsgrenzen,
|
||||||
|
int $monat
|
||||||
|
): ?float {
|
||||||
|
if ($modus === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($modus === 1) {
|
||||||
|
return $konstanteGrenze;
|
||||||
|
}
|
||||||
|
if ($modus !== 2) {
|
||||||
|
throw new InvalidArgumentException('Lastspitzenmodus muss 0, 1 oder 2 sein.');
|
||||||
|
}
|
||||||
|
if ($monat < 1 || $monat > 12 || !array_key_exists($monat, $monatsgrenzen)) {
|
||||||
|
throw new InvalidArgumentException('Fuer den aktuellen Monat fehlt eine Lastspitzengrenze.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) $monatsgrenzen[$monat];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function bestimmeBetriebsart(
|
||||||
|
bool $aktiv,
|
||||||
|
bool $netzleistungGueltig,
|
||||||
|
float $netzleistung,
|
||||||
|
string $bisherigeBetriebsart,
|
||||||
|
bool $peakAktiv,
|
||||||
|
float $sollwertSolarladen,
|
||||||
|
float $wirksameLastspitzengrenze,
|
||||||
|
float $umschaltdifferenzProzent
|
||||||
|
): string {
|
||||||
|
if (!$aktiv || !$netzleistungGueltig) {
|
||||||
|
return self::BETRIEBSART_INAKTIV;
|
||||||
|
}
|
||||||
|
if (!$peakAktiv) {
|
||||||
|
return self::BETRIEBSART_PV;
|
||||||
|
}
|
||||||
|
if ($umschaltdifferenzProzent < 0 || $umschaltdifferenzProzent > 100) {
|
||||||
|
throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 Prozent liegen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kandidat = $bisherigeBetriebsart;
|
||||||
|
if ($netzleistung < $sollwertSolarladen) {
|
||||||
|
$kandidat = self::BETRIEBSART_PV;
|
||||||
|
} elseif ($netzleistung > $wirksameLastspitzengrenze) {
|
||||||
|
$kandidat = self::BETRIEBSART_PEAK;
|
||||||
|
} elseif (!in_array($kandidat, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)) {
|
||||||
|
$kandidat = self::BETRIEBSART_PV;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kandidat === $bisherigeBetriebsart
|
||||||
|
|| !in_array($bisherigeBetriebsart, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)
|
||||||
|
|| $umschaltdifferenzProzent === 0.0
|
||||||
|
) {
|
||||||
|
return $kandidat;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pvKorrektur = $sollwertSolarladen - $netzleistung;
|
||||||
|
$peakKorrektur = $wirksameLastspitzengrenze - $netzleistung;
|
||||||
|
$bezugswert = max(abs($pvKorrektur), abs($peakKorrektur), 1.0);
|
||||||
|
$differenzProzent = abs($pvKorrektur - $peakKorrektur) / $bezugswert * 100.0;
|
||||||
|
|
||||||
|
return $differenzProzent >= $umschaltdifferenzProzent
|
||||||
|
? $kandidat
|
||||||
|
: $bisherigeBetriebsart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten
|
||||||
|
* @return array{
|
||||||
|
* Verteilbudget_W: int,
|
||||||
|
* AktuelleVerbraucherleistung_W: int,
|
||||||
|
* NichtSteuerbar_W: int,
|
||||||
|
* Sollwerte: array<int, int>,
|
||||||
|
* Restabweichung_W: int
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function verteile(
|
||||||
|
float $netzleistung,
|
||||||
|
float $zielnetzleistung,
|
||||||
|
array $verbraucher,
|
||||||
|
string $prioritaetsfeld
|
||||||
|
): array {
|
||||||
|
if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) {
|
||||||
|
throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$aktuelleLeistung = 0;
|
||||||
|
$nichtSteuerbar = 0;
|
||||||
|
$steuerbar = [];
|
||||||
|
|
||||||
|
foreach ($verbraucher as $eintrag) {
|
||||||
|
self::pruefeVerteilEintrag($eintrag, $prioritaetsfeld);
|
||||||
|
$leistung = self::aktuelleLeistung($eintrag['Daten']);
|
||||||
|
$aktuelleLeistung += $leistung;
|
||||||
|
|
||||||
|
$daten = $eintrag['Daten'];
|
||||||
|
if (!$daten['Verfuegbar'] || !$daten['AenderungMoeglich'] || $daten['Leistungswerte_W'] === []) {
|
||||||
|
$nichtSteuerbar += $leistung;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$steuerbar[] = [
|
||||||
|
'InstanzID' => $eintrag['InstanzID'],
|
||||||
|
'Prioritaet' => $daten[$prioritaetsfeld],
|
||||||
|
'Leistungswerte_W' => $daten['Leistungswerte_W'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($steuerbar, static function (array $links, array $rechts): int {
|
||||||
|
return [$links['Prioritaet'], $links['InstanzID']] <=> [$rechts['Prioritaet'], $rechts['InstanzID']];
|
||||||
|
});
|
||||||
|
|
||||||
|
$verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
|
||||||
|
$sollwerte = [];
|
||||||
|
$minimumSumme = 0;
|
||||||
|
foreach ($steuerbar as $eintrag) {
|
||||||
|
$minimum = self::kleinsterLeistungswert($eintrag['Leistungswerte_W']);
|
||||||
|
$sollwerte[$eintrag['InstanzID']] = $minimum;
|
||||||
|
$minimumSumme += $minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
$verfuegbar = max(0, $verteilbudget - $nichtSteuerbar - $minimumSumme);
|
||||||
|
foreach ($steuerbar as $eintrag) {
|
||||||
|
$instanzID = $eintrag['InstanzID'];
|
||||||
|
$minimum = $sollwerte[$instanzID];
|
||||||
|
$neuerSollwert = self::groessterLeistungswertBis(
|
||||||
|
$eintrag['Leistungswerte_W'],
|
||||||
|
$minimum + $verfuegbar
|
||||||
|
);
|
||||||
|
$verfuegbar -= $neuerSollwert - $minimum;
|
||||||
|
$sollwerte[$instanzID] = $neuerSollwert;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zugeteilt = $nichtSteuerbar + array_sum($sollwerte);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Verteilbudget_W' => $verteilbudget,
|
||||||
|
'AktuelleVerbraucherleistung_W' => $aktuelleLeistung,
|
||||||
|
'NichtSteuerbar_W' => $nichtSteuerbar,
|
||||||
|
'Sollwerte' => $sollwerte,
|
||||||
|
'Restabweichung_W' => $verteilbudget - $zugeteilt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $daten
|
||||||
|
*/
|
||||||
|
public static function angenommenerSollwert(array $daten): ?int
|
||||||
|
{
|
||||||
|
foreach ($daten['Zustand'] as $zustand) {
|
||||||
|
if ($zustand['Kennung'] === 'Sollleistung_W') {
|
||||||
|
return is_int($zustand['Wert']) ? $zustand['Wert'] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $daten
|
||||||
|
*/
|
||||||
|
private static function aktuelleLeistung(array $daten): int
|
||||||
|
{
|
||||||
|
if ($daten['Leistungsquelle'] !== Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN
|
||||||
|
&& is_numeric($daten['Istleistung_W'])
|
||||||
|
) {
|
||||||
|
return (int) round((float) $daten['Istleistung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::angenommenerSollwert($daten) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $eintrag
|
||||||
|
*/
|
||||||
|
private static function pruefeVerteilEintrag(array $eintrag, string $prioritaetsfeld): void
|
||||||
|
{
|
||||||
|
if (!isset($eintrag['InstanzID'], $eintrag['Daten'])
|
||||||
|
|| !is_int($eintrag['InstanzID'])
|
||||||
|
|| $eintrag['InstanzID'] <= 0
|
||||||
|
|| !is_array($eintrag['Daten'])
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('Verbrauchereintrag ist ungueltig.');
|
||||||
|
}
|
||||||
|
Nachrichtenvertrag::pruefeVerbraucherdaten($eintrag['Daten']);
|
||||||
|
if (!array_key_exists($prioritaetsfeld, $eintrag['Daten'])) {
|
||||||
|
throw new InvalidArgumentException('Prioritaet fehlt im Verbrauchereintrag.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
|
*/
|
||||||
|
private static function kleinsterLeistungswert(array $leistungswerte): int
|
||||||
|
{
|
||||||
|
$erster = $leistungswerte[0];
|
||||||
|
|
||||||
|
return is_int($erster) ? $erster : $erster['Von_W'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
|
*/
|
||||||
|
private static function groessterLeistungswertBis(array $leistungswerte, int $obergrenze): int
|
||||||
|
{
|
||||||
|
$gewaehlt = self::kleinsterLeistungswert($leistungswerte);
|
||||||
|
foreach ($leistungswerte as $eintrag) {
|
||||||
|
if (is_int($eintrag)) {
|
||||||
|
if ($eintrag > $obergrenze) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$gewaehlt = $eintrag;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eintrag['Von_W'] > $obergrenze) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$gewaehlt = min($eintrag['Bis_W'], $obergrenze);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $gewaehlt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,9 @@ interface ManagerSchnittstelle
|
|||||||
/**
|
/**
|
||||||
* Empfaengt den vollstaendigen aktuellen Datensatz eines Verbrauchers.
|
* Empfaengt den vollstaendigen aktuellen Datensatz eines Verbrauchers.
|
||||||
*
|
*
|
||||||
|
* Dies ist die einzige fachliche Empfangsmethode des Managers.
|
||||||
|
*
|
||||||
* @param array<string, mixed> $daten
|
* @param array<string, mixed> $daten
|
||||||
*/
|
*/
|
||||||
public function VerbraucherdatenEmpfangen(array $daten): void;
|
public function VerbraucherdatenEmpfangen(array $daten): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriert die gemeinsamen Properties und Variablen aller EMS-Verbraucher.
|
||||||
|
*
|
||||||
|
* Das Trait ist noch keine vollstaendige Steuerungsimplementierung. Die
|
||||||
|
* konkreten Module ergaenzen Geraeteanbindung, Leistungsangebot und Zustand.
|
||||||
|
*/
|
||||||
|
trait VerbraucherBasisTrait
|
||||||
|
{
|
||||||
|
protected function registriereVerbraucherBasis(): void
|
||||||
|
{
|
||||||
|
$this->RegisterPropertyInteger('PrioritaetPV', 0);
|
||||||
|
$this->RegisterPropertyInteger('PrioritaetPeak', 0);
|
||||||
|
$this->RegisterPropertyInteger('Meldeintervall', 10);
|
||||||
|
$this->RegisterPropertyInteger('VorgabeTimeout', 120);
|
||||||
|
$this->RegisterPropertyBoolean('EinstellungenInVisu', false);
|
||||||
|
$this->RegisterPropertyBoolean('LoggingEin', false);
|
||||||
|
|
||||||
|
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
|
||||||
|
$this->EnableAction('Aktiv');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function registriereVerbraucherDiagnose(): void
|
||||||
|
{
|
||||||
|
$this->RegisterVariableFloat('Istleistung', 'Istleistung', '', 20);
|
||||||
|
$this->RegisterVariableInteger('Leistungsquelle', 'Leistungsquelle', '', 30);
|
||||||
|
$this->RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 40);
|
||||||
|
$this->RegisterVariableBoolean('SollwertGueltig', 'Sollwert gueltig', '~Switch', 50);
|
||||||
|
$this->RegisterVariableBoolean('Verfuegbar', 'Verfuegbar', '~Switch', 60);
|
||||||
|
$this->RegisterVariableBoolean('AenderungMoeglich', 'Aenderung moeglich', '~Switch', 70);
|
||||||
|
$this->RegisterVariableBoolean('Stoerung', 'Stoerung', '', 80);
|
||||||
|
$this->RegisterVariableString('Stoertext', 'Stoertext', '', 90);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,11 @@ namespace Belevo\EnelixEMS;
|
|||||||
interface VerbraucherSchnittstelle
|
interface VerbraucherSchnittstelle
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Empfaengt die aktuelle Leistungsvorgabe des Managers.
|
* Empfaengt Kopf und Sollleistung_W als vollstaendigen Managerdatensatz.
|
||||||
|
*
|
||||||
|
* Dies ist die einzige fachliche Empfangsmethode des Verbrauchers.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $daten
|
* @param array<string, mixed> $daten
|
||||||
*/
|
*/
|
||||||
public function ManagerdatenEmpfangen(array $daten): void;
|
public function ManagerdatenEmpfangen(array $daten): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reine Hilfsfunktionen des mehrstufigen Warmwassererwaermers.
|
||||||
|
*
|
||||||
|
* Die Klasse kennt keine IP-Symcon-Funktionen und ist dadurch unabhaengig
|
||||||
|
* vom Laufzeitsystem testbar.
|
||||||
|
*/
|
||||||
|
final class WarmwasserRegler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<array{Stufe: int, Leistung_W: int, KontaktID: int}>
|
||||||
|
*/
|
||||||
|
public static function dekodiereLeistungsstufen(string $json): array
|
||||||
|
{
|
||||||
|
$werte = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($werte) || !self::istListe($werte)) {
|
||||||
|
throw new InvalidArgumentException('LeistungsStufen muss eine JSON-Liste sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ergebnis = [];
|
||||||
|
$leistungen = [];
|
||||||
|
$kontakte = [];
|
||||||
|
foreach ($werte as $index => $eintrag) {
|
||||||
|
if (!is_array($eintrag)) {
|
||||||
|
throw new InvalidArgumentException('Jede Leistungsstufe muss ein Objekt sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stufe = $eintrag['Stufe'] ?? $index + 1;
|
||||||
|
$leistung = $eintrag['Leistung'] ?? null;
|
||||||
|
$kontakt = $eintrag['Schaltkontakt_Stufe'] ?? null;
|
||||||
|
if (!is_int($stufe) || $stufe <= 0) {
|
||||||
|
throw new InvalidArgumentException('Stufe muss eine positive Ganzzahl sein.');
|
||||||
|
}
|
||||||
|
if (!is_int($leistung) || $leistung <= 0) {
|
||||||
|
throw new InvalidArgumentException('Leistung muss eine positive Ganzzahl sein.');
|
||||||
|
}
|
||||||
|
if (!is_int($kontakt) || $kontakt <= 0) {
|
||||||
|
throw new InvalidArgumentException('Schaltkontakt_Stufe muss eine positive Variablen-ID sein.');
|
||||||
|
}
|
||||||
|
if (isset($leistungen[$leistung])) {
|
||||||
|
throw new InvalidArgumentException('Leistungen muessen eindeutig sein.');
|
||||||
|
}
|
||||||
|
if (isset($kontakte[$kontakt])) {
|
||||||
|
throw new InvalidArgumentException('Schaltkontakte duerfen nur einer Stufe zugeordnet sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$leistungen[$leistung] = true;
|
||||||
|
$kontakte[$kontakt] = true;
|
||||||
|
$ergebnis[] = [
|
||||||
|
'Stufe' => $stufe,
|
||||||
|
'Leistung_W' => $leistung,
|
||||||
|
'KontaktID' => $kontakt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort(
|
||||||
|
$ergebnis,
|
||||||
|
static fn (array $links, array $rechts): int => $links['Leistung_W'] <=> $rechts['Leistung_W']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $ergebnis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public static function leistungswerte(array $stufen): array
|
||||||
|
{
|
||||||
|
return array_merge([0], array_column($stufen, 'Leistung_W'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||||
|
*/
|
||||||
|
public static function stufeFuerLeistung(array $stufen, int $leistung): int
|
||||||
|
{
|
||||||
|
if ($leistung === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
foreach ($stufen as $stufe) {
|
||||||
|
if ($stufe['Leistung_W'] === $leistung) {
|
||||||
|
return $stufe['Stufe'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Die Sollleistung entspricht keiner konfigurierten Leistungsstufe.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function pt1(float $bisher, float $eingang, int $deltaSekunden, int $zeitkonstante): float
|
||||||
|
{
|
||||||
|
if ($deltaSekunden <= 0) {
|
||||||
|
throw new InvalidArgumentException('Das Zeitintervall muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
if ($zeitkonstante <= 0) {
|
||||||
|
throw new InvalidArgumentException('Die Zeitkonstante muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$alpha = $deltaSekunden / ($zeitkonstante + $deltaSekunden);
|
||||||
|
|
||||||
|
return $bisher + $alpha * ($eingang - $bisher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function restzeitBisLastwechsel(
|
||||||
|
int $letzterLastwechsel,
|
||||||
|
int $jetzt,
|
||||||
|
int $sperrzeitSekunden
|
||||||
|
): int {
|
||||||
|
if ($sperrzeitSekunden < 0) {
|
||||||
|
throw new InvalidArgumentException('Die Lastwechsel-Sperrzeit darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
if ($letzterLastwechsel <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$verstrichen = max(0, $jetzt - $letzterLastwechsel);
|
||||||
|
|
||||||
|
return max(0, $sperrzeitSekunden - $verstrichen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function energieKWh(float $leistungW, int $dauerSekunden): float
|
||||||
|
{
|
||||||
|
if ($dauerSekunden < 0) {
|
||||||
|
throw new InvalidArgumentException('Die Dauer darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $leistungW * $dauerSekunden / 3600000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function kannZielErreichen(
|
||||||
|
float $volumenLiter,
|
||||||
|
float $isttemperatur,
|
||||||
|
float $zieltemperatur,
|
||||||
|
int $verbleibendeSekunden,
|
||||||
|
int $maximalleistungW
|
||||||
|
): bool {
|
||||||
|
if ($zieltemperatur <= $isttemperatur) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($volumenLiter <= 0 || $verbleibendeSekunden < 0 || $maximalleistungW <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$erforderlicheWaermeJ = 4186.0 * $volumenLiter * ($zieltemperatur - $isttemperatur);
|
||||||
|
$verfuegbareWaermeJ = $maximalleistungW * $verbleibendeSekunden;
|
||||||
|
|
||||||
|
return $verfuegbareWaermeJ >= $erforderlicheWaermeJ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: float, 1: float}
|
||||||
|
*/
|
||||||
|
public static function legionellenTemperaturgrenzen(
|
||||||
|
bool $aktiv,
|
||||||
|
int $sekundenSeitAbschluss,
|
||||||
|
int $minimalintervallTage,
|
||||||
|
int $maximalintervallTage,
|
||||||
|
float $mindesttemperatur,
|
||||||
|
float $maximaltemperatur,
|
||||||
|
float $legionellentemperatur
|
||||||
|
): array {
|
||||||
|
if (!$aktiv) {
|
||||||
|
return [$mindesttemperatur, $maximaltemperatur];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sekundenSeitAbschluss >= $minimalintervallTage * 86400) {
|
||||||
|
$maximaltemperatur = $legionellentemperatur;
|
||||||
|
}
|
||||||
|
if ($sekundenSeitAbschluss >= $maximalintervallTage * 86400) {
|
||||||
|
$mindesttemperatur = $legionellentemperatur;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$mindesttemperatur, $maximaltemperatur];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{Zeitpunkt: int, Solltemperatur: float}|null
|
||||||
|
*/
|
||||||
|
public static function naechsterZeitplaneintrag(string $json, DateTimeImmutable $jetzt): ?array
|
||||||
|
{
|
||||||
|
if (trim($json) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$werte = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($werte) || !self::istListe($werte)) {
|
||||||
|
throw new InvalidArgumentException('Zeitplan muss eine JSON-Liste sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$naechster = null;
|
||||||
|
foreach ($werte as $eintrag) {
|
||||||
|
if (!is_array($eintrag)
|
||||||
|
|| !is_string($eintrag['Uhrzeit'] ?? null)
|
||||||
|
|| !is_numeric($eintrag['Solltemperatur'] ?? null)
|
||||||
|
|| !preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $eintrag['Uhrzeit'])
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('Zeitplaneintraege benoetigen Uhrzeit HH:MM und Solltemperatur.');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$stunde, $minute] = array_map('intval', explode(':', $eintrag['Uhrzeit']));
|
||||||
|
$zeitpunkt = $jetzt->setTime($stunde, $minute, 0);
|
||||||
|
if ($zeitpunkt <= $jetzt) {
|
||||||
|
$zeitpunkt = $zeitpunkt->modify('+1 day');
|
||||||
|
}
|
||||||
|
if ($naechster === null || $zeitpunkt < $naechster['Datum']) {
|
||||||
|
$naechster = [
|
||||||
|
'Datum' => $zeitpunkt,
|
||||||
|
'Solltemperatur' => (float) $eintrag['Solltemperatur'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($naechster === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Zeitpunkt' => $naechster['Datum']->getTimestamp(),
|
||||||
|
'Solltemperatur' => $naechster['Solltemperatur'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<mixed> $wert */
|
||||||
|
private static function istListe(array $wert): bool
|
||||||
|
{
|
||||||
|
return $wert === [] || array_keys($wert) === range(0, count($wert) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class DokumentationsstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @return list<array{string}> */
|
||||||
|
public function modulReadmes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['Batterie'],
|
||||||
|
['Pufferspeicher'],
|
||||||
|
['Waermepumpe'],
|
||||||
|
['Ladestation-Stand-Alone'],
|
||||||
|
['Ladestation-Gateway'],
|
||||||
|
['Easee-Gateway'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider modulReadmes */
|
||||||
|
public function testJederModulentwurfHatEinReadme(string $modul): void
|
||||||
|
{
|
||||||
|
$pfad = __DIR__ . '/../docs/module/' . $modul . '/README.md';
|
||||||
|
self::assertFileExists($pfad);
|
||||||
|
|
||||||
|
$inhalt = file_get_contents($pfad);
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: Diskussionsentwurf', $inhalt);
|
||||||
|
self::assertStringContainsString('Properties', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchnittstellenDokumentiertSind(): void
|
||||||
|
{
|
||||||
|
self::assertFileExists(__DIR__ . '/../docs/Schnittstelle.md');
|
||||||
|
self::assertFileExists(__DIR__ . '/../docs/Obere-Anschluesse.md');
|
||||||
|
self::assertFileExists(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testManagerDokumentiertDenImplementiertenStand(): void
|
||||||
|
{
|
||||||
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Manager/README.md');
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: Implementiert', $inhalt);
|
||||||
|
self::assertStringContainsString('Verteilalgorithmus', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVerbraucherEinStufigDokumentiertDenImplementiertenStand(): void
|
||||||
|
{
|
||||||
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Verbraucher-1-Stufig/README.md');
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: implementiert', $inhalt);
|
||||||
|
self::assertStringContainsString('Umschaltabstand', $inhalt);
|
||||||
|
self::assertStringContainsString('Installation und Inbetriebnahme', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWarmwassererwaermerDokumentiertDenImplementiertenStand(): void
|
||||||
|
{
|
||||||
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Wassererwaermer/README.md');
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: implementiert', $inhalt);
|
||||||
|
self::assertStringContainsString('Properties', $inhalt);
|
||||||
|
self::assertStringContainsString('Managerkommunikation', $inhalt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\EinStufigRegler;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class EinStufigReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFreiesLeistungsangebotEnthaeltAusUndEin(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0, 3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGesperrtesOderErzwungenesAngebotEnthaeltNurIstleistung(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 3500, true, false, false)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, true)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 3500, false, true, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUngueltigeIstleistungWirdAbgewiesen(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 1200, true, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMindestlaufzeitWirdErstZumSpaetestmoeglichenStartErzwungen(): void
|
||||||
|
{
|
||||||
|
self::assertFalse(EinStufigRegler::mussMindestlaufzeitErzwingen(0, 3600, 7200));
|
||||||
|
self::assertTrue(EinStufigRegler::mussMindestlaufzeitErzwingen(0, 3600, 3600));
|
||||||
|
self::assertTrue(EinStufigRegler::mussMindestlaufzeitErzwingen(1800, 3600, 1200));
|
||||||
|
self::assertFalse(EinStufigRegler::mussMindestlaufzeitErzwingen(3600, 3600, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNaechsteTagespruefungIstEreignisbasiert(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
3600,
|
||||||
|
EinStufigRegler::sekundenBisNaechsteTagespruefung(0, 3600, 7200, false)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
1800,
|
||||||
|
EinStufigRegler::sekundenBisNaechsteTagespruefung(1800, 3600, 7200, true)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
7200,
|
||||||
|
EinStufigRegler::sekundenBisNaechsteTagespruefung(3600, 3600, 7200, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLaufzeitWirdInnerhalbEinesTagesFortgeschrieben(): void
|
||||||
|
{
|
||||||
|
$zone = new DateTimeZone('Europe/Zurich');
|
||||||
|
$von = (new DateTimeImmutable('2026-09-17 10:00:00', $zone))->getTimestamp();
|
||||||
|
$bis = (new DateTimeImmutable('2026-09-17 10:02:30', $zone))->getTimestamp();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['Tag' => '2026-09-17', 'Sekunden' => 250, 'StandZeit' => $bis],
|
||||||
|
EinStufigRegler::aktualisiereTageslaufzeit(
|
||||||
|
'2026-09-17',
|
||||||
|
100,
|
||||||
|
$von,
|
||||||
|
$bis,
|
||||||
|
true,
|
||||||
|
$zone
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLaufzeitWirdAnLokalerTagesgrenzeGetrennt(): void
|
||||||
|
{
|
||||||
|
$zone = new DateTimeZone('Europe/Zurich');
|
||||||
|
$von = (new DateTimeImmutable('2026-09-16 23:59:50', $zone))->getTimestamp();
|
||||||
|
$bis = (new DateTimeImmutable('2026-09-17 00:00:10', $zone))->getTimestamp();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['Tag' => '2026-09-17', 'Sekunden' => 10, 'StandZeit' => $bis],
|
||||||
|
EinStufigRegler::aktualisiereTageslaufzeit(
|
||||||
|
'2026-09-16',
|
||||||
|
1000,
|
||||||
|
$von,
|
||||||
|
$bis,
|
||||||
|
true,
|
||||||
|
$zone
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTagesendeBeruecksichtigtSommerzeit(): void
|
||||||
|
{
|
||||||
|
$zone = new DateTimeZone('Europe/Zurich');
|
||||||
|
$zeitpunkt = (new DateTimeImmutable('2026-03-29 00:00:00', $zone))->getTimestamp();
|
||||||
|
|
||||||
|
self::assertSame(23 * 3600, EinStufigRegler::sekundenBisTagesende($zeitpunkt, $zone));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ final class LibraryMetadataTest extends TestCase
|
|||||||
|
|
||||||
$daten = json_decode($inhalt, true, 512, JSON_THROW_ON_ERROR);
|
$daten = json_decode($inhalt, true, 512, JSON_THROW_ON_ERROR);
|
||||||
self::assertSame('Enelix EMS', $daten['name']);
|
self::assertSame('Enelix EMS', $daten['name']);
|
||||||
|
self::assertSame('Enelix', $daten['author']);
|
||||||
self::assertSame('8.0', $daten['compatibility']['version']);
|
self::assertSame('8.0', $daten['compatibility']['version']);
|
||||||
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $daten['id']);
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $daten['id']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ManagerModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testManagerIstAlsSymconModulVollstaendig(): void
|
||||||
|
{
|
||||||
|
foreach (['module.json', 'form.json', 'module.php'] as $datei) {
|
||||||
|
self::assertFileExists(__DIR__ . '/../Manager/' . $datei);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadaten = json_decode(
|
||||||
|
(string) file_get_contents(__DIR__ . '/../Manager/module.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
self::assertSame('Manager', $metadaten['name']);
|
||||||
|
self::assertSame('Enelix', $metadaten['vendor']);
|
||||||
|
self::assertSame(['Manager'], $metadaten['aliases']);
|
||||||
|
self::assertSame(3, $metadaten['type']);
|
||||||
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormularEnthaeltDieZentralenManagerProperties(): void
|
||||||
|
{
|
||||||
|
$formular = json_decode(
|
||||||
|
(string) file_get_contents(__DIR__ . '/../Manager/form.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$json = json_encode($formular, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'NetzleistungVariableID',
|
||||||
|
'VerbraucherZuordnung',
|
||||||
|
'AutomatischeVerbraucherZuordnung',
|
||||||
|
'Lastspitzenmodus',
|
||||||
|
'Monatsgrenzen',
|
||||||
|
'SollwertSolarladen',
|
||||||
|
'KeepAlive',
|
||||||
|
'VerbraucherTimeout',
|
||||||
|
'MesswertMaxAlter',
|
||||||
|
'Umschaltdifferenz',
|
||||||
|
'DiagnosevariablenAnzeigen',
|
||||||
|
'LoggingEin',
|
||||||
|
'VerbraucherAktualisieren',
|
||||||
|
] 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);
|
||||||
|
}
|
||||||
|
self::assertSame('Lizenzierung', $formular['elements'][0]['caption']);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"'SuchbereichID' => \$SuchbereichID",
|
||||||
|
$json
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"'Zuordnung' => \$AutomatischeVerbraucherZuordnung",
|
||||||
|
$json
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testManagerTrenntDiagnosevariablenUndDebugLogging(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false)",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('DiagnosevariablenAnzeigen')", $inhalt);
|
||||||
|
self::assertStringContainsString('UnregisterVariable($ident)', $inhalt);
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $inhalt);
|
||||||
|
self::assertStringContainsString('SendDebug(', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
self::assertStringContainsString('implements ManagerSchnittstelle', $inhalt);
|
||||||
|
self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt);
|
||||||
|
self::assertStringContainsString("'ManagerdatenEmpfangen'", $inhalt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\ManagerRegler;
|
||||||
|
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ManagerReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testOhnePeakBegrenzungBleibtSolarladenAktiv(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
ManagerRegler::BETRIEBSART_PV,
|
||||||
|
ManagerRegler::bestimmeBetriebsart(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
25000.0,
|
||||||
|
ManagerRegler::BETRIEBSART_INAKTIV,
|
||||||
|
false,
|
||||||
|
0.0,
|
||||||
|
15000.0,
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBetriebsartBleibtZwischenDenZielwertenErhalten(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
ManagerRegler::BETRIEBSART_PEAK,
|
||||||
|
ManagerRegler::bestimmeBetriebsart(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
5000.0,
|
||||||
|
ManagerRegler::BETRIEBSART_PEAK,
|
||||||
|
true,
|
||||||
|
0.0,
|
||||||
|
12000.0,
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUmschaltdifferenzVerhindertZuKleineAenderung(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
ManagerRegler::BETRIEBSART_PV,
|
||||||
|
ManagerRegler::bestimmeBetriebsart(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
11000.0,
|
||||||
|
ManagerRegler::BETRIEBSART_PV,
|
||||||
|
true,
|
||||||
|
10000.0,
|
||||||
|
10500.0,
|
||||||
|
60.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
0.0,
|
||||||
|
4500.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(20, 5, [0, 3000], 0),
|
||||||
|
$this->verbraucher(10, 0, [0, ['Von_W' => 1000, 'Bis_W' => 2000], 4000], 0),
|
||||||
|
],
|
||||||
|
'PrioritaetPV'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(4500, $ergebnis['Verteilbudget_W']);
|
||||||
|
self::assertSame([10 => 4000, 20 => 0], $ergebnis['Sollwerte']);
|
||||||
|
self::assertSame(500, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNichtAenderbarerVerbraucherReserviertSeineIstleistung(): void
|
||||||
|
{
|
||||||
|
$fest = $this->verbraucher(10, 0, [0, 3000], 3000, false);
|
||||||
|
$steuerbar = $this->verbraucher(20, 1, [0, ['Von_W' => 1, 'Bis_W' => 4000]], 0);
|
||||||
|
|
||||||
|
$ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV');
|
||||||
|
|
||||||
|
self::assertSame(3000, $ergebnis['NichtSteuerbar_W']);
|
||||||
|
self::assertSame([20 => 2000], $ergebnis['Sollwerte']);
|
||||||
|
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void
|
||||||
|
{
|
||||||
|
$verbraucher = $this->verbraucher(10, 0, [0, 3000], null, false);
|
||||||
|
$verbraucher['Daten']['Leistungsquelle'] = Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN;
|
||||||
|
$verbraucher['Daten']['Istleistung_W'] = null;
|
||||||
|
$verbraucher['Daten']['Zustand'][0]['Wert'] = 3000;
|
||||||
|
|
||||||
|
$ergebnis = ManagerRegler::verteile(3000.0, 0.0, [$verbraucher], 'PrioritaetPV');
|
||||||
|
|
||||||
|
self::assertSame(3000, $ergebnis['AktuelleVerbraucherleistung_W']);
|
||||||
|
self::assertSame(-3000, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMonatlicheGrenzeMussVollstaendigSein(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
ManagerRegler::wirksameLastspitzengrenze(2, 0.0, [1 => 12000.0], 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
|
* @return array{InstanzID: int, Daten: array<string, mixed>}
|
||||||
|
*/
|
||||||
|
private function verbraucher(
|
||||||
|
int $instanzID,
|
||||||
|
int $prioritaet,
|
||||||
|
array $leistungswerte,
|
||||||
|
?int $istleistung,
|
||||||
|
bool $aenderungMoeglich = true
|
||||||
|
): array {
|
||||||
|
$quelle = $istleistung === null
|
||||||
|
? Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN
|
||||||
|
: Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'InstanzID' => $instanzID,
|
||||||
|
'Daten' => [
|
||||||
|
'Kopf' => [
|
||||||
|
'Version' => Nachrichtenvertrag::VERSION,
|
||||||
|
'AbsenderID' => $instanzID,
|
||||||
|
'EmpfaengerID' => 1,
|
||||||
|
'Zeitpunkt' => 1788825600,
|
||||||
|
],
|
||||||
|
'PrioritaetPV' => $prioritaet,
|
||||||
|
'PrioritaetPeak' => $prioritaet,
|
||||||
|
'Leistungswerte_W' => $leistungswerte,
|
||||||
|
'AenderungMoeglich' => $aenderungMoeglich,
|
||||||
|
'Verfuegbar' => true,
|
||||||
|
'Istleistung_W' => $istleistung,
|
||||||
|
'Leistungsquelle' => $quelle,
|
||||||
|
'Zustand' => [[
|
||||||
|
'Kennung' => 'Sollleistung_W',
|
||||||
|
'Art' => 'Sollwert',
|
||||||
|
'Wert' => $istleistung,
|
||||||
|
'Einheit' => 'W',
|
||||||
|
]],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class VerbraucherBasisTraitTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGemeinsamePropertiesUndVariablenWerdenRegistriert(): void
|
||||||
|
{
|
||||||
|
$modul = new VerbraucherBasisDummy();
|
||||||
|
$modul->registriere();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'PrioritaetPV',
|
||||||
|
'PrioritaetPeak',
|
||||||
|
'Meldeintervall',
|
||||||
|
'VorgabeTimeout',
|
||||||
|
'EinstellungenInVisu',
|
||||||
|
'LoggingEin',
|
||||||
|
],
|
||||||
|
array_keys($modul->properties)
|
||||||
|
);
|
||||||
|
self::assertSame(['Aktiv'], array_keys($modul->variablen));
|
||||||
|
self::assertSame(['Aktiv'], $modul->aktionen);
|
||||||
|
|
||||||
|
$modul->registriereDiagnose();
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Aktiv',
|
||||||
|
'Istleistung',
|
||||||
|
'Leistungsquelle',
|
||||||
|
'Sollleistung',
|
||||||
|
'SollwertGueltig',
|
||||||
|
'Verfuegbar',
|
||||||
|
'AenderungMoeglich',
|
||||||
|
'Stoerung',
|
||||||
|
'Stoertext',
|
||||||
|
],
|
||||||
|
array_keys($modul->variablen)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class VerbraucherBasisDummy
|
||||||
|
{
|
||||||
|
use VerbraucherBasisTrait;
|
||||||
|
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $properties = [];
|
||||||
|
|
||||||
|
/** @var array<string, string> */
|
||||||
|
public array $variablen = [];
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
public array $aktionen = [];
|
||||||
|
|
||||||
|
public function registriere(): void
|
||||||
|
{
|
||||||
|
$this->registriereVerbraucherBasis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registriereDiagnose(): void
|
||||||
|
{
|
||||||
|
$this->registriereVerbraucherDiagnose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RegisterPropertyInteger(string $ident, int $standard): void
|
||||||
|
{
|
||||||
|
$this->properties[$ident] = $standard;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RegisterPropertyBoolean(string $ident, bool $standard): void
|
||||||
|
{
|
||||||
|
$this->properties[$ident] = $standard;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RegisterVariableBoolean(string $ident, string $name, string $profil, int $position): void
|
||||||
|
{
|
||||||
|
$this->variablen[$ident] = 'Boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RegisterVariableFloat(string $ident, string $name, string $profil, int $position): void
|
||||||
|
{
|
||||||
|
$this->variablen[$ident] = 'Float';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RegisterVariableInteger(string $ident, string $name, string $profil, int $position): void
|
||||||
|
{
|
||||||
|
$this->variablen[$ident] = 'Integer';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RegisterVariableString(string $ident, string $name, string $profil, int $position): void
|
||||||
|
{
|
||||||
|
$this->variablen[$ident] = 'String';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function EnableAction(string $ident): void
|
||||||
|
{
|
||||||
|
$this->aktionen[] = $ident;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class VerbraucherEinStufigModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
private const MODULPFAD = __DIR__ . '/../VerbraucherEinStufig/';
|
||||||
|
|
||||||
|
public function testModulIstVollstaendigUndMetadatenSindGueltig(): void
|
||||||
|
{
|
||||||
|
foreach (['module.json', 'form.json', 'module.php'] as $datei) {
|
||||||
|
self::assertFileExists(self::MODULPFAD . $datei);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadaten = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'module.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
self::assertSame('VerbraucherEinStufig', $metadaten['name']);
|
||||||
|
self::assertSame('Enelix', $metadaten['vendor']);
|
||||||
|
self::assertSame(['Verbraucher 1-Stufig'], $metadaten['aliases']);
|
||||||
|
self::assertSame(3, $metadaten['type']);
|
||||||
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPropertiesSindRegistriertUndImFormularVorhanden(): void
|
||||||
|
{
|
||||||
|
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
preg_match_all(
|
||||||
|
'/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/',
|
||||||
|
$basis . $modul,
|
||||||
|
$treffer
|
||||||
|
);
|
||||||
|
$properties = array_values(array_unique($treffer[1]));
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'PrioritaetPV',
|
||||||
|
'PrioritaetPeak',
|
||||||
|
'Meldeintervall',
|
||||||
|
'VorgabeTimeout',
|
||||||
|
'EinstellungenInVisu',
|
||||||
|
'LoggingEin',
|
||||||
|
'Nennleistung',
|
||||||
|
'SchaltkontaktVariableID',
|
||||||
|
'SchaltkontaktInvertiert',
|
||||||
|
'RueckmeldungVariableID',
|
||||||
|
'Mindestlaufzeit',
|
||||||
|
'Umschaltabstand',
|
||||||
|
'DiagnosevariablenAnzeigen',
|
||||||
|
],
|
||||||
|
$properties
|
||||||
|
);
|
||||||
|
|
||||||
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
self::assertStringContainsString('"name": "' . $property . '"', $formular, $property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNurUmschaltabstandErsetztAlteZyklusProperties(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyInteger('Umschaltabstand', 5)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('Interval'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('IdleCounterMax'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterTimer('Regelzyklus'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegelungReagiertAufEreignisseUndEinmalTimer(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('public function MessageSink', $modul);
|
||||||
|
self::assertStringContainsString('RegisterMessage($neueID, self::VM_UPDATE)', $modul);
|
||||||
|
foreach (['Umschaltfreigabe', 'VorgabeTimeout', 'Tagesplanung'] as $timer) {
|
||||||
|
self::assertStringContainsString("'$timer'", $modul);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"SetTimerInterval('Meldezyklus', \$this->ReadPropertyInteger('Meldeintervall') * 1000)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModulVerwendetAusschliesslichDenVertrag30(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
|
||||||
|
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
|
||||||
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeManagerdaten', $modul);
|
||||||
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeVerbraucherdaten', $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFachvariablenUndRueckmeldestoerungSindEnthalten(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterVariableBoolean('Schaltzustand'",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterVariableInteger('Tageslaufzeit'",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString("'Kennung' => 'Rueckmeldefehler'", $modul);
|
||||||
|
self::assertStringContainsString("'Kennung' => 'Tageslaufzeit_s'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDiagnosevariablenSindOptional(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('DiagnosevariablenAnzeigen')", $modul);
|
||||||
|
self::assertStringContainsString('registriereVerbraucherDiagnose', $modul);
|
||||||
|
self::assertStringContainsString('UnregisterVariable($ident)', $modul);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\WarmwasserRegler;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class WarmwasserReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testLeistungsstufenWerdenValidiertUndAufsteigendSortiert(): void
|
||||||
|
{
|
||||||
|
$stufen = WarmwasserRegler::dekodiereLeistungsstufen(json_encode([
|
||||||
|
['Stufe' => 2, 'Leistung' => 6000, 'Schaltkontakt_Stufe' => 102],
|
||||||
|
['Stufe' => 1, 'Leistung' => 3000, 'Schaltkontakt_Stufe' => 101],
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
['Stufe' => 1, 'Leistung_W' => 3000, 'KontaktID' => 101],
|
||||||
|
['Stufe' => 2, 'Leistung_W' => 6000, 'KontaktID' => 102],
|
||||||
|
],
|
||||||
|
$stufen
|
||||||
|
);
|
||||||
|
self::assertSame([0, 3000, 6000], WarmwasserRegler::leistungswerte($stufen));
|
||||||
|
self::assertSame(2, WarmwasserRegler::stufeFuerLeistung($stufen, 6000));
|
||||||
|
self::assertSame(0, WarmwasserRegler::stufeFuerLeistung($stufen, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoppelteKontakteWerdenAbgewiesen(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
WarmwasserRegler::dekodiereLeistungsstufen(json_encode([
|
||||||
|
['Stufe' => 1, 'Leistung' => 3000, 'Schaltkontakt_Stufe' => 101],
|
||||||
|
['Stufe' => 2, 'Leistung' => 6000, 'Schaltkontakt_Stufe' => 101],
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNichtKonfigurierteSollleistungWirdAbgewiesen(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
WarmwasserRegler::stufeFuerLeistung([
|
||||||
|
['Stufe' => 1, 'Leistung_W' => 3000, 'KontaktID' => 101],
|
||||||
|
], 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPt1VerwendetRegelintervallUndZeitkonstante(): void
|
||||||
|
{
|
||||||
|
self::assertEqualsWithDelta(20.4, WarmwasserRegler::pt1(20.0, 30.0, 5, 120), 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLastwechselRestzeitWirdInSekundenBerechnet(): void
|
||||||
|
{
|
||||||
|
self::assertSame(0, WarmwasserRegler::restzeitBisLastwechsel(0, 100, 5));
|
||||||
|
self::assertSame(5, WarmwasserRegler::restzeitBisLastwechsel(100, 100, 5));
|
||||||
|
self::assertSame(2, WarmwasserRegler::restzeitBisLastwechsel(100, 103, 5));
|
||||||
|
self::assertSame(0, WarmwasserRegler::restzeitBisLastwechsel(100, 105, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnergieWirdInKWhBerechnet(): void
|
||||||
|
{
|
||||||
|
self::assertEqualsWithDelta(3.0, WarmwasserRegler::energieKWh(3000.0, 3600), 0.000001);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThermischePrognose(): void
|
||||||
|
{
|
||||||
|
self::assertTrue(WarmwasserRegler::kannZielErreichen(300.0, 50.0, 60.0, 7200, 3000));
|
||||||
|
self::assertFalse(WarmwasserRegler::kannZielErreichen(300.0, 20.0, 60.0, 3600, 3000));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNaechsterZeitplaneintragIstHeuteOderMorgen(): void
|
||||||
|
{
|
||||||
|
$jetzt = new DateTimeImmutable('2026-09-16 10:00:00', new DateTimeZone('Europe/Zurich'));
|
||||||
|
$eintrag = WarmwasserRegler::naechsterZeitplaneintrag(json_encode([
|
||||||
|
['Uhrzeit' => '08:00', 'Solltemperatur' => 50],
|
||||||
|
['Uhrzeit' => '11:00', 'Solltemperatur' => 55],
|
||||||
|
], JSON_THROW_ON_ERROR), $jetzt);
|
||||||
|
|
||||||
|
self::assertNotNull($eintrag);
|
||||||
|
self::assertSame(
|
||||||
|
(new DateTimeImmutable('2026-09-16 11:00:00', new DateTimeZone('Europe/Zurich')))->getTimestamp(),
|
||||||
|
$eintrag['Zeitpunkt']
|
||||||
|
);
|
||||||
|
self::assertSame(55.0, $eintrag['Solltemperatur']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLegionellenschaltungHebtTemperaturgrenzenZweistufigAn(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[45.0, 60.0],
|
||||||
|
WarmwasserRegler::legionellenTemperaturgrenzen(true, 3 * 86400, 4, 7, 45.0, 60.0, 65.0)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[45.0, 65.0],
|
||||||
|
WarmwasserRegler::legionellenTemperaturgrenzen(true, 4 * 86400, 4, 7, 45.0, 60.0, 65.0)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[65.0, 65.0],
|
||||||
|
WarmwasserRegler::legionellenTemperaturgrenzen(true, 7 * 86400, 4, 7, 45.0, 60.0, 65.0)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[45.0, 60.0],
|
||||||
|
WarmwasserRegler::legionellenTemperaturgrenzen(false, 8 * 86400, 4, 7, 45.0, 60.0, 65.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class WarmwassererwaermerModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
private const MODULPFAD = __DIR__ . '/../Warmwassererwaermer/';
|
||||||
|
|
||||||
|
public function testModulIstVollstaendigUndMetadatenSindGueltig(): void
|
||||||
|
{
|
||||||
|
foreach (['module.json', 'form.json', 'module.php'] as $datei) {
|
||||||
|
self::assertFileExists(self::MODULPFAD . $datei);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadaten = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'module.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
self::assertSame('VerbraucherWarmwassererwaermer', $metadaten['name']);
|
||||||
|
self::assertSame('Enelix', $metadaten['vendor']);
|
||||||
|
self::assertSame(['Wassererwärmer'], $metadaten['aliases']);
|
||||||
|
self::assertSame(3, $metadaten['type']);
|
||||||
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAlle19PropertiesSindRegistriertUndImFormularDokumentiert(): void
|
||||||
|
{
|
||||||
|
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
preg_match_all('/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/', $basis . $modul, $treffer);
|
||||||
|
$properties = array_values(array_unique($treffer[1]));
|
||||||
|
|
||||||
|
self::assertCount(19, $properties);
|
||||||
|
|
||||||
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
self::assertStringContainsString('"name": "' . $property . '"', $formular, $property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegelungIstEreignisbasiertUndSperrtLastwechselFuenfSekunden(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString("RegisterPropertyInteger('LastwechselSperrzeit', 5)", $inhalt);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('Interval'", $inhalt);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('IdleCounterMax'", $inhalt);
|
||||||
|
self::assertStringNotContainsString("ReadPropertyInteger('Interval')", $inhalt);
|
||||||
|
self::assertStringContainsString("'LastwechselFreigabe'", $inhalt);
|
||||||
|
self::assertStringContainsString('LetzteTemperaturberechnung', $inhalt);
|
||||||
|
self::assertStringContainsString('$angebot = [$aktuelleLeistung];', $inhalt);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableBoolean('Idle'", $inhalt);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableInteger('IdleCounter'", $inhalt);
|
||||||
|
self::assertStringNotContainsString('ZustandIdle', $inhalt);
|
||||||
|
self::assertStringContainsString("['Idle', 'IdleCounter']", $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModulVerwendetAusschliesslichDenVertrag30(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
self::assertStringContainsString('implements VerbraucherSchnittstelle', $inhalt);
|
||||||
|
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $inhalt);
|
||||||
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeManagerdaten', $inhalt);
|
||||||
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeVerbraucherdaten', $inhalt);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $inhalt);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $inhalt);
|
||||||
|
self::assertStringContainsString("'AutomatischeVerbraucherZuordnung'", $inhalt);
|
||||||
|
self::assertStringContainsString("'VerbraucherZuordnung'", $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVierzehnDiagnosevariablenWerdenBedarfsgesteuertAngelegtUndGeloescht(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
preg_match('/private const DIAGNOSE_VARIABLEN = \[(.*?)\];/s', $inhalt, $treffer);
|
||||||
|
self::assertArrayHasKey(1, $treffer);
|
||||||
|
preg_match_all('/\'([^\']+)\'/', $treffer[1], $variablen);
|
||||||
|
|
||||||
|
self::assertCount(14, $variablen[1]);
|
||||||
|
foreach (['Istleistung', 'Sollleistung', 'Stoerung', 'Stoertext', 'AktiveStufe', 'BezogeneEnergie'] as $ident) {
|
||||||
|
self::assertContains($ident, $variablen[1]);
|
||||||
|
self::assertStringContainsString("Zustand" . $ident, $inhalt);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('DiagnosevariablenAnzeigen')", $inhalt);
|
||||||
|
self::assertStringContainsString('registriereVerbraucherDiagnose', $inhalt);
|
||||||
|
self::assertStringContainsString('UnregisterVariable($ident)', $inhalt);
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $inhalt);
|
||||||
|
self::assertStringNotContainsString("case 'LogStatus':", $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLegionellentemperaturWirdNurBeiAktiverFunktionAngelegt(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('LegionellenfunktionAktiv')", $inhalt);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterVariableFloat('Legionellentemperatur'",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString("UnregisterVariable('Legionellentemperatur')", $inhalt);
|
||||||
|
self::assertStringContainsString("ZustandLegionellentemperatur", $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormularIstWieGewuenschtGegliedertUndHatKeineAktionsknoepfe(): void
|
||||||
|
{
|
||||||
|
$formular = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'form.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Manager und Zeitverhalten',
|
||||||
|
'Speichereinstellungen',
|
||||||
|
'Erweiterte Speichereinstellungen',
|
||||||
|
'Erweiterte sonstige Einstellungen',
|
||||||
|
],
|
||||||
|
array_column($formular['elements'], 'caption')
|
||||||
|
);
|
||||||
|
self::assertArrayNotHasKey('actions', $formular);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTemperatursollwerteSindGeprueftBedienbar(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
foreach (['Mindesttemperatur', 'Maximaltemperatur', 'Legionellentemperatur'] as $ident) {
|
||||||
|
self::assertStringContainsString("EnableAction('" . $ident . "')", $inhalt);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString('setzeTemperaturSollwert', $inhalt);
|
||||||
|
self::assertStringContainsString('Mindesttemperatur < Maximaltemperatur', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchaltfolgeIstBreakBeforeMake(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
$aus = strpos($inhalt, "RequestAction(\$stufe['KontaktID'], false)");
|
||||||
|
$ein = strpos($inhalt, "RequestAction(\$stufe['KontaktID'], true)");
|
||||||
|
|
||||||
|
self::assertIsInt($aus);
|
||||||
|
self::assertIsInt($ein);
|
||||||
|
self::assertLessThan($ein, $aus);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user