This commit is contained in:
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"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": "Abfrageintervall",
|
||||||
|
"caption": "Geraetestatus abfragen",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Ladestation",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "Select",
|
||||||
|
"name": "Geraetetyp",
|
||||||
|
"caption": "Geraetetyp",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"caption": "Nicht konfiguriert",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "go-e Charger (alte API)",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "go-e Charger Gemini / Gemini flex",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"caption": "smart-me Pico",
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "Geraeteadresse",
|
||||||
|
"caption": "IP-Adresse oder Hostname (go-e)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "GeraeteID",
|
||||||
|
"caption": "Geraete-ID (Pico)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "Seriennummer",
|
||||||
|
"caption": "Seriennummer (Pico)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "Benutzername",
|
||||||
|
"caption": "Benutzername (Pico)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PasswordTextBox",
|
||||||
|
"name": "Passwort",
|
||||||
|
"caption": "Passwort (Pico)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "MaximalerLadestrom",
|
||||||
|
"caption": "Maximaler Ladestrom",
|
||||||
|
"suffix": " A",
|
||||||
|
"minimum": 6,
|
||||||
|
"maximum": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "Ladefreigabe",
|
||||||
|
"caption": "Ladefreigabe beim Start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "Solarladen",
|
||||||
|
"caption": "Solarladen beim Start"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Darstellung und Diagnose",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "EinstellungenInVisu",
|
||||||
|
"caption": "Ladefreigabe und Solarladen in der Visualisierung anzeigen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "DiagnosevariablenAnzeigen",
|
||||||
|
"caption": "Diagnosevariablen anzeigen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "LoggingEin",
|
||||||
|
"caption": "Diagnoseprotokoll aktivieren"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
{
|
||||||
|
"code": 102,
|
||||||
|
"icon": "active",
|
||||||
|
"caption": "Aktiv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 201,
|
||||||
|
"icon": "error",
|
||||||
|
"caption": "Konfiguration ungueltig"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 202,
|
||||||
|
"icon": "error",
|
||||||
|
"caption": "Geraetekommunikation fehlgeschlagen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "{0D94913C-0F31-4C29-A685-6EB4AE58E55D}",
|
||||||
|
"name": "LadestationStandAlone",
|
||||||
|
"type": 3,
|
||||||
|
"vendor": "Enelix",
|
||||||
|
"aliases": [
|
||||||
|
"Ladestation Stand-Alone"
|
||||||
|
],
|
||||||
|
"parentRequirements": [],
|
||||||
|
"childRequirements": [],
|
||||||
|
"implemented": [],
|
||||||
|
"prefix": "ENELIX",
|
||||||
|
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/LadestationStandAlone"
|
||||||
|
}
|
||||||
@@ -0,0 +1,763 @@
|
|||||||
|
<?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/LadestationAdapter.php';
|
||||||
|
require_once __DIR__ . '/../libs/LadestationRegler.php';
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\LadestationAdapter;
|
||||||
|
use Belevo\EnelixEMS\LadestationRegler;
|
||||||
|
use Belevo\EnelixEMS\LadestationTransport;
|
||||||
|
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||||
|
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
||||||
|
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
|
||||||
|
|
||||||
|
class LadestationStandAlone 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_KOMMUNIKATIONSFEHLER = 202;
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const DIAGNOSE_VARIABLEN = [
|
||||||
|
'Istleistung',
|
||||||
|
'Leistungsquelle',
|
||||||
|
'Sollleistung',
|
||||||
|
'SollwertGueltig',
|
||||||
|
'Verfuegbar',
|
||||||
|
'AenderungMoeglich',
|
||||||
|
'Stoerung',
|
||||||
|
'Stoertext',
|
||||||
|
'LetzterGeraetebefehl',
|
||||||
|
'LeistungsangebotDiagnose',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function Create(): void
|
||||||
|
{
|
||||||
|
parent::Create();
|
||||||
|
|
||||||
|
$this->registriereVerbraucherBasis();
|
||||||
|
$this->RegisterPropertyInteger('Geraetetyp', 0);
|
||||||
|
$this->RegisterPropertyString('Geraeteadresse', '');
|
||||||
|
$this->RegisterPropertyString('GeraeteID', '');
|
||||||
|
$this->RegisterPropertyString('Seriennummer', '');
|
||||||
|
$this->RegisterPropertyString('Benutzername', '');
|
||||||
|
$this->RegisterPropertyString('Passwort', '');
|
||||||
|
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
|
||||||
|
$this->RegisterPropertyInteger('Abfrageintervall', 5);
|
||||||
|
$this->RegisterPropertyBoolean('Ladefreigabe', false);
|
||||||
|
$this->RegisterPropertyBoolean('Solarladen', true);
|
||||||
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||||
|
|
||||||
|
// Ausschliesslich fuer den automatisierten Symcon-Funktionstest.
|
||||||
|
$this->RegisterPropertyBoolean('Testmodus', false);
|
||||||
|
$this->RegisterPropertyString('Testantwort', '');
|
||||||
|
|
||||||
|
$this->RegisterVariableBoolean('FahrzeugVerbunden', 'Fahrzeug verbunden', '~Switch', 100);
|
||||||
|
$this->RegisterVariableBoolean('FahrzeugGeladen', 'Fahrzeug geladen', '~Switch', 110);
|
||||||
|
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
|
||||||
|
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
|
||||||
|
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
|
||||||
|
|
||||||
|
$this->RegisterAttributeBoolean('InitialwerteGesetzt', false);
|
||||||
|
$this->RegisterAttributeBoolean('KonfigurationLadefreigabe', false);
|
||||||
|
$this->RegisterAttributeBoolean('KonfigurationSolarladen', true);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandLadefreigabe', false);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandSolarladen', true);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandFahrzeugVerbunden', false);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandFahrzeugGeladen', false);
|
||||||
|
$this->RegisterAttributeInteger('ZustandFahrzeugstatus', 0);
|
||||||
|
$this->RegisterAttributeFloat('ZustandLadestrom', 0.0);
|
||||||
|
$this->RegisterAttributeInteger('ZustandPhasenzahl', 0);
|
||||||
|
$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->RegisterAttributeString('Leistungsangebot', '[0]');
|
||||||
|
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
|
||||||
|
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzterGesetzterStrom', -1);
|
||||||
|
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'Statuszyklus',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Statuszyklus', false);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'Meldezyklus',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'RueckmeldungVerzoegert',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'VorgabeTimeout',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ApplyChanges(): void
|
||||||
|
{
|
||||||
|
parent::ApplyChanges();
|
||||||
|
|
||||||
|
$this->initialisiereLokaleEinstellungen();
|
||||||
|
$this->aktualisiereVariablen();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pruefeKonfiguration();
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->deaktiviereTimer();
|
||||||
|
$this->setzeZustand('Verfuegbar', false);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', false);
|
||||||
|
$this->setzeStoerung($fehler->getMessage());
|
||||||
|
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||||
|
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->SetTimerInterval('Statuszyklus', $this->ReadPropertyInteger('Abfrageintervall') * 1000);
|
||||||
|
$this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000);
|
||||||
|
$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->aktualisiere(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'Ladefreigabe':
|
||||||
|
case 'Solarladen':
|
||||||
|
$this->WriteAttributeBoolean('Zustand' . $ident, (bool) $wert);
|
||||||
|
$this->setzeSichtbareVariable($ident, (bool) $wert);
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->aktualisiere(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'Statuszyklus':
|
||||||
|
$this->aktualisiere(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'VorgabeTimeout':
|
||||||
|
$this->SetTimerInterval('VorgabeTimeout', 0);
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->aktualisiere(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'Melden':
|
||||||
|
if ((bool) $wert) {
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||||
|
}
|
||||||
|
$this->aktualisiere(false);
|
||||||
|
$this->sendeVerbraucherdaten();
|
||||||
|
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 diese Ladestation nicht aktiv zugeordnet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||||
|
$this->aktualisiere(false, false);
|
||||||
|
$sollleistung = $daten['Sollleistung_W'];
|
||||||
|
if ($sollleistung === null) {
|
||||||
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||||
|
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->aktualisiere(false, false);
|
||||||
|
}
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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->SetTimerInterval('VorgabeTimeout', $this->ReadPropertyInteger('VorgabeTimeout') * 1000);
|
||||||
|
$this->aktualisiere(false, false);
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiere(bool $meldungPlanen, bool $statusLesen = true): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($statusLesen) {
|
||||||
|
$this->leseGeraetestatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
$aktiv = (bool) $this->GetValue('Aktiv');
|
||||||
|
$ladefreigabe = $this->ReadAttributeBoolean('ZustandLadefreigabe');
|
||||||
|
$verbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
|
||||||
|
$geladen = $this->ReadAttributeBoolean('ZustandFahrzeugGeladen');
|
||||||
|
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
|
||||||
|
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
|
||||||
|
$solarladen = $this->ReadAttributeBoolean('ZustandSolarladen');
|
||||||
|
$peakbetrieb = $this->ReadAttributeString('Betriebsart')
|
||||||
|
=== Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||||
|
$verfuegbar = LadestationRegler::leistungsangebot(
|
||||||
|
$aktiv,
|
||||||
|
$ladefreigabe,
|
||||||
|
$verbunden,
|
||||||
|
$geladen,
|
||||||
|
$phasenzahl,
|
||||||
|
$maximalstrom,
|
||||||
|
$solarladen,
|
||||||
|
false
|
||||||
|
) !== [0];
|
||||||
|
$angebot = LadestationRegler::leistungsangebot(
|
||||||
|
$aktiv,
|
||||||
|
$ladefreigabe,
|
||||||
|
$verbunden,
|
||||||
|
$geladen,
|
||||||
|
$phasenzahl,
|
||||||
|
$maximalstrom,
|
||||||
|
$solarladen,
|
||||||
|
$peakbetrieb
|
||||||
|
);
|
||||||
|
$angebotJson = json_encode($angebot, JSON_THROW_ON_ERROR);
|
||||||
|
$this->WriteAttributeString('Leistungsangebot', $angebotJson);
|
||||||
|
$this->setzeSichtbareVariable('LeistungsangebotDiagnose', $angebotJson);
|
||||||
|
$this->setzeZustand('Verfuegbar', $verfuegbar);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
|
||||||
|
|
||||||
|
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
|
||||||
|
$this->setzeLadestrom($sollleistung);
|
||||||
|
$this->setzeStoerung('');
|
||||||
|
$this->SetStatus(self::STATUS_AKTIV);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->setzeZustand('Verfuegbar', false);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', false);
|
||||||
|
$this->setzeStoerung($fehler->getMessage());
|
||||||
|
$this->SetStatus(self::STATUS_KOMMUNIKATIONSFEHLER);
|
||||||
|
$this->protokolliere('Geraetekommunikation', $fehler->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($meldungPlanen) {
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function leseGeraetestatus(): void
|
||||||
|
{
|
||||||
|
$transport = new LadestationTransport(
|
||||||
|
fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage)
|
||||||
|
);
|
||||||
|
$antwort = $transport->statusLesen(
|
||||||
|
$this->ReadPropertyInteger('Geraetetyp'),
|
||||||
|
$this->geraetekonfiguration()
|
||||||
|
);
|
||||||
|
$rohstatus = LadestationAdapter::dekodiereStatus(
|
||||||
|
$this->ReadPropertyInteger('Geraetetyp'),
|
||||||
|
$antwort
|
||||||
|
);
|
||||||
|
$status = LadestationRegler::normalisiereStatus(
|
||||||
|
$rohstatus,
|
||||||
|
$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden')
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($status as $ident => $wert) {
|
||||||
|
$this->setzeZustand($ident, $wert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<int> $angebot */
|
||||||
|
private function bestimmeWirksameSollleistung(array $angebot): int
|
||||||
|
{
|
||||||
|
if ((bool) $this->leseZustand('SollwertGueltig')) {
|
||||||
|
$sollleistung = (int) $this->leseZustand('Sollleistung');
|
||||||
|
if (in_array($sollleistung, $angebot, true)) {
|
||||||
|
return $sollleistung;
|
||||||
|
}
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->ReadAttributeBoolean('ZustandSolarladen') && $angebot !== [0]) {
|
||||||
|
return $angebot[count($angebot) - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setzeLadestrom(int $leistung): void
|
||||||
|
{
|
||||||
|
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
|
||||||
|
$strom = $leistung === 0
|
||||||
|
? 0
|
||||||
|
: LadestationRegler::stromFuerLeistung($leistung, $phasenzahl);
|
||||||
|
if ($strom === $this->ReadAttributeInteger('LetzterGesetzterStrom')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transport = new LadestationTransport(
|
||||||
|
fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage)
|
||||||
|
);
|
||||||
|
$transport->ladestromSetzen(
|
||||||
|
$this->ReadPropertyInteger('Geraetetyp'),
|
||||||
|
$this->geraetekonfiguration(),
|
||||||
|
$strom
|
||||||
|
);
|
||||||
|
$this->WriteAttributeInteger('LetzterGesetzterStrom', $strom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $anfrage
|
||||||
|
*/
|
||||||
|
private function fuehreHttpAnfrageAus(array $anfrage): string
|
||||||
|
{
|
||||||
|
$protokoll = [
|
||||||
|
'Methode' => $anfrage['Methode'],
|
||||||
|
'URL' => $anfrage['URL'],
|
||||||
|
];
|
||||||
|
$this->setzeSichtbareVariable(
|
||||||
|
'LetzterGeraetebefehl',
|
||||||
|
json_encode($protokoll, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||||
|
$url = (string) $anfrage['URL'];
|
||||||
|
if (str_ends_with($url, '/api/status')
|
||||||
|
|| str_ends_with($url, '/mqtt?payload=')
|
||||||
|
|| str_contains($url, '/pico/charging/')
|
||||||
|
) {
|
||||||
|
return $this->ReadPropertyString('Testantwort');
|
||||||
|
}
|
||||||
|
return '{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
$curl = curl_init();
|
||||||
|
if ($curl === false) {
|
||||||
|
throw new RuntimeException('HTTP-Anfrage konnte nicht initialisiert werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt($curl, CURLOPT_URL, (string) $anfrage['URL']);
|
||||||
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
|
||||||
|
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
|
||||||
|
if ($anfrage['Methode'] === 'POST') {
|
||||||
|
curl_setopt($curl, CURLOPT_POST, true);
|
||||||
|
}
|
||||||
|
if (isset($anfrage['Benutzername'], $anfrage['Passwort'])) {
|
||||||
|
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||||
|
curl_setopt(
|
||||||
|
$curl,
|
||||||
|
CURLOPT_USERPWD,
|
||||||
|
(string) $anfrage['Benutzername'] . ':' . (string) $anfrage['Passwort']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$antwort = curl_exec($curl);
|
||||||
|
if ($antwort === false) {
|
||||||
|
$fehler = curl_error($curl);
|
||||||
|
curl_close($curl);
|
||||||
|
throw new RuntimeException('HTTP-Anfrage fehlgeschlagen: ' . $fehler);
|
||||||
|
}
|
||||||
|
$status = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($curl);
|
||||||
|
if ($status >= 400) {
|
||||||
|
throw new RuntimeException('Geraet antwortet mit HTTP-Status ' . $status . '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $antwort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
private function geraetekonfiguration(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Geraeteadresse' => $this->ReadPropertyString('Geraeteadresse'),
|
||||||
|
'GeraeteID' => $this->ReadPropertyString('GeraeteID'),
|
||||||
|
'Seriennummer' => $this->ReadPropertyString('Seriennummer'),
|
||||||
|
'Benutzername' => $this->ReadPropertyString('Benutzername'),
|
||||||
|
'Passwort' => $this->ReadPropertyString('Passwort'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$stoerung = (bool) $this->leseZustand('Stoerung');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Kopf' => [
|
||||||
|
'Version' => Nachrichtenvertrag::VERSION,
|
||||||
|
'AbsenderID' => $this->InstanceID,
|
||||||
|
'EmpfaengerID' => $managerID,
|
||||||
|
'Zeitpunkt' => time(),
|
||||||
|
],
|
||||||
|
'Betriebsart' => $this->ReadAttributeString('Betriebsart'),
|
||||||
|
'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_GEMESSEN,
|
||||||
|
'Zustand' => [
|
||||||
|
[
|
||||||
|
'Kennung' => 'Sollleistung_W',
|
||||||
|
'Art' => 'Sollwert',
|
||||||
|
'Wert' => (bool) $this->leseZustand('SollwertGueltig')
|
||||||
|
? (int) $this->leseZustand('Sollleistung')
|
||||||
|
: null,
|
||||||
|
'Einheit' => 'W',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'FahrzeugVerbunden',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => (bool) $this->leseZustand('FahrzeugVerbunden'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'FahrzeugGeladen',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => (bool) $this->leseZustand('FahrzeugGeladen'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Fahrzeugstatus',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => (int) $this->leseZustand('Fahrzeugstatus'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Phasenzahl',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => (int) $this->leseZustand('Phasenzahl'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Ladestrom_A',
|
||||||
|
'Art' => 'Istwert',
|
||||||
|
'Wert' => (float) $this->leseZustand('Ladestrom'),
|
||||||
|
'Einheit' => 'A',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Ladefreigabe',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => $this->ReadAttributeBoolean('ZustandLadefreigabe'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Solarladen',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => $this->ReadAttributeBoolean('ZustandSolarladen'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Ladefehler',
|
||||||
|
'Art' => 'Stoerung',
|
||||||
|
'Wert' => $stoerung,
|
||||||
|
'Einheit' => '',
|
||||||
|
'Text' => (string) $this->leseZustand('Stoertext'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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', 'Abfrageintervall'] as $property) {
|
||||||
|
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||||
|
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
|
||||||
|
if ($maximalstrom < 6 || $maximalstrom > 32) {
|
||||||
|
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$geraetetyp = $this->ReadPropertyInteger('Geraetetyp');
|
||||||
|
$konfiguration = $this->geraetekonfiguration();
|
||||||
|
LadestationAdapter::statusAnfrage($geraetetyp, $konfiguration);
|
||||||
|
LadestationAdapter::steuerAnfragen($geraetetyp, $konfiguration, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initialisiereLokaleEinstellungen(): void
|
||||||
|
{
|
||||||
|
$initialisiert = $this->ReadAttributeBoolean('InitialwerteGesetzt');
|
||||||
|
$ladefreigabe = $this->ReadPropertyBoolean('Ladefreigabe');
|
||||||
|
$solarladen = $this->ReadPropertyBoolean('Solarladen');
|
||||||
|
|
||||||
|
if (!$initialisiert
|
||||||
|
|| $ladefreigabe !== $this->ReadAttributeBoolean('KonfigurationLadefreigabe')
|
||||||
|
) {
|
||||||
|
$this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
|
||||||
|
}
|
||||||
|
if (!$initialisiert
|
||||||
|
|| $solarladen !== $this->ReadAttributeBoolean('KonfigurationSolarladen')
|
||||||
|
) {
|
||||||
|
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeBoolean('KonfigurationLadefreigabe', $ladefreigabe);
|
||||||
|
$this->WriteAttributeBoolean('KonfigurationSolarladen', $solarladen);
|
||||||
|
$this->WriteAttributeBoolean('InitialwerteGesetzt', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiereVariablen(): void
|
||||||
|
{
|
||||||
|
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
|
||||||
|
$this->RegisterVariableBoolean('Ladefreigabe', 'Ladefreigabe', '~Switch', 150);
|
||||||
|
$this->RegisterVariableBoolean('Solarladen', 'Solarladen', '~Switch', 160);
|
||||||
|
$this->EnableAction('Ladefreigabe');
|
||||||
|
$this->EnableAction('Solarladen');
|
||||||
|
$this->SetValue('Ladefreigabe', $this->ReadAttributeBoolean('ZustandLadefreigabe'));
|
||||||
|
$this->SetValue('Solarladen', $this->ReadAttributeBoolean('ZustandSolarladen'));
|
||||||
|
} else {
|
||||||
|
$this->entferneVariable('Ladefreigabe');
|
||||||
|
$this->entferneVariable('Solarladen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||||
|
$this->registriereVerbraucherDiagnose();
|
||||||
|
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 200);
|
||||||
|
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 210);
|
||||||
|
$this->SetValue(
|
||||||
|
'LeistungsangebotDiagnose',
|
||||||
|
$this->ReadAttributeString('Leistungsangebot')
|
||||||
|
);
|
||||||
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
|
if (!in_array($ident, ['LetzterGeraetebefehl', 'LeistungsangebotDiagnose'], true)) {
|
||||||
|
$this->setzeSichtbareVariable($ident, $this->leseZustand($ident));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
|
$this->entferneVariable($ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entferneVariable(string $ident): void
|
||||||
|
{
|
||||||
|
$id = @$this->GetIDForIdent($ident);
|
||||||
|
if (is_int($id) && $id > 0 && IPS_VariableExists($id)) {
|
||||||
|
$this->UnregisterVariable($ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param mixed $wert */
|
||||||
|
private function setzeZustand(string $ident, $wert): void
|
||||||
|
{
|
||||||
|
switch ($ident) {
|
||||||
|
case 'FahrzeugVerbunden':
|
||||||
|
case 'FahrzeugGeladen':
|
||||||
|
case 'SollwertGueltig':
|
||||||
|
case 'Verfuegbar':
|
||||||
|
case 'AenderungMoeglich':
|
||||||
|
case 'Stoerung':
|
||||||
|
$this->WriteAttributeBoolean('Zustand' . $ident, (bool) $wert);
|
||||||
|
break;
|
||||||
|
case 'Fahrzeugstatus':
|
||||||
|
case 'Phasenzahl':
|
||||||
|
case 'Sollleistung':
|
||||||
|
$this->WriteAttributeInteger('Zustand' . $ident, (int) $wert);
|
||||||
|
break;
|
||||||
|
case 'Ladestrom_A':
|
||||||
|
$ident = 'Ladestrom';
|
||||||
|
$this->WriteAttributeFloat('ZustandLadestrom', (float) $wert);
|
||||||
|
break;
|
||||||
|
case 'Istleistung_W':
|
||||||
|
$ident = 'Istleistung';
|
||||||
|
$this->WriteAttributeFloat('ZustandIstleistung', (float) $wert);
|
||||||
|
break;
|
||||||
|
case 'Stoertext':
|
||||||
|
$this->WriteAttributeString('ZustandStoertext', (string) $wert);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new LogicException('Unbekannter Zustand: ' . $ident);
|
||||||
|
}
|
||||||
|
$this->setzeSichtbareVariable($ident, $wert);
|
||||||
|
if ($ident === 'Istleistung') {
|
||||||
|
$this->setzeSichtbareVariable(
|
||||||
|
'Leistungsquelle',
|
||||||
|
Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
private function leseZustand(string $ident)
|
||||||
|
{
|
||||||
|
if ($ident === 'Ladefreigabe' || $ident === 'Solarladen') {
|
||||||
|
return $this->ReadAttributeBoolean('Zustand' . $ident);
|
||||||
|
}
|
||||||
|
if (in_array($ident, [
|
||||||
|
'FahrzeugVerbunden', 'FahrzeugGeladen', 'SollwertGueltig',
|
||||||
|
'Verfuegbar', 'AenderungMoeglich', 'Stoerung',
|
||||||
|
], true)) {
|
||||||
|
return $this->ReadAttributeBoolean('Zustand' . $ident);
|
||||||
|
}
|
||||||
|
if (in_array($ident, ['Fahrzeugstatus', 'Phasenzahl', 'Sollleistung'], true)) {
|
||||||
|
return $this->ReadAttributeInteger('Zustand' . $ident);
|
||||||
|
}
|
||||||
|
if ($ident === 'Ladestrom' || $ident === 'Istleistung') {
|
||||||
|
return $this->ReadAttributeFloat('Zustand' . $ident);
|
||||||
|
}
|
||||||
|
if ($ident === 'Stoertext') {
|
||||||
|
return $this->ReadAttributeString('ZustandStoertext');
|
||||||
|
}
|
||||||
|
if ($ident === 'Leistungsquelle') {
|
||||||
|
return Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN;
|
||||||
|
}
|
||||||
|
throw new LogicException('Unbekannter Zustand: ' . $ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param mixed $wert */
|
||||||
|
private function setzeSichtbareVariable(string $ident, $wert): void
|
||||||
|
{
|
||||||
|
$id = @$this->GetIDForIdent($ident);
|
||||||
|
if (is_int($id) && $id > 0 && IPS_VariableExists($id)) {
|
||||||
|
$this->SetValue($ident, $wert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
private function leseLeistungsangebot(): array
|
||||||
|
{
|
||||||
|
$angebot = json_decode(
|
||||||
|
$this->ReadAttributeString('Leistungsangebot'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
return is_array($angebot) ? array_map('intval', $angebot) : [0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setzeStoerung(string $text): void
|
||||||
|
{
|
||||||
|
$this->setzeZustand('Stoerung', $text !== '');
|
||||||
|
$this->setzeZustand('Stoertext', $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deaktiviereTimer(): void
|
||||||
|
{
|
||||||
|
foreach (['Statuszyklus', 'Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) {
|
||||||
|
$this->SetTimerInterval($timer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function protokolliere(string $bereich, string $nachricht): void
|
||||||
|
{
|
||||||
|
if (!$this->ReadPropertyBoolean('LoggingEin')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->SendDebug($bereich, $nachricht, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
-17
@@ -24,6 +24,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
private const VERBRAUCHER_MODUL_IDS = [
|
private const VERBRAUCHER_MODUL_IDS = [
|
||||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
||||||
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||||
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
|
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
|
||||||
];
|
];
|
||||||
private const MONATSNAMEN = [
|
private const MONATSNAMEN = [
|
||||||
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
||||||
@@ -357,25 +359,44 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stoerungen = [];
|
|
||||||
$verbraucher = $this->aktuelleVerbraucher($stoerungen);
|
|
||||||
$lastspitzengrenze = ManagerRegler::wirksameLastspitzengrenze(
|
$lastspitzengrenze = ManagerRegler::wirksameLastspitzengrenze(
|
||||||
$this->ReadPropertyInteger('Lastspitzenmodus'),
|
$this->ReadPropertyInteger('Lastspitzenmodus'),
|
||||||
$this->ReadPropertyFloat('Lastspitzengrenze'),
|
$this->ReadPropertyFloat('Lastspitzengrenze'),
|
||||||
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')),
|
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')),
|
||||||
(int) date('n')
|
(int) date('n')
|
||||||
);
|
);
|
||||||
|
$bisherigeBetriebsart = (string) $this->GetValue('Betriebsart');
|
||||||
$betriebsart = ManagerRegler::bestimmeBetriebsart(
|
$betriebsart = ManagerRegler::bestimmeBetriebsart(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
$netzleistung,
|
$netzleistung,
|
||||||
(string) $this->GetValue('Betriebsart'),
|
$bisherigeBetriebsart,
|
||||||
$lastspitzengrenze !== null,
|
$lastspitzengrenze !== null,
|
||||||
$this->ReadPropertyFloat('SollwertSolarladen'),
|
$this->ReadPropertyFloat('SollwertSolarladen'),
|
||||||
$lastspitzengrenze ?? 0.0,
|
$lastspitzengrenze ?? 0.0,
|
||||||
$this->ReadPropertyFloat('Umschaltdifferenz')
|
$this->ReadPropertyFloat('Umschaltdifferenz')
|
||||||
);
|
);
|
||||||
|
$this->SetValue('Betriebsart', $betriebsart);
|
||||||
|
if ($betriebsart !== $bisherigeBetriebsart) {
|
||||||
|
$this->WriteAttributeString('LetzteSollwerte', '{}');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stoerungen = [];
|
||||||
|
$synchronisiert = true;
|
||||||
|
$verbraucher = $this->aktuelleVerbraucher($stoerungen, $betriebsart, $synchronisiert);
|
||||||
|
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
|
||||||
|
if (!$synchronisiert) {
|
||||||
|
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||||
|
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
||||||
|
'Betriebsart' => $betriebsart,
|
||||||
|
'Synchronisiert' => false,
|
||||||
|
'Verbraucher' => $verbraucher,
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
$this->aktualisiereStoerungen($verbraucher, $stoerungen);
|
||||||
|
$this->SetStatus(self::STATUS_AKTIV);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
|
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
|
||||||
? (float) $lastspitzengrenze
|
? (float) $lastspitzengrenze
|
||||||
: $this->ReadPropertyFloat('SollwertSolarladen');
|
: $this->ReadPropertyFloat('SollwertSolarladen');
|
||||||
@@ -385,12 +406,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$verbraucher,
|
$verbraucher,
|
||||||
$betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV'
|
$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('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
|
||||||
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
||||||
'Betriebsart' => $betriebsart,
|
'Betriebsart' => $betriebsart,
|
||||||
|
'Synchronisiert' => true,
|
||||||
'Restabweichung_W' => $ergebnis['Restabweichung_W'],
|
'Restabweichung_W' => $ergebnis['Restabweichung_W'],
|
||||||
'Verbraucher' => $verbraucher,
|
'Verbraucher' => $verbraucher,
|
||||||
], JSON_THROW_ON_ERROR));
|
], JSON_THROW_ON_ERROR));
|
||||||
@@ -401,7 +420,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$this->sendeSollwert((int) $instanzID, $sollleistung);
|
$this->sendeManagerdaten((int) $instanzID, $betriebsart, $sollleistung);
|
||||||
$letzteSollwerte[(string) $instanzID] = $sollleistung;
|
$letzteSollwerte[(string) $instanzID] = $sollleistung;
|
||||||
} catch (Throwable $fehler) {
|
} catch (Throwable $fehler) {
|
||||||
$stoerungen[] = 'Sollwert an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
|
$stoerungen[] = 'Sollwert an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
|
||||||
@@ -437,8 +456,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
* @param list<string> $stoerungen
|
* @param list<string> $stoerungen
|
||||||
* @return list<array{InstanzID: int, Daten: array<string, mixed>}>
|
* @return list<array{InstanzID: int, Daten: array<string, mixed>}>
|
||||||
*/
|
*/
|
||||||
private function aktuelleVerbraucher(array &$stoerungen): array
|
private function aktuelleVerbraucher(
|
||||||
{
|
array &$stoerungen,
|
||||||
|
string $betriebsart,
|
||||||
|
bool &$synchronisiert
|
||||||
|
): array {
|
||||||
$ids = $this->aktiveVerbraucherIDs();
|
$ids = $this->aktiveVerbraucherIDs();
|
||||||
$this->setzeDiagnosewert('VerbraucherAnzahl', count($ids));
|
$this->setzeDiagnosewert('VerbraucherAnzahl', count($ids));
|
||||||
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
||||||
@@ -447,12 +469,23 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
|
|
||||||
foreach ($ids as $instanzID) {
|
foreach ($ids as $instanzID) {
|
||||||
$eintrag = $cache[(string) $instanzID] ?? null;
|
$eintrag = $cache[(string) $instanzID] ?? null;
|
||||||
|
$grund = '';
|
||||||
if (!is_array($eintrag) || !isset($eintrag['EmpfangenAm'], $eintrag['Daten'])) {
|
if (!is_array($eintrag) || !isset($eintrag['EmpfangenAm'], $eintrag['Daten'])) {
|
||||||
$stoerungen[] = 'Noch keine Meldung von Instanz ' . $instanzID . '.';
|
$grund = 'Noch keine Meldung';
|
||||||
continue;
|
} elseif (time() - (int) $eintrag['EmpfangenAm'] > $timeout) {
|
||||||
|
$grund = 'Meldung ist veraltet';
|
||||||
|
} elseif (($eintrag['Daten']['Betriebsart'] ?? null) !== $betriebsart) {
|
||||||
|
$grund = 'Leistungsangebot gehoert nicht zur Betriebsart ' . $betriebsart;
|
||||||
}
|
}
|
||||||
if (time() - (int) $eintrag['EmpfangenAm'] > $timeout) {
|
|
||||||
$stoerungen[] = 'Meldung von Instanz ' . $instanzID . ' ist veraltet.';
|
if ($grund !== '') {
|
||||||
|
$synchronisiert = false;
|
||||||
|
$stoerungen[] = $grund . ' von Instanz ' . $instanzID . '.';
|
||||||
|
try {
|
||||||
|
$this->sendeManagerdaten($instanzID, $betriebsart, null);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$stoerungen[] = 'Betriebsart an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$aktuell[] = ['InstanzID' => $instanzID, 'Daten' => $eintrag['Daten']];
|
$aktuell[] = ['InstanzID' => $instanzID, 'Daten' => $eintrag['Daten']];
|
||||||
@@ -624,8 +657,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendeSollwert(int $instanzID, int $sollleistung): void
|
private function sendeManagerdaten(
|
||||||
{
|
int $instanzID,
|
||||||
|
string $betriebsart,
|
||||||
|
?int $sollleistung
|
||||||
|
): void {
|
||||||
if (!IPS_InstanceExists($instanzID)) {
|
if (!IPS_InstanceExists($instanzID)) {
|
||||||
throw new RuntimeException('Instanz existiert nicht.');
|
throw new RuntimeException('Instanz existiert nicht.');
|
||||||
}
|
}
|
||||||
@@ -636,11 +672,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
'EmpfaengerID' => $instanzID,
|
'EmpfaengerID' => $instanzID,
|
||||||
'Zeitpunkt' => time(),
|
'Zeitpunkt' => time(),
|
||||||
],
|
],
|
||||||
|
'Betriebsart' => $betriebsart,
|
||||||
'Sollleistung_W' => $sollleistung,
|
'Sollleistung_W' => $sollleistung,
|
||||||
];
|
];
|
||||||
Nachrichtenvertrag::pruefeManagerdaten($paket);
|
Nachrichtenvertrag::pruefeManagerdaten($paket);
|
||||||
IPS_RequestAction($instanzID, 'ManagerdatenEmpfangen', json_encode($paket, JSON_THROW_ON_ERROR));
|
IPS_RequestAction($instanzID, 'ManagerdatenEmpfangen', json_encode($paket, JSON_THROW_ON_ERROR));
|
||||||
$this->protokolliere('Sollwert', ['InstanzID' => $instanzID, 'Sollleistung_W' => $sollleistung]);
|
$this->protokolliere('Managerdaten', [
|
||||||
|
'InstanzID' => $instanzID,
|
||||||
|
'Betriebsart' => $betriebsart,
|
||||||
|
'Sollleistung_W' => $sollleistung,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"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": "LastwechselSperrzeit", "caption": "Lastwechsel-Sperrzeit", "suffix": " s", "minimum": 0}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Pufferspeicher",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "List",
|
||||||
|
"name": "LeistungsStufen",
|
||||||
|
"caption": "Leistungsstufen",
|
||||||
|
"add": true,
|
||||||
|
"delete": 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": "300px",
|
||||||
|
"add": 0,
|
||||||
|
"edit": {"type": "SelectVariable", "validVariableTypes": [0]}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "PufferfuehlerVariableID",
|
||||||
|
"caption": "Puffertemperatur",
|
||||||
|
"validVariableTypes": [1, 2]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "AussentemperaturVariableID",
|
||||||
|
"caption": "Aussentemperatur",
|
||||||
|
"validVariableTypes": [1, 2]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Heizkurve",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "FusspunktVorlauftemperatur",
|
||||||
|
"caption": "Vorlauftemperatur bei 20 Grad C aussen",
|
||||||
|
"suffix": " Grad C",
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "HeizkurvenSteigung",
|
||||||
|
"caption": "Heizkurvensteigung",
|
||||||
|
"minimum": 0,
|
||||||
|
"digits": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "HeizkurveMinimaltemperatur",
|
||||||
|
"caption": "Untere Begrenzung der Solltemperatur",
|
||||||
|
"suffix": " Grad C",
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "HeizkurveMaximaltemperatur",
|
||||||
|
"caption": "Obere Begrenzung der Solltemperatur",
|
||||||
|
"suffix": " Grad C",
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Hysterese",
|
||||||
|
"caption": "Hysterese",
|
||||||
|
"suffix": " K",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Select",
|
||||||
|
"name": "MindesttemperaturModus",
|
||||||
|
"caption": "Optionale Mindesttemperatur",
|
||||||
|
"options": [
|
||||||
|
{"caption": "Aus", "value": 0},
|
||||||
|
{"caption": "Statisch", "value": 1},
|
||||||
|
{"caption": "Differenz zur Solltemperatur", "value": 2}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Mindesttemperatur",
|
||||||
|
"caption": "Statische Mindesttemperatur",
|
||||||
|
"suffix": " Grad C",
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "MindesttemperaturDifferenz",
|
||||||
|
"caption": "Differenz zur Solltemperatur",
|
||||||
|
"suffix": " K",
|
||||||
|
"minimum": 0,
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "WaermepumpenSolltemperaturVariableID",
|
||||||
|
"caption": "Optionale Solltemperatur der Waermepumpe",
|
||||||
|
"validVariableTypes": [1, 2]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "HeizkurveVonWaermepumpeUebernehmen",
|
||||||
|
"caption": "Wert von Waermepumpe uebernehmen",
|
||||||
|
"onClick": "IPS_RequestAction($id, 'HeizkurveVonWaermepumpeUebernehmen', 0);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Erweiterte Einstellungen",
|
||||||
|
"items": [
|
||||||
|
{"type": "NumberSpinner", "name": "TemperaturMaxAlter", "caption": "Maximales Temperaturalter", "suffix": " s", "minimum": 1},
|
||||||
|
{"type": "CheckBox", "name": "PuffertemperaturGlaetten", "caption": "Puffertemperatur glaetten"},
|
||||||
|
{"type": "NumberSpinner", "name": "ZeitKonstante", "caption": "PT1-Zeitkonstante", "suffix": " s", "minimum": 1},
|
||||||
|
{"type": "CheckBox", "name": "EinstellungenInVisu", "caption": "Einstellungen in der Visualisierung anzeigen"},
|
||||||
|
{"type": "CheckBox", "name": "DiagnosevariablenAnzeigen", "caption": "Diagnosevariablen anzeigen"},
|
||||||
|
{"type": "CheckBox", "name": "LoggingEin", "caption": "Diagnoseprotokoll aktivieren"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
{"code": 102, "icon": "active", "caption": "Aktiv"},
|
||||||
|
{"code": 201, "icon": "error", "caption": "Temperaturmessung ungueltig"},
|
||||||
|
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"},
|
||||||
|
{"code": 203, "icon": "error", "caption": "Schaltfehler"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}",
|
||||||
|
"name": "VerbraucherPufferspeicher",
|
||||||
|
"type": 3,
|
||||||
|
"vendor": "Enelix",
|
||||||
|
"aliases": [
|
||||||
|
"Pufferspeicher"
|
||||||
|
],
|
||||||
|
"parentRequirements": [],
|
||||||
|
"childRequirements": [],
|
||||||
|
"implemented": [],
|
||||||
|
"prefix": "ENELIX",
|
||||||
|
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/Pufferspeicher"
|
||||||
|
}
|
||||||
@@ -0,0 +1,899 @@
|
|||||||
|
<?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/PufferspeicherRegler.php';
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||||
|
use Belevo\EnelixEMS\PufferspeicherRegler;
|
||||||
|
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
||||||
|
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
|
||||||
|
|
||||||
|
class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnittstelle
|
||||||
|
{
|
||||||
|
use VerbraucherBasisTrait;
|
||||||
|
|
||||||
|
private const MANAGER_MODULE_ID = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
||||||
|
private const STATUS_AKTIV = 102;
|
||||||
|
private const STATUS_TEMPERATUR_UNGUELTIG = 201;
|
||||||
|
private const STATUS_KONFIGURATION_UNGUELTIG = 202;
|
||||||
|
private const STATUS_SCHALTFEHLER = 203;
|
||||||
|
private const VM_UPDATE = 10603;
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const DIAGNOSE_VARIABLEN = [
|
||||||
|
'Istleistung',
|
||||||
|
'Leistungsquelle',
|
||||||
|
'Sollleistung',
|
||||||
|
'SollwertGueltig',
|
||||||
|
'Verfuegbar',
|
||||||
|
'AenderungMoeglich',
|
||||||
|
'Stoerung',
|
||||||
|
'Stoertext',
|
||||||
|
'AktiveStufe',
|
||||||
|
'BezogeneEnergie',
|
||||||
|
'TemperaturenGueltig',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function Create(): void
|
||||||
|
{
|
||||||
|
parent::Create();
|
||||||
|
|
||||||
|
$this->registriereVerbraucherBasis();
|
||||||
|
$this->RegisterPropertyString('LeistungsStufen', '[]');
|
||||||
|
$this->RegisterPropertyInteger('PufferfuehlerVariableID', 0);
|
||||||
|
$this->RegisterPropertyInteger('AussentemperaturVariableID', 0);
|
||||||
|
$this->RegisterPropertyInteger('WaermepumpenSolltemperaturVariableID', 0);
|
||||||
|
$this->RegisterPropertyFloat('FusspunktVorlauftemperatur', 35.0);
|
||||||
|
$this->RegisterPropertyFloat('HeizkurvenSteigung', 1.0);
|
||||||
|
$this->RegisterPropertyFloat('HeizkurveMinimaltemperatur', 20.0);
|
||||||
|
$this->RegisterPropertyFloat('HeizkurveMaximaltemperatur', 80.0);
|
||||||
|
$this->RegisterPropertyFloat('Hysterese', 5.0);
|
||||||
|
$this->RegisterPropertyInteger('MindesttemperaturModus', PufferspeicherRegler::MINIMUM_AUS);
|
||||||
|
$this->RegisterPropertyFloat('Mindesttemperatur', 20.0);
|
||||||
|
$this->RegisterPropertyFloat('MindesttemperaturDifferenz', 5.0);
|
||||||
|
$this->RegisterPropertyInteger('LastwechselSperrzeit', 5);
|
||||||
|
$this->RegisterPropertyInteger('TemperaturMaxAlter', 120);
|
||||||
|
$this->RegisterPropertyBoolean('PuffertemperaturGlaetten', false);
|
||||||
|
$this->RegisterPropertyInteger('ZeitKonstante', 120);
|
||||||
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||||
|
|
||||||
|
$this->RegisterVariableFloat('Puffertemperatur', 'Puffertemperatur', '~Temperature', 100);
|
||||||
|
$this->RegisterVariableFloat('Aussentemperatur', 'Aussentemperatur', '~Temperature', 110);
|
||||||
|
$this->RegisterVariableFloat('Solltemperatur', 'Solltemperatur', '~Temperature', 120);
|
||||||
|
$this->RegisterVariableBoolean('Heizbedarf', 'Heizbedarf', '~Switch', 130);
|
||||||
|
|
||||||
|
$this->RegisterAttributeInteger('RegistriertePufferID', 0);
|
||||||
|
$this->RegisterAttributeInteger('RegistrierteAussenID', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzteBerechnung', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzteTemperaturberechnung', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzterLastwechsel', 0);
|
||||||
|
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
||||||
|
$this->RegisterAttributeFloat('Glaettungswert', 0.0);
|
||||||
|
$this->RegisterAttributeBoolean('GlaettungInitialisiert', false);
|
||||||
|
$this->RegisterAttributeString('Leistungsangebot', '[0]');
|
||||||
|
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
|
||||||
|
$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->RegisterAttributeInteger('ZustandAktiveStufe', 0);
|
||||||
|
$this->RegisterAttributeFloat('ZustandBezogeneEnergie', 0.0);
|
||||||
|
$this->RegisterAttributeBoolean('ZustandTemperaturenGueltig', false);
|
||||||
|
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'LastwechselFreigabe',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'LastwechselFreigabe', false);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'Meldezyklus',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'RueckmeldungVerzoegert',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ApplyChanges(): void
|
||||||
|
{
|
||||||
|
parent::ApplyChanges();
|
||||||
|
|
||||||
|
$this->aktualisiereVariablen();
|
||||||
|
try {
|
||||||
|
$this->pruefeKonfiguration();
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->deaktiviereTimer();
|
||||||
|
$this->setzeZustand('Verfuegbar', false);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', false);
|
||||||
|
$this->setzeZustand('Stoerung', true);
|
||||||
|
$this->setzeZustand('Stoertext', $fehler->getMessage());
|
||||||
|
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||||
|
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registriereTemperaturmeldungen();
|
||||||
|
$this->SetTimerInterval('LastwechselFreigabe', 0);
|
||||||
|
$this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000);
|
||||||
|
$this->regelzyklus(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
|
||||||
|
{
|
||||||
|
if ((int) $nachricht !== self::VM_UPDATE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (in_array((int) $senderID, [
|
||||||
|
$this->ReadPropertyInteger('PufferfuehlerVariableID'),
|
||||||
|
$this->ReadPropertyInteger('AussentemperaturVariableID'),
|
||||||
|
], true)) {
|
||||||
|
$this->regelzyklus(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function RequestAction($ident, $wert): void
|
||||||
|
{
|
||||||
|
switch ($ident) {
|
||||||
|
case 'Aktiv':
|
||||||
|
$this->SetValue('Aktiv', (bool) $wert);
|
||||||
|
if (!(bool) $wert) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
}
|
||||||
|
$this->regelzyklus(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'HeizkurveVonWaermepumpeUebernehmen':
|
||||||
|
$this->uebernehmeWaermepumpenwert();
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'LastwechselFreigabe':
|
||||||
|
$this->SetTimerInterval('LastwechselFreigabe', 0);
|
||||||
|
$this->regelzyklus(true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'Melden':
|
||||||
|
if ((bool) $wert) {
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||||
|
}
|
||||||
|
$this->regelzyklus(false);
|
||||||
|
$this->sendeVerbraucherdaten();
|
||||||
|
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 Pufferspeicher nicht aktiv zugeordnet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||||
|
$this->regelzyklus(false);
|
||||||
|
$angebot = $this->leseLeistungsangebot();
|
||||||
|
$sollleistung = $daten['Sollleistung_W'];
|
||||||
|
if ($sollleistung === null) {
|
||||||
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||||
|
&& !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true)
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->regelzyklus(false);
|
||||||
|
}
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!in_array($sollleistung, $angebot, 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->regelzyklus(false);
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function regelzyklus(bool $meldungPlanen): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$jetzt = time();
|
||||||
|
$this->aktualisiereEnergie($jetzt);
|
||||||
|
$temperaturenGueltig = $this->aktualisiereTemperaturen();
|
||||||
|
$stufen = PufferspeicherRegler::dekodiereLeistungsstufen(
|
||||||
|
$this->ReadPropertyString('LeistungsStufen')
|
||||||
|
);
|
||||||
|
|
||||||
|
$solltemperatur = 0.0;
|
||||||
|
$einschaltschwelle = 0.0;
|
||||||
|
if ($temperaturenGueltig) {
|
||||||
|
$solltemperatur = PufferspeicherRegler::solltemperatur(
|
||||||
|
(float) $this->GetValue('Aussentemperatur'),
|
||||||
|
$this->ReadPropertyFloat('FusspunktVorlauftemperatur'),
|
||||||
|
$this->ReadPropertyFloat('HeizkurvenSteigung'),
|
||||||
|
$this->ReadPropertyFloat('HeizkurveMinimaltemperatur'),
|
||||||
|
$this->ReadPropertyFloat('HeizkurveMaximaltemperatur')
|
||||||
|
);
|
||||||
|
$einschaltschwelle = PufferspeicherRegler::einschaltschwelle(
|
||||||
|
$solltemperatur,
|
||||||
|
$this->ReadPropertyFloat('Hysterese'),
|
||||||
|
$this->ReadPropertyInteger('MindesttemperaturModus'),
|
||||||
|
$this->ReadPropertyFloat('Mindesttemperatur'),
|
||||||
|
$this->ReadPropertyFloat('MindesttemperaturDifferenz')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->SetValue('Solltemperatur', $solltemperatur);
|
||||||
|
|
||||||
|
$aktiv = (bool) $this->GetValue('Aktiv');
|
||||||
|
$peak = $this->ReadAttributeString('Betriebsart') === Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||||
|
$angebot = PufferspeicherRegler::leistungsangebot(
|
||||||
|
$aktiv,
|
||||||
|
$peak,
|
||||||
|
$temperaturenGueltig,
|
||||||
|
(float) $this->GetValue('Puffertemperatur'),
|
||||||
|
$einschaltschwelle,
|
||||||
|
$stufen
|
||||||
|
);
|
||||||
|
$heizbedarf = count($angebot) > 1;
|
||||||
|
$this->SetValue('Heizbedarf', $heizbedarf);
|
||||||
|
|
||||||
|
$verfuegbar = $aktiv && $temperaturenGueltig && $stufen !== [];
|
||||||
|
$aenderungMoeglich = $verfuegbar && count($angebot) > 1;
|
||||||
|
$zwangsleistung = (!$verfuegbar || !$heizbedarf) ? 0 : null;
|
||||||
|
$aktuelleLeistung = (int) round((float) $this->leseZustand('Istleistung'));
|
||||||
|
$restzeit = PufferspeicherRegler::restzeitBisLastwechsel(
|
||||||
|
$this->ReadAttributeInteger('LetzterLastwechsel'),
|
||||||
|
$jetzt,
|
||||||
|
$this->ReadPropertyInteger('LastwechselSperrzeit')
|
||||||
|
);
|
||||||
|
$lastwechselGesperrt = $restzeit > 0 && $verfuegbar && $zwangsleistung !== 0;
|
||||||
|
$this->SetTimerInterval('LastwechselFreigabe', $lastwechselGesperrt ? $restzeit * 1000 : 0);
|
||||||
|
if ($lastwechselGesperrt) {
|
||||||
|
$angebot = [$aktuelleLeistung];
|
||||||
|
$aenderungMoeglich = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit');
|
||||||
|
if ($letzteVorgabe <= 0
|
||||||
|
|| $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout')
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ziel = 0;
|
||||||
|
if ($lastwechselGesperrt) {
|
||||||
|
$ziel = $aktuelleLeistung;
|
||||||
|
} elseif ($zwangsleistung !== null) {
|
||||||
|
$ziel = $zwangsleistung;
|
||||||
|
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
||||||
|
$ziel = (int) $this->leseZustand('Sollleistung');
|
||||||
|
if (!in_array($ziel, $angebot, true)) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$ziel = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastwechsel = $this->schalteLeistung($ziel, $jetzt);
|
||||||
|
if ($lastwechsel) {
|
||||||
|
$angebot = [$ziel];
|
||||||
|
$aenderungMoeglich = false;
|
||||||
|
}
|
||||||
|
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
||||||
|
$this->setzeZustand('Sollleistung', $ziel);
|
||||||
|
$this->setzeZustand('Verfuegbar', $verfuegbar);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
|
||||||
|
$this->aktualisiereStoerungen($temperaturenGueltig);
|
||||||
|
if ($meldungPlanen) {
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
}
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->WriteAttributeString('Schaltfehler', $fehler->getMessage());
|
||||||
|
$this->setzeZustand('Verfuegbar', false);
|
||||||
|
$this->setzeZustand('AenderungMoeglich', false);
|
||||||
|
$this->setzeZustand('Stoerung', true);
|
||||||
|
$this->setzeZustand('Stoertext', $fehler->getMessage());
|
||||||
|
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||||
|
$this->protokolliere('Regelzyklus', $fehler->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiereTemperaturen(): bool
|
||||||
|
{
|
||||||
|
$pufferID = $this->ReadPropertyInteger('PufferfuehlerVariableID');
|
||||||
|
$aussenID = $this->ReadPropertyInteger('AussentemperaturVariableID');
|
||||||
|
if (!$this->temperaturwertGueltig($pufferID) || !$this->temperaturwertGueltig($aussenID)) {
|
||||||
|
$this->setzeZustand('TemperaturenGueltig', false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$puffertemperatur = (float) GetValue($pufferID);
|
||||||
|
$aussentemperatur = (float) GetValue($aussenID);
|
||||||
|
$jetzt = time();
|
||||||
|
$letzteBerechnung = $this->ReadAttributeInteger('LetzteTemperaturberechnung');
|
||||||
|
$this->WriteAttributeInteger('LetzteTemperaturberechnung', $jetzt);
|
||||||
|
if ($this->ReadPropertyBoolean('PuffertemperaturGlaetten')) {
|
||||||
|
if (!$this->ReadAttributeBoolean('GlaettungInitialisiert')) {
|
||||||
|
$this->WriteAttributeFloat('Glaettungswert', $puffertemperatur);
|
||||||
|
$this->WriteAttributeBoolean('GlaettungInitialisiert', true);
|
||||||
|
} elseif ($letzteBerechnung > 0 && $jetzt > $letzteBerechnung) {
|
||||||
|
$puffertemperatur = PufferspeicherRegler::pt1(
|
||||||
|
$this->ReadAttributeFloat('Glaettungswert'),
|
||||||
|
$puffertemperatur,
|
||||||
|
$jetzt - $letzteBerechnung,
|
||||||
|
$this->ReadPropertyInteger('ZeitKonstante')
|
||||||
|
);
|
||||||
|
$this->WriteAttributeFloat('Glaettungswert', $puffertemperatur);
|
||||||
|
} else {
|
||||||
|
$puffertemperatur = $this->ReadAttributeFloat('Glaettungswert');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->WriteAttributeBoolean('GlaettungInitialisiert', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->SetValue('Puffertemperatur', $puffertemperatur);
|
||||||
|
$this->SetValue('Aussentemperatur', $aussentemperatur);
|
||||||
|
$this->setzeZustand('TemperaturenGueltig', true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function temperaturwertGueltig(int $variablenID): bool
|
||||||
|
{
|
||||||
|
if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$variable = IPS_GetVariable($variablenID);
|
||||||
|
if (!in_array((int) $variable['VariableType'], [1, 2], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (time() - (int) $variable['VariableUpdated'] > $this->ReadPropertyInteger('TemperaturMaxAlter')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$wert = GetValue($variablenID);
|
||||||
|
|
||||||
|
return is_int($wert) || is_float($wert);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function uebernehmeWaermepumpenwert(): void
|
||||||
|
{
|
||||||
|
$wpID = $this->ReadPropertyInteger('WaermepumpenSolltemperaturVariableID');
|
||||||
|
$aussenID = $this->ReadPropertyInteger('AussentemperaturVariableID');
|
||||||
|
if (!$this->temperaturwertGueltig($wpID) || !$this->temperaturwertGueltig($aussenID)) {
|
||||||
|
throw new RuntimeException('Waermepumpen-Solltemperatur oder Aussentemperatur ist ungueltig.');
|
||||||
|
}
|
||||||
|
$fusspunkt = PufferspeicherRegler::fusspunktAusWaermepumpenwert(
|
||||||
|
(float) GetValue($wpID),
|
||||||
|
(float) GetValue($aussenID),
|
||||||
|
$this->ReadPropertyFloat('HeizkurvenSteigung')
|
||||||
|
);
|
||||||
|
IPS_SetProperty($this->InstanceID, 'FusspunktVorlauftemperatur', $fusspunkt);
|
||||||
|
IPS_ApplyChanges($this->InstanceID);
|
||||||
|
$this->protokolliere('Heizkurve', ['FusspunktVorlauftemperatur' => $fusspunkt]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function schalteLeistung(int $zielLeistung, int $jetzt): bool
|
||||||
|
{
|
||||||
|
$stufen = PufferspeicherRegler::dekodiereLeistungsstufen(
|
||||||
|
$this->ReadPropertyString('LeistungsStufen')
|
||||||
|
);
|
||||||
|
$zielStufe = PufferspeicherRegler::stufeFuerLeistung($stufen, $zielLeistung);
|
||||||
|
$bisher = (int) round((float) $this->leseZustand('Istleistung'));
|
||||||
|
if ($zielLeistung === $bisher && $this->kontakteEntsprechenZiel($stufen, $zielLeistung)) {
|
||||||
|
$this->WriteAttributeString('Schaltfehler', '');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($stufen as $stufe) {
|
||||||
|
RequestAction($stufe['KontaktID'], false);
|
||||||
|
}
|
||||||
|
foreach ($stufen as $stufe) {
|
||||||
|
if ($stufe['Leistung_W'] === $zielLeistung && $zielLeistung > 0) {
|
||||||
|
RequestAction($stufe['KontaktID'], true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
foreach ($stufen as $stufe) {
|
||||||
|
try {
|
||||||
|
RequestAction($stufe['KontaktID'], false);
|
||||||
|
} catch (Throwable $ignoriert) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->setzeZustand('Istleistung', 0.0);
|
||||||
|
$this->setzeZustand('AktiveStufe', 0);
|
||||||
|
throw new RuntimeException('Leistungsstufe konnte nicht sicher geschaltet werden: ' . $fehler->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeString('Schaltfehler', '');
|
||||||
|
$this->setzeZustand('Istleistung', (float) $zielLeistung);
|
||||||
|
$this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET);
|
||||||
|
$this->setzeZustand('AktiveStufe', $zielStufe);
|
||||||
|
$this->WriteAttributeInteger('LetzterLastwechsel', $jetzt);
|
||||||
|
$this->protokolliere('Leistungsstufe', ['Leistung_W' => $zielLeistung, 'Stufe' => $zielStufe]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen */
|
||||||
|
private function kontakteEntsprechenZiel(array $stufen, int $zielLeistung): bool
|
||||||
|
{
|
||||||
|
foreach ($stufen as $stufe) {
|
||||||
|
if (!IPS_VariableExists($stufe['KontaktID'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$sollEin = $zielLeistung > 0 && $stufe['Leistung_W'] === $zielLeistung;
|
||||||
|
if ((bool) GetValue($stufe['KontaktID']) !== $sollEin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function aktualisiereEnergie(int $jetzt): void
|
||||||
|
{
|
||||||
|
$letzteBerechnung = $this->ReadAttributeInteger('LetzteBerechnung');
|
||||||
|
$this->WriteAttributeInteger('LetzteBerechnung', $jetzt);
|
||||||
|
if ($letzteBerechnung <= 0 || $jetzt <= $letzteBerechnung) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$zuwachs = PufferspeicherRegler::energieKWh(
|
||||||
|
(float) $this->leseZustand('Istleistung'),
|
||||||
|
$jetzt - $letzteBerechnung
|
||||||
|
);
|
||||||
|
$this->setzeZustand(
|
||||||
|
'BezogeneEnergie',
|
||||||
|
(float) $this->leseZustand('BezogeneEnergie') + $zuwachs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function aktualisiereStoerungen(bool $temperaturenGueltig): void
|
||||||
|
{
|
||||||
|
$stoerungen = [];
|
||||||
|
if (!$temperaturenGueltig) {
|
||||||
|
$stoerungen[] = 'Puffer- oder Aussentemperatur fehlt oder ist veraltet.';
|
||||||
|
}
|
||||||
|
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||||
|
$stoerungen[] = $this->ReadAttributeString('Schaltfehler');
|
||||||
|
}
|
||||||
|
$this->setzeZustand('Stoerung', $stoerungen !== []);
|
||||||
|
$this->setzeZustand('Stoertext', implode("
|
||||||
|
", array_values(array_unique($stoerungen))));
|
||||||
|
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||||
|
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||||
|
} elseif (!$temperaturenGueltig) {
|
||||||
|
$this->SetStatus(self::STATUS_TEMPERATUR_UNGUELTIG);
|
||||||
|
} else {
|
||||||
|
$this->SetStatus(self::STATUS_AKTIV);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$temperaturenGueltig = (bool) $this->leseZustand('TemperaturenGueltig');
|
||||||
|
$schaltfehler = $this->ReadAttributeString('Schaltfehler');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Kopf' => [
|
||||||
|
'Version' => Nachrichtenvertrag::VERSION,
|
||||||
|
'AbsenderID' => $this->InstanceID,
|
||||||
|
'EmpfaengerID' => $managerID,
|
||||||
|
'Zeitpunkt' => time(),
|
||||||
|
],
|
||||||
|
'Betriebsart' => $this->ReadAttributeString('Betriebsart'),
|
||||||
|
'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' => 'Puffertemperatur_C',
|
||||||
|
'Art' => 'Istwert',
|
||||||
|
'Wert' => $temperaturenGueltig ? (float) $this->GetValue('Puffertemperatur') : null,
|
||||||
|
'Einheit' => 'C',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Aussentemperatur_C',
|
||||||
|
'Art' => 'Istwert',
|
||||||
|
'Wert' => $temperaturenGueltig ? (float) $this->GetValue('Aussentemperatur') : null,
|
||||||
|
'Einheit' => 'C',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Solltemperatur_C',
|
||||||
|
'Art' => 'Sollwert',
|
||||||
|
'Wert' => $temperaturenGueltig ? (float) $this->GetValue('Solltemperatur') : null,
|
||||||
|
'Einheit' => 'C',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Heizbedarf',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => (bool) $this->GetValue('Heizbedarf'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'AktiveStufe',
|
||||||
|
'Art' => 'Status',
|
||||||
|
'Wert' => (int) $this->leseZustand('AktiveStufe'),
|
||||||
|
'Einheit' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Fuehlerfehler',
|
||||||
|
'Art' => 'Stoerung',
|
||||||
|
'Wert' => !$temperaturenGueltig,
|
||||||
|
'Einheit' => '',
|
||||||
|
'Text' => $temperaturenGueltig
|
||||||
|
? ''
|
||||||
|
: 'Puffer- oder Aussentemperatur fehlt oder ist veraltet.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Schaltfehler',
|
||||||
|
'Art' => 'Stoerung',
|
||||||
|
'Wert' => $schaltfehler !== '',
|
||||||
|
'Einheit' => '',
|
||||||
|
'Text' => $schaltfehler,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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',
|
||||||
|
'TemperaturMaxAlter',
|
||||||
|
'ZeitKonstante',
|
||||||
|
] as $property) {
|
||||||
|
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||||
|
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($this->ReadPropertyInteger('LastwechselSperrzeit') < 0) {
|
||||||
|
throw new InvalidArgumentException('LastwechselSperrzeit darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
if ($this->ReadPropertyFloat('Hysterese') <= 0.0) {
|
||||||
|
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
PufferspeicherRegler::solltemperatur(
|
||||||
|
20.0,
|
||||||
|
$this->ReadPropertyFloat('FusspunktVorlauftemperatur'),
|
||||||
|
$this->ReadPropertyFloat('HeizkurvenSteigung'),
|
||||||
|
$this->ReadPropertyFloat('HeizkurveMinimaltemperatur'),
|
||||||
|
$this->ReadPropertyFloat('HeizkurveMaximaltemperatur')
|
||||||
|
);
|
||||||
|
PufferspeicherRegler::einschaltschwelle(
|
||||||
|
50.0,
|
||||||
|
$this->ReadPropertyFloat('Hysterese'),
|
||||||
|
$this->ReadPropertyInteger('MindesttemperaturModus'),
|
||||||
|
$this->ReadPropertyFloat('Mindesttemperatur'),
|
||||||
|
$this->ReadPropertyFloat('MindesttemperaturDifferenz')
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (['PufferfuehlerVariableID', 'AussentemperaturVariableID'] as $property) {
|
||||||
|
$variablenID = $this->ReadPropertyInteger($property);
|
||||||
|
if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) {
|
||||||
|
throw new InvalidArgumentException($property . ' ist nicht eingerichtet.');
|
||||||
|
}
|
||||||
|
$variable = IPS_GetVariable($variablenID);
|
||||||
|
if (!in_array((int) $variable['VariableType'], [1, 2], true)) {
|
||||||
|
throw new InvalidArgumentException($property . ' muss eine Integer- oder Floatvariable sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpID = $this->ReadPropertyInteger('WaermepumpenSolltemperaturVariableID');
|
||||||
|
if ($wpID > 0) {
|
||||||
|
if (!IPS_VariableExists($wpID)) {
|
||||||
|
throw new InvalidArgumentException('WaermepumpenSolltemperaturVariableID existiert nicht.');
|
||||||
|
}
|
||||||
|
$wpVariable = IPS_GetVariable($wpID);
|
||||||
|
if (!in_array((int) $wpVariable['VariableType'], [1, 2], true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'WaermepumpenSolltemperaturVariableID muss eine Integer- oder Floatvariable sein.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stufen = PufferspeicherRegler::dekodiereLeistungsstufen(
|
||||||
|
$this->ReadPropertyString('LeistungsStufen')
|
||||||
|
);
|
||||||
|
if ($stufen === []) {
|
||||||
|
throw new InvalidArgumentException('Mindestens eine Leistungsstufe ist erforderlich.');
|
||||||
|
}
|
||||||
|
foreach ($stufen as $stufe) {
|
||||||
|
if (!IPS_VariableExists($stufe['KontaktID'])) {
|
||||||
|
throw new InvalidArgumentException('Schaltkontakt ' . $stufe['KontaktID'] . ' existiert nicht.');
|
||||||
|
}
|
||||||
|
$kontakt = IPS_GetVariable($stufe['KontaktID']);
|
||||||
|
if ((int) $kontakt['VariableType'] !== 0) {
|
||||||
|
throw new InvalidArgumentException('Alle Schaltkontakte muessen Booleanvariablen sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function registriereTemperaturmeldungen(): void
|
||||||
|
{
|
||||||
|
$zuordnungen = [
|
||||||
|
'RegistriertePufferID' => $this->ReadPropertyInteger('PufferfuehlerVariableID'),
|
||||||
|
'RegistrierteAussenID' => $this->ReadPropertyInteger('AussentemperaturVariableID'),
|
||||||
|
];
|
||||||
|
foreach ($zuordnungen as $attribut => $neueID) {
|
||||||
|
$alteID = $this->ReadAttributeInteger($attribut);
|
||||||
|
if ($alteID > 0 && $alteID !== $neueID) {
|
||||||
|
$this->UnregisterMessage($alteID, self::VM_UPDATE);
|
||||||
|
}
|
||||||
|
if ($neueID > 0 && $neueID !== $alteID) {
|
||||||
|
$this->RegisterMessage($neueID, self::VM_UPDATE);
|
||||||
|
}
|
||||||
|
$this->WriteAttributeInteger($attribut, $neueID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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->RegisterVariableInteger('AktiveStufe', 'Aktive Stufe', '', 200);
|
||||||
|
$this->RegisterVariableFloat('BezogeneEnergie', 'Bezogene Energie', '~Electricity', 210);
|
||||||
|
$this->RegisterVariableBoolean(
|
||||||
|
'TemperaturenGueltig',
|
||||||
|
'Temperaturen gueltig',
|
||||||
|
'~Switch',
|
||||||
|
220
|
||||||
|
);
|
||||||
|
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',
|
||||||
|
'TemperaturenGueltig' => 'ZustandTemperaturenGueltig',
|
||||||
|
];
|
||||||
|
$integerAttribute = [
|
||||||
|
'Sollleistung' => 'ZustandSollleistung',
|
||||||
|
'AktiveStufe' => 'ZustandAktiveStufe',
|
||||||
|
];
|
||||||
|
$floatAttribute = [
|
||||||
|
'Istleistung' => 'ZustandIstleistung',
|
||||||
|
'BezogeneEnergie' => 'ZustandBezogeneEnergie',
|
||||||
|
];
|
||||||
|
$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',
|
||||||
|
'TemperaturenGueltig' => 'ZustandTemperaturenGueltig',
|
||||||
|
];
|
||||||
|
$integerAttribute = [
|
||||||
|
'Sollleistung' => 'ZustandSollleistung',
|
||||||
|
'AktiveStufe' => 'ZustandAktiveStufe',
|
||||||
|
];
|
||||||
|
$floatAttribute = [
|
||||||
|
'Istleistung' => 'ZustandIstleistung',
|
||||||
|
'BezogeneEnergie' => 'ZustandBezogeneEnergie',
|
||||||
|
];
|
||||||
|
$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 [0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_map('intval', $angebot));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deaktiviereTimer(): void
|
||||||
|
{
|
||||||
|
foreach (['LastwechselFreigabe', 'Meldezyklus', 'RueckmeldungVerzoegert'] 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,9 @@ Energiemanagement, Manager und steuerbare Verbraucher fuer IP-Symcon.
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
Das Repository befindet sich im Aufbau. Der gemeinsame Nachrichtenvertrag, die
|
Das Repository befindet sich im Aufbau. Der gemeinsame Nachrichtenvertrag, die
|
||||||
Verbraucherbasis, der Manager, der mehrstufige Warmwassererwaermer und der
|
Verbraucherbasis, der Manager, der mehrstufige Warmwassererwaermer, der
|
||||||
1-stufige Verbraucher sind implementiert. Die weiteren Module sind als
|
1-stufige Verbraucher und die Ladestation Stand-Alone sind implementiert. Die
|
||||||
Diskussionsentwürfe dokumentiert.
|
weiteren Module sind als Diskussionsentwürfe dokumentiert.
|
||||||
|
|
||||||
## Geplante Module
|
## Geplante Module
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ Diskussionsentwürfe dokumentiert.
|
|||||||
- Pufferspeicher
|
- Pufferspeicher
|
||||||
- Verbraucher 1-Stufig (implementiert)
|
- Verbraucher 1-Stufig (implementiert)
|
||||||
- Waermepumpe
|
- Waermepumpe
|
||||||
- Ladestation Stand-Alone
|
- Ladestation Stand-Alone (implementiert)
|
||||||
- Ladestation Gateway
|
- Ladestation Gateway
|
||||||
- Easee Gateway
|
- Easee Gateway
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ der Uebernahme in einen Freigabebranch erfolgreich sein.
|
|||||||
- [Manager-Modul](docs/module/Manager/README.md)
|
- [Manager-Modul](docs/module/Manager/README.md)
|
||||||
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
||||||
- [Verbraucher-1-Stufig-Modul](docs/module/Verbraucher-1-Stufig/README.md)
|
- [Verbraucher-1-Stufig-Modul](docs/module/Verbraucher-1-Stufig/README.md)
|
||||||
|
- [Ladestation-Stand-Alone-Modul](docs/module/Ladestation-Stand-Alone/README.md)
|
||||||
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
||||||
- [Migration Verbraucher 1-Stufig](docs/migration/Verbraucher-1-Stufig.md)
|
- [Migration Verbraucher 1-Stufig](docs/migration/Verbraucher-1-Stufig.md)
|
||||||
- [Obere Anschlüsse des Managers](docs/Obere-Anschluesse.md)
|
- [Obere Anschlüsse des Managers](docs/Obere-Anschluesse.md)
|
||||||
|
|||||||
@@ -87,6 +87,11 @@
|
|||||||
"suffix": " s",
|
"suffix": " s",
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"maximum": 86400
|
"maximum": 86400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "PeakSperreBeiMindestlaufzeitAnbieten",
|
||||||
|
"caption": "Bei Peak Sperre waehrend der Mindestlaufzeit anbieten"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false);
|
$this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false);
|
||||||
$this->RegisterPropertyInteger('RueckmeldungVariableID', 0);
|
$this->RegisterPropertyInteger('RueckmeldungVariableID', 0);
|
||||||
$this->RegisterPropertyInteger('Mindestlaufzeit', 0);
|
$this->RegisterPropertyInteger('Mindestlaufzeit', 0);
|
||||||
|
$this->RegisterPropertyBoolean('PeakSperreBeiMindestlaufzeitAnbieten', true);
|
||||||
$this->RegisterPropertyInteger('Mindesteinschaltdauer', 5);
|
$this->RegisterPropertyInteger('Mindesteinschaltdauer', 5);
|
||||||
$this->RegisterPropertyInteger('Mindestausschaltdauer', 5);
|
$this->RegisterPropertyInteger('Mindestausschaltdauer', 5);
|
||||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||||
@@ -63,6 +64,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$this->RegisterAttributeInteger('LaufzeitStandZeit', 0);
|
$this->RegisterAttributeInteger('LaufzeitStandZeit', 0);
|
||||||
$this->RegisterAttributeString('LaufzeitTag', '');
|
$this->RegisterAttributeString('LaufzeitTag', '');
|
||||||
$this->RegisterAttributeString('Leistungsangebot', '[0]');
|
$this->RegisterAttributeString('Leistungsangebot', '[0]');
|
||||||
|
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
|
||||||
$this->RegisterAttributeBoolean('Initialisiert', false);
|
$this->RegisterAttributeBoolean('Initialisiert', false);
|
||||||
$this->RegisterAttributeBoolean('SchaltvorgangLaeuft', false);
|
$this->RegisterAttributeBoolean('SchaltvorgangLaeuft', false);
|
||||||
$this->RegisterAttributeBoolean('SchaltpruefungAusstehend', false);
|
$this->RegisterAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
@@ -205,8 +207,18 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.');
|
throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||||
$this->aktualisiere(false);
|
$this->aktualisiere(false);
|
||||||
$sollleistung = $daten['Sollleistung_W'];
|
$sollleistung = $daten['Sollleistung_W'];
|
||||||
|
if ($sollleistung === null) {
|
||||||
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||||
|
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
}
|
||||||
|
$this->planeMeldung();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
|
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
|
||||||
throw new InvalidArgumentException(
|
throw new InvalidArgumentException(
|
||||||
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
||||||
@@ -264,9 +276,16 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$jetzt
|
$jetzt
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$peakbetrieb = $this->ReadAttributeString('Betriebsart')
|
||||||
|
=== Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||||
|
$peakSperreBeiMindestlaufzeitAnbieten = $this->ReadPropertyBoolean(
|
||||||
|
'PeakSperreBeiMindestlaufzeitAnbieten'
|
||||||
|
);
|
||||||
|
$mindestlaufzeitLokalErzwingen = $mindestlaufzeitErzwungen
|
||||||
|
&& (!$peakbetrieb || !$peakSperreBeiMindestlaufzeitAnbieten);
|
||||||
$ziel = 0;
|
$ziel = 0;
|
||||||
if ($verfuegbar) {
|
if ($verfuegbar) {
|
||||||
if ($mindestlaufzeitErzwungen) {
|
if ($mindestlaufzeitLokalErzwingen) {
|
||||||
$ziel = $this->ReadPropertyInteger('Nennleistung');
|
$ziel = $this->ReadPropertyInteger('Nennleistung');
|
||||||
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
||||||
$ziel = (int) $this->leseZustand('Sollleistung');
|
$ziel = (int) $this->leseZustand('Sollleistung');
|
||||||
@@ -302,7 +321,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$this->setzeZustand('SchaltbefehlAusstehend', $wartetAufRueckmeldung);
|
$this->setzeZustand('SchaltbefehlAusstehend', $wartetAufRueckmeldung);
|
||||||
$this->setzeZustand('RestMindestzeit', $restMindestzeit);
|
$this->setzeZustand('RestMindestzeit', $restMindestzeit);
|
||||||
$aenderungMoeglich = $verfuegbar
|
$aenderungMoeglich = $verfuegbar
|
||||||
&& !$mindestlaufzeitErzwungen
|
&& (!$mindestlaufzeitErzwungen
|
||||||
|
|| ($peakbetrieb && $peakSperreBeiMindestlaufzeitAnbieten))
|
||||||
&& $restMindestzeit === 0
|
&& $restMindestzeit === 0
|
||||||
&& !$wartetAufRueckmeldung;
|
&& !$wartetAufRueckmeldung;
|
||||||
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
|
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
|
||||||
@@ -312,7 +332,9 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$istleistung,
|
$istleistung,
|
||||||
$verfuegbar,
|
$verfuegbar,
|
||||||
$aenderungMoeglich,
|
$aenderungMoeglich,
|
||||||
$mindestlaufzeitErzwungen
|
$mindestlaufzeitErzwungen,
|
||||||
|
$peakbetrieb,
|
||||||
|
$peakSperreBeiMindestlaufzeitAnbieten
|
||||||
);
|
);
|
||||||
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
||||||
$this->aktualisiereStoerung();
|
$this->aktualisiereStoerung();
|
||||||
@@ -599,6 +621,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
'EmpfaengerID' => $managerID,
|
'EmpfaengerID' => $managerID,
|
||||||
'Zeitpunkt' => time(),
|
'Zeitpunkt' => time(),
|
||||||
],
|
],
|
||||||
|
'Betriebsart' => $this->ReadAttributeString('Betriebsart'),
|
||||||
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
||||||
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
||||||
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
$this->RegisterAttributeFloat('Glaettungswert', 0.0);
|
$this->RegisterAttributeFloat('Glaettungswert', 0.0);
|
||||||
$this->RegisterAttributeBoolean('GlaettungInitialisiert', false);
|
$this->RegisterAttributeBoolean('GlaettungInitialisiert', false);
|
||||||
$this->RegisterAttributeString('Leistungsangebot', '[]');
|
$this->RegisterAttributeString('Leistungsangebot', '[]');
|
||||||
|
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
|
||||||
$this->RegisterAttributeString('Schaltfehler', '');
|
$this->RegisterAttributeString('Schaltfehler', '');
|
||||||
$this->RegisterAttributeFloat('ZustandIstleistung', 0.0);
|
$this->RegisterAttributeFloat('ZustandIstleistung', 0.0);
|
||||||
$this->RegisterAttributeInteger('ZustandSollleistung', 0);
|
$this->RegisterAttributeInteger('ZustandSollleistung', 0);
|
||||||
@@ -208,9 +209,20 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.');
|
throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||||
$this->regelzyklus(false);
|
$this->regelzyklus(false);
|
||||||
$angebot = $this->leseLeistungsangebot();
|
$angebot = $this->leseLeistungsangebot();
|
||||||
$sollleistung = $daten['Sollleistung_W'];
|
$sollleistung = $daten['Sollleistung_W'];
|
||||||
|
if ($sollleistung === null) {
|
||||||
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||||
|
&& !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true)
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$this->regelzyklus(false);
|
||||||
|
}
|
||||||
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!in_array($sollleistung, $angebot, true)) {
|
if (!in_array($sollleistung, $angebot, true)) {
|
||||||
throw new InvalidArgumentException('Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.');
|
throw new InvalidArgumentException('Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.');
|
||||||
}
|
}
|
||||||
@@ -363,45 +375,47 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
(float) $this->ReadAttributeFloat('ZustandLegionellentemperatur')
|
(float) $this->ReadAttributeFloat('ZustandLegionellentemperatur')
|
||||||
);
|
);
|
||||||
$hysterese = $this->ReadPropertyFloat('Hysterese');
|
$hysterese = $this->ReadPropertyFloat('Hysterese');
|
||||||
$maximaltemperaturErreicht = $this->ReadAttributeBoolean('MaximaltemperaturErreicht');
|
|
||||||
if ($temperatur >= $maximum) {
|
|
||||||
$maximaltemperaturErreicht = true;
|
|
||||||
} elseif ($temperatur <= $maximum - $hysterese) {
|
|
||||||
$maximaltemperaturErreicht = false;
|
|
||||||
}
|
|
||||||
$this->WriteAttributeBoolean('MaximaltemperaturErreicht', $maximaltemperaturErreicht);
|
|
||||||
|
|
||||||
$nachladen = (bool) $this->leseZustand('NachladenAktiv');
|
|
||||||
if ($temperatur < $minimum) {
|
|
||||||
$nachladen = true;
|
|
||||||
} elseif ($temperatur >= $minimum + $hysterese) {
|
|
||||||
$nachladen = false;
|
|
||||||
}
|
|
||||||
$this->setzeZustand('NachladenAktiv', $nachladen);
|
|
||||||
|
|
||||||
$zeitplanErfordertHeizen = false;
|
|
||||||
$naechster = WarmwasserRegler::naechsterZeitplaneintrag(
|
$naechster = WarmwasserRegler::naechsterZeitplaneintrag(
|
||||||
$this->ReadPropertyString('Zeitplan'),
|
$this->ReadPropertyString('Zeitplan'),
|
||||||
new DateTimeImmutable('now')
|
new DateTimeImmutable('now')
|
||||||
);
|
);
|
||||||
if ($naechster !== null && $temperatur < $naechster['Solltemperatur']) {
|
if ($naechster !== null
|
||||||
$zeitplanErfordertHeizen = !WarmwasserRegler::kannZielErreichen(
|
&& $temperatur < $naechster['Solltemperatur']
|
||||||
|
&& !WarmwasserRegler::kannZielErreichen(
|
||||||
(float) $this->ReadPropertyInteger('Boilervolumen'),
|
(float) $this->ReadPropertyInteger('Boilervolumen'),
|
||||||
$temperatur,
|
$temperatur,
|
||||||
$naechster['Solltemperatur'],
|
$naechster['Solltemperatur'],
|
||||||
max(0, $naechster['Zeitpunkt'] - $jetzt),
|
max(0, $naechster['Zeitpunkt'] - $jetzt),
|
||||||
$maximalleistung
|
$maximalleistung
|
||||||
);
|
)
|
||||||
|
) {
|
||||||
|
$minimum = $naechster['Solltemperatur'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($nachladen || $zeitplanErfordertHeizen) {
|
$stufeAktiv = (float) $this->leseZustand('Istleistung') > 0.0;
|
||||||
return [[$maximalleistung], false, true, $maximalleistung];
|
$nachladen = $temperatur < $minimum
|
||||||
|
|| ($temperatur < $minimum + $hysterese && $stufeAktiv);
|
||||||
|
$this->setzeZustand('NachladenAktiv', $nachladen);
|
||||||
|
|
||||||
|
$peakbetrieb = $this->ReadAttributeString('Betriebsart')
|
||||||
|
=== Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||||
|
$angebot = WarmwasserRegler::leistungsangebot(
|
||||||
|
$peakbetrieb,
|
||||||
|
$temperatur,
|
||||||
|
$minimum,
|
||||||
|
$maximum,
|
||||||
|
$hysterese,
|
||||||
|
$stufeAktiv,
|
||||||
|
$alleLeistungen
|
||||||
|
);
|
||||||
|
if ($angebot === [0]) {
|
||||||
|
return [$angebot, false, true, 0];
|
||||||
}
|
}
|
||||||
if ($maximaltemperaturErreicht) {
|
if ($angebot === [$maximalleistung]) {
|
||||||
return [[0], false, true, 0];
|
return [$angebot, false, true, $maximalleistung];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$alleLeistungen, true, true, null];
|
return [$angebot, true, true, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function aktualisiereLegionellenstatus(bool $temperaturGueltig, int $jetzt): void
|
private function aktualisiereLegionellenstatus(bool $temperaturGueltig, int $jetzt): void
|
||||||
@@ -587,6 +601,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
'EmpfaengerID' => $managerID,
|
'EmpfaengerID' => $managerID,
|
||||||
'Zeitpunkt' => time(),
|
'Zeitpunkt' => time(),
|
||||||
],
|
],
|
||||||
|
'Betriebsart' => $this->ReadAttributeString('Betriebsart'),
|
||||||
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
||||||
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
||||||
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
||||||
|
|||||||
+66
-65
@@ -1,6 +1,6 @@
|
|||||||
# EMS-Schnittstelle
|
# EMS-Schnittstelle
|
||||||
|
|
||||||
Vertragsversion: `3.0`
|
Vertragsversion: `4.0`
|
||||||
|
|
||||||
Es gibt genau eine fachliche Empfangsmethode je Richtung:
|
Es gibt genau eine fachliche Empfangsmethode je Richtung:
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ public function VerbraucherdatenEmpfangen(array $daten): void;
|
|||||||
|
|
||||||
| Feld | Typ | Bedeutung |
|
| Feld | Typ | Bedeutung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Version` | Text | Vertragsversion `3.0` |
|
| `Version` | Text | Vertragsversion `4.0` |
|
||||||
| `AbsenderID` | Ganzzahl | Sendende Symcon-Instanz |
|
| `AbsenderID` | Ganzzahl | Sendende Symcon-Instanz |
|
||||||
| `EmpfaengerID` | Ganzzahl | Empfangende Symcon-Instanz |
|
| `EmpfaengerID` | Ganzzahl | Empfangende Symcon-Instanz |
|
||||||
| `Zeitpunkt` | Ganzzahl | Unixzeit in UTC |
|
| `Zeitpunkt` | Ganzzahl | Unixzeit in UTC |
|
||||||
@@ -23,118 +23,119 @@ public function VerbraucherdatenEmpfangen(array $daten): void;
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Kopf": {
|
"Kopf": {
|
||||||
"Version": "3.0",
|
"Version": "4.0",
|
||||||
"AbsenderID": 10001,
|
"AbsenderID": 10001,
|
||||||
"EmpfaengerID": 20001,
|
"EmpfaengerID": 20001,
|
||||||
"Zeitpunkt": 1788825600
|
"Zeitpunkt": 1788825600
|
||||||
},
|
},
|
||||||
|
"Betriebsart": "PV",
|
||||||
"Sollleistung_W": 1501
|
"Sollleistung_W": 1501
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`Sollleistung_W` ist immer eine Ganzzahl und muss im aktuell gemeldeten Leistungsangebot liegen.
|
`Betriebsart` ist `PV` oder `Peak`. `Sollleistung_W` ist eine Ganzzahl
|
||||||
|
aus dem fuer diese Betriebsart gemeldeten Leistungsangebot oder `null`.
|
||||||
|
|
||||||
|
`null` kuendigt nur die Betriebsart an. Der Verbraucher uebernimmt sie,
|
||||||
|
berechnet sein Leistungsangebot neu und meldet es zurueck. Eine vorhandene
|
||||||
|
Sollleistung wird dabei nur verworfen, wenn sie im neuen Angebot nicht mehr
|
||||||
|
zulaessig ist.
|
||||||
|
|
||||||
## Verbraucher an Manager
|
## Verbraucher an Manager
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Kopf": {
|
"Kopf": {
|
||||||
"Version": "3.0",
|
"Version": "4.0",
|
||||||
"AbsenderID": 20001,
|
"AbsenderID": 20001,
|
||||||
"EmpfaengerID": 10001,
|
"EmpfaengerID": 10001,
|
||||||
"Zeitpunkt": 1788825602
|
"Zeitpunkt": 1788825602
|
||||||
},
|
},
|
||||||
|
"Betriebsart": "Peak",
|
||||||
"PrioritaetPV": 0,
|
"PrioritaetPV": 0,
|
||||||
"PrioritaetPeak": 0,
|
"PrioritaetPeak": 0,
|
||||||
"Leistungswerte_W": [
|
"Leistungswerte_W": [0],
|
||||||
-3000,
|
"AenderungMoeglich": false,
|
||||||
-2000,
|
|
||||||
{"Von_W": -1000, "Bis_W": -500},
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
{"Von_W": 1000, "Bis_W": 2000},
|
|
||||||
3000
|
|
||||||
],
|
|
||||||
"AenderungMoeglich": true,
|
|
||||||
"Verfuegbar": true,
|
"Verfuegbar": true,
|
||||||
"Istleistung_W": 1498.5,
|
"Istleistung_W": 0,
|
||||||
"Leistungsquelle": 2,
|
"Leistungsquelle": 1,
|
||||||
"Zustand": [
|
"Zustand": [
|
||||||
{
|
{
|
||||||
"Kennung": "Sollleistung_W",
|
"Kennung": "Sollleistung_W",
|
||||||
"Art": "Sollwert",
|
"Art": "Sollwert",
|
||||||
"Wert": 1501,
|
"Wert": 0,
|
||||||
"Einheit": "W"
|
"Einheit": "W"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Feste Regeln
|
## Betriebsart-Synchronisation
|
||||||
|
|
||||||
- Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet eine hoehere Prioritaet.
|
1. Der Manager bestimmt `PV` oder `Peak`.
|
||||||
|
2. Meldungen einer anderen Betriebsart werden nicht zur Verteilung verwendet.
|
||||||
|
3. Der Manager sendet diesen Verbrauchern eine Betriebsart-Ankuendigung mit
|
||||||
|
`Sollleistung_W=null`.
|
||||||
|
4. Jeder Verbraucher berechnet und meldet seine PowerSteps fuer diese
|
||||||
|
Betriebsart.
|
||||||
|
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der Manager
|
||||||
|
Sollleistungen.
|
||||||
|
|
||||||
|
Damit kann jeder Verbrauchertyp unterschiedliche Angebote fuer PV und Peak
|
||||||
|
melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
|
||||||
|
|
||||||
|
## Feste Regeln
|
||||||
|
|
||||||
|
- Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet hoehere Prioritaet.
|
||||||
- `Leistungsquelle`: 0 nicht vorhanden, 1 berechnet, 2 gemessen.
|
- `Leistungsquelle`: 0 nicht vorhanden, 1 berechnet, 2 gemessen.
|
||||||
- Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`.
|
- Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`.
|
||||||
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W` einschliesslich.
|
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W`.
|
||||||
- Die Leistungswerte sind aufsteigend, eindeutig und ueberschneiden sich nicht.
|
- Leistungswerte sind aufsteigend, eindeutig und ueberschneiden sich nicht.
|
||||||
- `Zustand` enthaelt immer den gemeinsamen Eintrag `Sollleistung_W` und daneben nur benoetigte geraetespezifische Eintraege.
|
- `Zustand` enthaelt immer `Sollleistung_W`.
|
||||||
- Eine aktive Stoerung wird mit `Art=Stoerung` und `Wert=true` gemeldet; eine behobene mit `false`.
|
- Verbraucher werden ausschliesslich im Manager zugeordnet.
|
||||||
- Verbraucher werden ausschliesslich im Manager zugeordnet. Der Verbraucher besitzt keine Manager-ID-Property.
|
- Der Verbraucher besitzt keine Manager-ID-Property.
|
||||||
|
|
||||||
## Technische Umsetzung in IP-Symcon
|
## Technische Umsetzung in IP-Symcon
|
||||||
|
|
||||||
Der Transport erfolgt über `IPS_RequestAction` mit JSON. `MessageSink` erkennt
|
Der Transport erfolgt ueber `IPS_RequestAction` mit JSON. `MessageSink`
|
||||||
registrierte Änderungen; der empfangende Action-Handler prüft das Paket und
|
erkennt registrierte Aenderungen. Empfang und Neuberechnung sind intern
|
||||||
ruft danach genau eine fachliche Empfangsmethode auf. Empfang und Neuberechnung
|
entkoppelt, damit keine gegenseitige Endlosschleife entsteht.
|
||||||
werden intern entkoppelt, damit keine gegenseitige Endlosschleife entsteht.
|
|
||||||
|
|
||||||
Das PHP-Interface legt nur die Methode fest. Die folgenden gemeinsamen
|
Das PHP-Interface legt nur die Empfangsmethode fest. Die gemeinsamen
|
||||||
Symcon-Datenpunkte werden durch `VerbraucherBasisTrait` registriert.
|
Symcon-Datenpunkte registriert `VerbraucherBasisTrait`.
|
||||||
|
|
||||||
### Gemeinsame Properties aller Verbraucher
|
### Gemeinsame Properties aller Verbraucher
|
||||||
|
|
||||||
| Ident | Typ | Standard | Beschreibung |
|
| Ident | Typ | Standard | Beschreibung |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `PrioritaetPV` | Integer | `0` | Priorität ab 0; kleinere Zahl bedeutet höhere Priorität. |
|
| `PrioritaetPV` | Integer | `0` | Prioritaet in der Betriebsart PV |
|
||||||
| `PrioritaetPeak` | Integer | `0` | Wie PV-Priorität, ohne fachliche Obergrenze. |
|
| `PrioritaetPeak` | Integer | `0` | Prioritaet in der Betriebsart Peak |
|
||||||
| `Meldeintervall` | Integer | `10` | Vollständige Rückmeldung in Sekunden; muss grösser als 0 sein. |
|
| `Meldeintervall` | Integer | `10` | Vollstaendige Rueckmeldung in Sekunden |
|
||||||
| `VorgabeTimeout` | Integer | `120` | Sollwert wird nach dieser Zeit ohne Erneuerung ungültig. |
|
| `VorgabeTimeout` | Integer | `120` | Ablaufzeit einer Sollleistung |
|
||||||
| `EinstellungenInVisu` | Boolean | `false` | Blendet lokale Einstellvariablen ein und macht sie geprüft bedienbar. |
|
| `EinstellungenInVisu` | Boolean | `false` | Lokale Einstellungen in der Visualisierung |
|
||||||
| `LoggingEin` | Boolean | `false` | Aktiviert das laufende Diagnoseprotokoll. Module können zusätzliche Diagnosevariablen getrennt einblenden. |
|
| `LoggingEin` | Boolean | `false` | Laufendes Diagnoseprotokoll |
|
||||||
|
|
||||||
### Gemeinsame Variablen aller Verbraucher
|
### Gemeinsame Variablen aller Verbraucher
|
||||||
|
|
||||||
| Ident | Typ / Zugriff | Beschreibung |
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Start `false`. |
|
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Start `false` |
|
||||||
| `Istleistung` | Float / Anzeige | Aktuelle Leistung in W; nur zusammen mit `Leistungsquelle` gültig. |
|
| `Istleistung` | Float / Anzeige | Aktuelle Leistung in W |
|
||||||
| `Leistungsquelle` | Integer / Anzeige | `0` nicht vorhanden, `1` berechnet, `2` gemessen. |
|
| `Leistungsquelle` | Integer / Anzeige | 0 nicht vorhanden, 1 berechnet, 2 gemessen |
|
||||||
| `Sollleistung` | Integer / Anzeige | Tatsächlich angenommene Vorgabe in W. |
|
| `Sollleistung` | Integer / Anzeige | Angenommene oder lokal erzwungene Vorgabe |
|
||||||
| `SollwertGueltig` | Boolean / Anzeige | Kennzeichnet eine aktuell gültige Vorgabe. |
|
| `SollwertGueltig` | Boolean / Anzeige | Aktuelle, nicht abgelaufene Vorgabe |
|
||||||
| `Verfuegbar` | Boolean / Anzeige | Verbraucher grundsätzlich für EMS-Steuerung verfügbar. |
|
| `Verfuegbar` | Boolean / Anzeige | Verbraucher grundsaetzlich verfuegbar |
|
||||||
| `AenderungMoeglich` | Boolean / Anzeige | Neue Vorgabe darf momentan übernommen werden. |
|
| `AenderungMoeglich` | Boolean / Anzeige | Neue Vorgabe darf uebernommen werden |
|
||||||
| `Stoerung` | Boolean / Anzeige | Mindestens ein Zustandseintrag der Art `Stoerung` ist aktiv. |
|
| `Stoerung` | Boolean / Anzeige | Mindestens eine Stoerung aktiv |
|
||||||
| `Stoertext` | String / Anzeige | Zusammengefasste lesbare Störbeschreibung. |
|
| `Stoertext` | String / Anzeige | Zusammengefasste Stoerbeschreibung |
|
||||||
|
|
||||||
`Leistungswerte_W` und `Zustand` werden intern gehalten und direkt in die
|
`Leistungswerte_W`, `Betriebsart` und `Zustand` werden intern gehalten
|
||||||
Nachricht geschrieben. Dafür werden keine zusätzlichen Symcon-Variablen
|
und direkt in die Nachricht geschrieben.
|
||||||
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
|
## Zeitverhalten
|
||||||
|
|
||||||
- Rückmeldung nach dem Start, bei relevanten Änderungen und zusätzlich alle
|
- Rueckmeldung nach Start, relevanten Aenderungen und alle `Meldeintervall`
|
||||||
`Meldeintervall` Sekunden.
|
Sekunden.
|
||||||
- Laufende Vorgaben werden vom Manager standardmässig alle 60 Sekunden erneuert.
|
- Laufende Vorgaben werden vom Manager standardmaessig erneuert.
|
||||||
- Nach `VorgabeTimeout` Sekunden ist eine nicht erneuerte Vorgabe ungültig.
|
- Nach `VorgabeTimeout` ist eine nicht erneuerte Vorgabe ungueltig.
|
||||||
- Nach einem Neustart wird zuerst der Gerätezustand erfasst; eine alte Vorgabe
|
- Nach einem Neustart wird keine alte Vorgabe ungeprueft aufgenommen.
|
||||||
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,76 @@
|
|||||||
|
# ADR 0004: Betriebsartabhaengige Leistungsangebote
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Der Nachrichtenvertrag 3.0 uebermittelte vom Manager nur
|
||||||
|
`Sollleistung_W`. Verbraucher meldeten ein einziges Leistungsangebot und
|
||||||
|
kannten die aktuelle Betriebsart nicht. Damit konnten Module keine
|
||||||
|
unterschiedlichen PowerSteps fuer PV- und Peakbetrieb bereitstellen.
|
||||||
|
|
||||||
|
Eine einfache Ergaenzung des Sollwertpakets reicht nicht aus: Beim Wechsel der
|
||||||
|
Betriebsart besitzt der Manager zunaechst noch das Angebot der vorherigen
|
||||||
|
Betriebsart. Eine sofortige Verteilung koennte deshalb einen Sollwert erzeugen,
|
||||||
|
den der Verbraucher im neuen Modus ablehnen muss.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
Der Vertrag wird inkompatibel auf Version `4.0` angehoben.
|
||||||
|
|
||||||
|
- Managerdaten und Verbraucherdaten enthalten verpflichtend `Betriebsart`
|
||||||
|
mit dem Wert `PV` oder `Peak`.
|
||||||
|
- `Sollleistung_W` in Managerdaten darf `null` sein.
|
||||||
|
- `null` ist eine reine Betriebsart-Ankuendigung und kein Schaltbefehl.
|
||||||
|
- Der Verbraucher uebernimmt die Betriebsart, berechnet sein Leistungsangebot
|
||||||
|
neu und meldet dieses mit derselben Betriebsart zurueck.
|
||||||
|
- Der Manager verwendet nur Angebote seiner aktuellen Betriebsart.
|
||||||
|
- Solange mindestens ein aktiver Verbraucher nicht synchronisiert ist, erfolgt
|
||||||
|
keine Verteilung.
|
||||||
|
- Nach einem Betriebsartwechsel verwirft der Manager seinen Sollwertcache.
|
||||||
|
|
||||||
|
Die konkrete Differenz zwischen PV- und Peakangebot bleibt Verantwortung des
|
||||||
|
Verbrauchermoduls. Die derzeit implementierten Verbraucher uebernehmen die
|
||||||
|
zustandsabhaengigen Enelix-1-Angebote gezielt:
|
||||||
|
|
||||||
|
- Der einstufige Verbraucher bietet in Peak normalerweise `[0]`. Bei faelliger
|
||||||
|
Tagesmindestlaufzeit ist konfigurierbar, ob `[0, Nennleistung]` angeboten
|
||||||
|
oder nur die Nennleistung erzwungen wird.
|
||||||
|
- Die Ladestation bietet in Peak mit Solarladen `[0]`, ohne Solarladen dagegen
|
||||||
|
`[0, ...Ladestufen]` an.
|
||||||
|
- Der Warmwassererwaermer bietet unter seiner wirksamen Mindesttemperatur auch
|
||||||
|
in Peak `[0, ...Leistungsstufen]` an.
|
||||||
|
- Der Pufferspeicher bietet im Peakbetrieb unabhaengig vom Zustand `[0]` an.
|
||||||
|
- Technische Schaltsperren und ausstehende Rueckmeldungen duerfen das Angebot
|
||||||
|
weiterhin auf die aktuell gehaltene Leistung begrenzen.
|
||||||
|
|
||||||
|
## Alternativen
|
||||||
|
|
||||||
|
### Zwei Angebote gleichzeitig melden
|
||||||
|
|
||||||
|
Separate Felder fuer PV- und Peak-PowerSteps wuerden den Umschalt-Handshake
|
||||||
|
vermeiden. Sie verdoppeln jedoch alle Angebotsdaten und zwingen jedes Modul,
|
||||||
|
beide Zustaende jederzeit parallel zu berechnen.
|
||||||
|
|
||||||
|
### Betriebsart ohne Synchronisation senden
|
||||||
|
|
||||||
|
Der Manager koennte Betriebsart und Sollleistung in einem Paket senden. Das
|
||||||
|
erste Kommando nach einem Wechsel waere dann aus dem alten Angebot berechnet
|
||||||
|
und koennte ungueltig sein.
|
||||||
|
|
||||||
|
### Betriebsart aus einer Manager-Variable lesen
|
||||||
|
|
||||||
|
Eine direkte Objektkopplung wuerde die definierte Schnittstelle umgehen,
|
||||||
|
mehrere Manager erschweren und Verbraucher unnoetig an die Managerinstanz
|
||||||
|
binden.
|
||||||
|
|
||||||
|
## Folgen
|
||||||
|
|
||||||
|
- Alle Manager und Verbraucher einer Installation muessen gemeinsam auf
|
||||||
|
Vertrag 4.0 aktualisiert werden.
|
||||||
|
- Version 3.0 und 4.0 koennen nicht innerhalb derselben Managerzuordnung
|
||||||
|
gemischt werden.
|
||||||
|
- Ein Betriebsartwechsel benoetigt mindestens einen zusaetzlichen
|
||||||
|
Nachrichtenumlauf.
|
||||||
|
- Tests pruefen Vertragsvalidierung, Synchronisation und modulspezifische
|
||||||
|
Angebote fuer PV und Peak.
|
||||||
|
- ADR 0001 bleibt als historische Entscheidung erhalten, wird fuer den
|
||||||
|
aktuellen Vertrag aber durch diese Entscheidung abgeloest.
|
||||||
@@ -1,46 +1,63 @@
|
|||||||
# Ladestation Stand-Alone
|
# Ladestation Stand-Alone
|
||||||
|
|
||||||
> Status: Diskussionsentwurf. Eigenständiges Verbrauchermodul mit direkter
|
> Status: Implementiert. Eigenstaendiges Verbrauchermodul mit direkter
|
||||||
> Geräteanbindung; keine Beziehung zur Ladestation Gateway.
|
> Geraeteanbindung; keine Beziehung zur Ladestation Gateway.
|
||||||
|
|
||||||
## Zusätzliche Variablen
|
## Unterstuetzte Geraete
|
||||||
|
|
||||||
|
| Geraetetyp | Status | Steuerung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| go-e Charger, alte API | `GET /mqtt?payload=` | `alw` und `amp` |
|
||||||
|
| go-e Charger Gemini / Gemini flex | `GET /api/status` | `frc` und `amp` |
|
||||||
|
| smart-me Pico | Pico-Charging-API | Load-Management-Current-API mit Basic Auth |
|
||||||
|
|
||||||
|
Die Routen, Leistungsfaktoren und Erkennung wurden gezielt aus Enelix 1
|
||||||
|
uebernommen. Zugangsdaten werden weder als Variable noch im Diagnoseprotokoll
|
||||||
|
ausgegeben.
|
||||||
|
|
||||||
|
## Verhalten
|
||||||
|
|
||||||
|
- `FahrzeugVerbunden` entspricht bei go-e `car != 1` und bei Pico `State != 1`.
|
||||||
|
- Die Phasenerkennung verwendet wie Enelix 1 die gemessene Ladeleistung:
|
||||||
|
ueber 7500 W sind dreiphasig, sonst einphasig.
|
||||||
|
- Ein bereits verbundenes Fahrzeug gilt bei einem ermittelten Maximalstrom
|
||||||
|
unter 6 A als geladen.
|
||||||
|
- PV mit Solarladen bietet `0 W` und alle Ladestufen von 6 A bis zum
|
||||||
|
konfigurierten Maximum an.
|
||||||
|
- PV ohne Solarladen bietet ausschliesslich die Maximalleistung an.
|
||||||
|
- Peak mit Solarladen bietet nur `[0]` an und stoppt die Ladestation.
|
||||||
|
- Peak ohne Solarladen bietet `[0, ...Ladestufen]` an, damit der Manager die
|
||||||
|
Ladeleistung wie in Enelix 1 stufenweise reduzieren kann.
|
||||||
|
- Ohne gueltige Manager-Vorgabe wird bei Solarladen mit 0 A und ohne
|
||||||
|
Solarladen mit maximaler Leistung gefahren.
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
| Ident | Typ / Zugriff | Beschreibung |
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `FahrzeugVerbunden` | Boolean / Anzeige | Nur bei gültigem Gerätestatus aussagekräftig. |
|
| `FahrzeugVerbunden` | Boolean / Anzeige | Geraet meldet ein verbundenes Fahrzeug. |
|
||||||
| `Fahrzeugstatus` | Integer / Anzeige | `0` unbekannt, `1` nicht verbunden, `2` bereit, `3` lädt, `4` voll, `5` Fehler. |
|
| `FahrzeugGeladen` | Boolean / Anzeige | Enelix-1-Erkennung unter 6 A. |
|
||||||
| `Ladestrom` | Float / Anzeige | Aktueller Ladestrom in A. |
|
| `Fahrzeugstatus` | Integer / Anzeige | `0` unbekannt, `1` getrennt, `2` bereit, `3` laedt, `4` geladen. |
|
||||||
|
| `Ladestrom` | Float / Anzeige | Aus der gemessenen Leistung ermittelter Strom in A. |
|
||||||
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig, `3` dreiphasig. |
|
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig, `3` dreiphasig. |
|
||||||
| `Ladefreigabe` | Boolean / lokal bedienbar | Lokale Ladeerlaubnis zusätzlich zu `Aktiv`. |
|
| `Ladefreigabe` | Boolean / bedienbar | Nur bei `EinstellungenInVisu`; lokale Ladeerlaubnis. |
|
||||||
| `Solarladen` | Boolean / lokal bedienbar | Überschussorientiertes Laden ein/aus. |
|
| `Solarladen` | Boolean / bedienbar | Nur bei `EinstellungenInVisu`; variable Ladestufen ein/aus. |
|
||||||
|
|
||||||
## Zusätzliche Properties
|
## Properties
|
||||||
|
|
||||||
| Ident | Typ | Standard / Beschreibung |
|
Neben den gemeinsamen Verbraucher-Properties werden Geraetetyp, Adresse,
|
||||||
| --- | --- | --- |
|
Pico-ID, Seriennummer, Zugangsdaten, maximaler Ladestrom, Abfrageintervall
|
||||||
| `Ladefreigabe` | Boolean | `false`. |
|
sowie die Startwerte fuer Ladefreigabe und Solarladen konfiguriert.
|
||||||
| `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
|
Die gemeinsamen Verbraucher- und Diagnosevariablen entsprechen der
|
||||||
Sollwert-Variable entfallen. Ladefreigabe und Solarladen erscheinen nur bei
|
EMS-Schnittstelle Version 4.0. `LoggingEin` steuert ausschliesslich das
|
||||||
aktivem `EinstellungenInVisu` als bedienbare Variablen.
|
laufende Debug-Protokoll.
|
||||||
|
|
||||||
## Zustand
|
## Tests
|
||||||
|
|
||||||
`Fahrzeugstatus`, `FahrzeugVerbunden`, `Ladefreigabe`, `Solarladen` und
|
Die Adaptertests verwenden einen injizierten Fake-HTTP-Transport. Dadurch
|
||||||
`Ladestrom_A` sowie `Ladefehler` und gerätespezifische Fehler.
|
werden fuer alle drei Geraetevarianten Statusantworten, URL, HTTP-Methode,
|
||||||
|
Authentisierung und Steueraufrufe geprueft, ohne ein reales Geraet anzusprechen.
|
||||||
## Offene Punkte
|
Der Symcon-Funktionstest nutzt den internen, nicht im Konfigurationsformular
|
||||||
|
sichtbaren `Testmodus` und prueft zusaetzlich Fahrzeug-/Phasenerkennung,
|
||||||
- Direkte Adapter und ihre noch gültigen API-Routen bei der Implementierung prüfen.
|
die vier PV-/Peak-Angebote sowie die Umschaltung ueber den `Solarladen`-Button.
|
||||||
- Lokalen Mindestladebedarf bei deaktiviertem Solarladen genau festlegen.
|
|
||||||
|
|||||||
@@ -1,54 +1,144 @@
|
|||||||
# Pufferspeicher
|
# Verbraucher Pufferspeicher
|
||||||
|
|
||||||
> Status: Diskussionsentwurf.
|
> Status: implementiert fuer IP-Symcon 8.0+ und den Enelix-2-Vertrag `4.0`.
|
||||||
|
|
||||||
Regelt elektrische Heizstufen nach Pufferbedarf. Die Heizkurve verwendet einen
|
Das Modul bindet elektrische Heizstufen eines Pufferspeichers an den
|
||||||
Fusspunkt bei 20 °C Aussentemperatur und eine Steigung:
|
Enelix-Manager an. Die Freigabe wird aus Puffer- und Aussentemperatur,
|
||||||
|
Heizkurve, Hysterese, lokaler Aktivierung und aktueller Betriebsart berechnet.
|
||||||
|
|
||||||
`Solltemperatur = FusspunktVorlauftemperatur + HeizkurvenSteigung × (20 - Aussentemperatur)`
|
Es ist eine gezielte Adaption des Enelix-1-Moduls `Puffer_Speicher`.
|
||||||
|
Leistungsstufen, Temperaturquellen, optionale PT1-Glaettung und Heizkurve
|
||||||
|
wurden uebernommen und ueberarbeitet. Zyklische Altlogik, alte
|
||||||
|
Kommunikationsvariablen, `Idle` und `PowerSteps` als Symcon-Variable wurden
|
||||||
|
verworfen.
|
||||||
|
|
||||||
Anschliessend wird auf Mindest- und Maximaltemperatur begrenzt.
|
| Technisches Merkmal | Wert |
|
||||||
|
| --- | --- |
|
||||||
|
| Modulname | `VerbraucherPufferspeicher` |
|
||||||
|
| Alias | `Pufferspeicher` |
|
||||||
|
| Modul-ID | `{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}` |
|
||||||
|
| Enelix-Vertrag | `4.0` |
|
||||||
|
|
||||||
## Zusätzliche Variablen
|
## Regelverhalten
|
||||||
|
|
||||||
| Ident | Typ / Zugriff | Beschreibung |
|
Die Solltemperatur wird berechnet als:
|
||||||
|
|
||||||
|
`FusspunktVorlauftemperatur + HeizkurvenSteigung * (20 - Aussentemperatur)`
|
||||||
|
|
||||||
|
Anschliessend wird sie auf `HeizkurveMinimaltemperatur` und
|
||||||
|
`HeizkurveMaximaltemperatur` begrenzt.
|
||||||
|
|
||||||
|
Die Einschaltschwelle wird je nach `MindesttemperaturModus` bestimmt:
|
||||||
|
|
||||||
|
- `Aus`: Solltemperatur minus Hysterese,
|
||||||
|
- `Statisch`: konfigurierte absolute Mindesttemperatur,
|
||||||
|
- `Differenz`: Solltemperatur minus konfigurierte Differenz.
|
||||||
|
|
||||||
|
Es gilt:
|
||||||
|
|
||||||
|
- im PV-Betrieb unter der Einschaltschwelle: `[0, ...Leistungsstufen]`,
|
||||||
|
- im PV-Betrieb an oder oberhalb der Einschaltschwelle: `[0]`,
|
||||||
|
- im Peakbetrieb unabhaengig von der Temperatur: `[0]`,
|
||||||
|
- bei `Aktiv=false`: immer `[0]`,
|
||||||
|
- bei ungueltigen Temperaturen: `[0]` und nicht verfuegbar.
|
||||||
|
|
||||||
|
Der Pufferspeicher ist im Peakbetrieb damit pauschal gesperrt. Beim Wechsel in
|
||||||
|
Peak wird eine aktive Stufe sicher ausgeschaltet.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Die Properties erscheinen im Formular in dieser Reihenfolge.
|
||||||
|
|
||||||
|
### Manager und Zeitverhalten
|
||||||
|
|
||||||
|
| Ident | Standard | Beschreibung |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `PrioritaetPV` | `0` | Prioritaet im PV-Betrieb |
|
||||||
|
| `PrioritaetPeak` | `0` | Prioritaet im Peakbetrieb |
|
||||||
|
| `Meldeintervall` | `10 s` | Vollstaendige Rueckmeldung |
|
||||||
|
| `VorgabeTimeout` | `120 s` | Gueltigkeit einer Vorgabe |
|
||||||
|
| `LastwechselSperrzeit` | `5 s` | Mindestzeit zwischen Lastwechseln |
|
||||||
|
|
||||||
|
### Pufferspeicher
|
||||||
|
|
||||||
|
| Ident | Standard | Beschreibung |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `LeistungsStufen` | `[]` | Stufe, Leistung und Boolean-Schaltkontakt |
|
||||||
|
| `PufferfuehlerVariableID` | `0` | Integer- oder Floatvariable |
|
||||||
|
| `AussentemperaturVariableID` | `0` | Integer- oder Floatvariable |
|
||||||
|
|
||||||
|
### Heizkurve
|
||||||
|
|
||||||
|
| Ident | Standard | Beschreibung |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `FusspunktVorlauftemperatur` | `35.0 C` | Vorlauf bei 20 C aussen |
|
||||||
|
| `HeizkurvenSteigung` | `1.0` | Anhebung je Kelvin fallender Aussentemperatur |
|
||||||
|
| `HeizkurveMinimaltemperatur` | `20.0 C` | Untere Sollwertbegrenzung |
|
||||||
|
| `HeizkurveMaximaltemperatur` | `80.0 C` | Obere Sollwertbegrenzung |
|
||||||
|
| `Hysterese` | `5.0 K` | Abstand zur Einschaltschwelle |
|
||||||
|
| `MindesttemperaturModus` | `Aus` | Aus, statisch oder Differenz |
|
||||||
|
| `Mindesttemperatur` | `20.0 C` | Wert im statischen Modus |
|
||||||
|
| `MindesttemperaturDifferenz` | `5.0 K` | Wert im Differenzmodus |
|
||||||
|
| `WaermepumpenSolltemperaturVariableID` | `0` | Optionale Quelle fuer die WP-Uebernahme |
|
||||||
|
|
||||||
|
Der Button **Wert von Waermepumpe uebernehmen** berechnet den Fusspunkt so,
|
||||||
|
dass die lokale Heizkurve beim aktuellen Aussenwert durch den aktuellen
|
||||||
|
WP-Sollwert verlaeuft. Es entsteht keine dauerhafte Laufzeitkopplung.
|
||||||
|
|
||||||
|
### Erweiterte Einstellungen
|
||||||
|
|
||||||
|
| Ident | Standard | Beschreibung |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `TemperaturMaxAlter` | `120 s` | Maximal zulaessiges Alter beider Messwerte |
|
||||||
|
| `PuffertemperaturGlaetten` | `false` | Aktiviert PT1-Glaettung |
|
||||||
|
| `ZeitKonstante` | `120 s` | PT1-Zeitkonstante |
|
||||||
|
| `EinstellungenInVisu` | `false` | Gemeinsame Verbraucheroption |
|
||||||
|
| `DiagnosevariablenAnzeigen` | `false` | Diagnosevariablen einblenden |
|
||||||
|
| `LoggingEin` | `false` | Debug-Protokoll aktivieren |
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
|
Immer vorhanden:
|
||||||
|
|
||||||
|
| Ident | Typ | Beschreibung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Puffertemperatur` | Float / Anzeige | Aktuelle Temperatur in °C. |
|
| `Aktiv` | Boolean / bedienbar | Lokale Ein-/Aus-Freigabe |
|
||||||
| `Aussentemperatur` | Float / Anzeige | Heizkurveneingang in °C. |
|
| `Puffertemperatur` | Float | Aktueller, gegebenenfalls geglaetteter Wert |
|
||||||
| `TemperaturenGueltig` | Boolean / Anzeige | Beide Quellen sind gültig. |
|
| `Aussentemperatur` | Float | Heizkurveneingang |
|
||||||
| `Solltemperatur` | Float / Anzeige | Berechneter und begrenzter Sollwert in °C. |
|
| `Solltemperatur` | Float | Berechneter und begrenzter Sollwert |
|
||||||
| `Heizbedarf` | Boolean / Anzeige | Bedarf unter Berücksichtigung der Hysterese. |
|
| `Heizbedarf` | Boolean | Temperatur liegt unter der Einschaltschwelle |
|
||||||
| `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
|
Optional sichtbar sind die gemeinsamen Verbraucher-Diagnosevariablen sowie
|
||||||
|
`AktiveStufe`, `BezogeneEnergie` und `TemperaturenGueltig`.
|
||||||
|
|
||||||
| Ident | Typ | Standard / Beschreibung |
|
## Managerkommunikation
|
||||||
| --- | --- | --- |
|
|
||||||
| `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,
|
Das Modul implementiert `VerbraucherSchnittstelle` und Vertrag `4.0`.
|
||||||
Zeitplan und Zeitzone entfallen.
|
Es uebernimmt zuerst die vom Manager angekuendigte Betriebsart, berechnet das
|
||||||
|
passende Leistungsangebot und meldet es mit derselben Betriebsart zurueck.
|
||||||
|
Erst danach wird eine Sollleistung angenommen.
|
||||||
|
|
||||||
## Zustand
|
Die Rueckmeldung enthaelt zusaetzlich:
|
||||||
|
|
||||||
`Puffertemperatur_C` und `Aussentemperatur_C` als Istwerte,
|
- `Puffertemperatur_C`,
|
||||||
`Solltemperatur_C` als Sollwert, `Heizbedarf` als Status und `Fuehlerfehler` als
|
- `Aussentemperatur_C`,
|
||||||
Störung.
|
- `Solltemperatur_C`,
|
||||||
|
- `Heizbedarf`,
|
||||||
|
- `AktiveStufe`,
|
||||||
|
- `Fuehlerfehler` und `Schaltfehler`.
|
||||||
|
|
||||||
## Offene Punkte
|
## Sicherheit und Schaltung
|
||||||
|
|
||||||
- Optionale Einbindung der Wärmepumpe beim Pufferheizen ausarbeiten.
|
Die Kontakte werden Break-before-make geschaltet: zuerst alle aus, danach
|
||||||
|
hoechstens eine Stufe ein. Ein Schaltfehler fuehrt zu einem Ausschaltversuch
|
||||||
|
aller Kontakte und einer Stoerungsmeldung. Hardwareseitige Verriegelungen,
|
||||||
|
Temperaturbegrenzer und Schutzorgane bleiben erforderlich.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `PufferspeicherReglerTest` prueft Heizkurve, Hysterese, Ein/Aus, Peak und
|
||||||
|
alle Mindesttemperatur-Modi.
|
||||||
|
- `PufferspeicherModulstrukturTest` prueft Metadaten, Properties, Formular,
|
||||||
|
Vertrag und Schaltfolge.
|
||||||
|
- Der standardisierte Symcon-Test prueft die reale Modulinstanz und ist im
|
||||||
|
gemeinsamen Manifest sowie in der Manager-Matrix registriert.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# EMS-Module und Modulentwürfe
|
# EMS-Module und Modulentwürfe
|
||||||
|
|
||||||
> Manager, Warmwassererwaermer und Verbraucher 1-Stufig sind als installierbare
|
> Manager, Warmwassererwaermer, Verbraucher 1-Stufig und Ladestation
|
||||||
> IP-Symcon-Module umgesetzt. Die weiteren Ordner enthalten Besprechungsgrundlagen.
|
> Stand-Alone sind als installierbare IP-Symcon-Module umgesetzt. Die weiteren
|
||||||
|
> Ordner enthalten Besprechungsgrundlagen.
|
||||||
|
|
||||||
Alle steuerbaren Verbraucher verwenden die gemeinsamen Datenpunkte aus der
|
Alle steuerbaren Verbraucher verwenden die gemeinsamen Datenpunkte aus der
|
||||||
[EMS-Schnittstelle](../Schnittstelle.md). In den Modul-READMEs stehen deshalb
|
[EMS-Schnittstelle](../Schnittstelle.md). In den Modul-READMEs stehen deshalb
|
||||||
@@ -15,7 +16,7 @@ nur zusätzliche Properties, Variablen, Zustände und offene Punkte.
|
|||||||
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
||||||
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher (implementiert) |
|
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher (implementiert) |
|
||||||
| [Wärmepumpe](Waermepumpe/README.md) | Sperrkontakt oder SG Ready |
|
| [Wärmepumpe](Waermepumpe/README.md) | Sperrkontakt oder SG Ready |
|
||||||
| [Ladestation Stand-Alone](Ladestation-Stand-Alone/README.md) | Direkte Geräteanbindung |
|
| [Ladestation Stand-Alone](Ladestation-Stand-Alone/README.md) | Direkte Geräteanbindung (implementiert) |
|
||||||
| [Ladestation Gateway](Ladestation-Gateway/README.md) | Ladestation am Easee Gateway |
|
| [Ladestation Gateway](Ladestation-Gateway/README.md) | Ladestation am Easee Gateway |
|
||||||
| [Easee Gateway](Easee-Gateway/README.md) | Gemeinsame Easee-Kommunikation |
|
| [Easee Gateway](Easee-Gateway/README.md) | Gemeinsame Easee-Kommunikation |
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
# Verbraucher 1-Stufig
|
# Verbraucher 1-Stufig
|
||||||
|
|
||||||
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `3.0`.
|
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `4.0`.
|
||||||
|
|
||||||
Das Modul schaltet einen elektrischen Ein-/Aus-Verbraucher. Im freien Zustand
|
Das Modul schaltet einen elektrischen Ein-/Aus-Verbraucher. Im frei
|
||||||
meldet es dem Manager das Leistungsangebot `[0, Nennleistung]`. Waerend einer
|
schaltbaren Zustand meldet es bei PV `[0, Nennleistung]` und bei Peak `[0]`.
|
||||||
Mindest-Ein- oder Mindest-Aus-Zeit, waehrend einer ausstehenden Rueckmeldung
|
Ist die Tagesmindestlaufzeit faellig, wird bei PV die Nennleistung erzwungen.
|
||||||
und bei lokal erzwungener Tagesmindestlaufzeit wird nur der aktuelle
|
Bei Peak ist konfigurierbar, ob `[0, Nennleistung]` angeboten oder die
|
||||||
bestaetigte Leistungswert angeboten.
|
Nennleistung ohne Sperrmoeglichkeit erzwungen wird. Waerend einer
|
||||||
|
Mindest-Ein- oder Mindest-Aus-Zeit sowie bei einer ausstehenden Rueckmeldung
|
||||||
|
wird unabhaengig von der Betriebsart nur der aktuelle bestaetigte
|
||||||
|
Leistungswert angeboten.
|
||||||
|
|
||||||
## Architekturentscheidung
|
## Architekturentscheidung
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ Regelberechnung.
|
|||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
Zu den sechs gemeinsamen Verbraucher-Properties aus
|
Zu den sechs gemeinsamen Verbraucher-Properties aus
|
||||||
[`Schnittstelle.md`](../../Schnittstelle.md) kommen acht
|
[`Schnittstelle.md`](../../Schnittstelle.md) kommen neun
|
||||||
Modul-Properties hinzu.
|
Modul-Properties hinzu.
|
||||||
|
|
||||||
| Ident | Typ | Standard | Beschreibung |
|
| Ident | Typ | Standard | Beschreibung |
|
||||||
@@ -37,6 +40,7 @@ Modul-Properties hinzu.
|
|||||||
| `SchaltkontaktInvertiert` | Boolean | `false` | Kehrt die Ein-/Aus-Semantik des Aktors um. |
|
| `SchaltkontaktInvertiert` | Boolean | `false` | Kehrt die Ein-/Aus-Semantik des Aktors um. |
|
||||||
| `RueckmeldungVariableID` | Integer | `0` | Optionale Boolean-Rueckmeldung; `true` bedeutet physisch eingeschaltet. |
|
| `RueckmeldungVariableID` | Integer | `0` | Optionale Boolean-Rueckmeldung; `true` bedeutet physisch eingeschaltet. |
|
||||||
| `Mindestlaufzeit` | Integer | `0` | Geforderte Laufzeit pro lokalem Kalendertag in Sekunden, maximal 86400. |
|
| `Mindestlaufzeit` | Integer | `0` | Geforderte Laufzeit pro lokalem Kalendertag in Sekunden, maximal 86400. |
|
||||||
|
| `PeakSperreBeiMindestlaufzeitAnbieten` | Boolean | `true` | Bietet bei faelliger Tagesmindestlaufzeit im Peakbetrieb zusaetzlich `0` als Sperre an. Bei `false` wird die Nennleistung lokal erzwungen. |
|
||||||
| `Mindesteinschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Ein-Zustand gehalten wird. |
|
| `Mindesteinschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Ein-Zustand gehalten wird. |
|
||||||
| `Mindestausschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Aus-Zustand gehalten wird. |
|
| `Mindestausschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Aus-Zustand gehalten wird. |
|
||||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt gemeinsame und modulspezifische Diagnosevariablen an. |
|
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt gemeinsame und modulspezifische Diagnosevariablen an. |
|
||||||
@@ -126,15 +130,20 @@ beruecksichtigt. Nur der bestaetigte Ein-Zustand zaehlt.
|
|||||||
|
|
||||||
Ist die konfigurierte Tagesmindestlaufzeit noch nicht erreicht, wird der
|
Ist die konfigurierte Tagesmindestlaufzeit noch nicht erreicht, wird der
|
||||||
Verbraucher zum spaetestmoeglichen Zeitpunkt angefordert, an dem die
|
Verbraucher zum spaetestmoeglichen Zeitpunkt angefordert, an dem die
|
||||||
Restlaufzeit bis Mitternacht noch erfuellt werden kann. Eine noch aktive
|
Restlaufzeit bis Mitternacht noch erfuellt werden kann. Im PV-Betrieb wird die
|
||||||
Mindestausschaltdauer hat dabei Vorrang; eine unmoegliche Restlaufzeit kann
|
Nennleistung dann lokal erzwungen. Im Peakbetrieb meldet das Modul bei
|
||||||
nicht rueckwirkend nachgeholt werden.
|
aktivierter Property `PeakSperreBeiMindestlaufzeitAnbieten` wie Enelix 1
|
||||||
|
`[0, Nennleistung]`, sodass der Manager zwischen Sperren und Nachholen der
|
||||||
|
Laufzeit waehlen kann. Ist die Property deaktiviert, wird auch im Peakbetrieb
|
||||||
|
nur die Nennleistung angeboten und lokal erzwungen. Eine noch aktive
|
||||||
|
Mindestausschaltdauer hat Vorrang; eine unmoegliche Restlaufzeit kann nicht
|
||||||
|
rueckwirkend nachgeholt werden.
|
||||||
|
|
||||||
## Managerkommunikation
|
## Managerkommunikation
|
||||||
|
|
||||||
Das Modul implementiert
|
Das Modul implementiert
|
||||||
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
|
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
|
||||||
ausschliesslich Vertrag `3.0`. Der Verbraucher besitzt keine
|
ausschliesslich Vertrag `4.0`. Der Verbraucher besitzt keine
|
||||||
Manager-ID-Property. Er akzeptiert nur Manager, in deren manueller oder
|
Manager-ID-Property. Er akzeptiert nur Manager, in deren manueller oder
|
||||||
automatischer Verbraucherzuordnung seine Instanz aktiv eingetragen ist.
|
automatischer Verbraucherzuordnung seine Instanz aktiv eingetragen ist.
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Verbraucher Warmwassererwaermer
|
# Verbraucher Warmwassererwaermer
|
||||||
|
|
||||||
> Status: implementiert fuer IP-Symcon 8.0+ und den Enelix-2-Vertrag `3.0`.
|
> Status: implementiert fuer IP-Symcon 8.0+ und den Enelix-2-Vertrag `4.0`.
|
||||||
|
|
||||||
Das Modul bindet einen elektrischen Warmwasserspeicher mit einer oder mehreren
|
Das Modul bindet einen elektrischen Warmwasserspeicher mit einer oder mehreren
|
||||||
exklusiven Leistungsstufen an den Enelix-Manager an. Der Manager kann nur eine
|
exklusiven Leistungsstufen an den Enelix-Manager an. Der Manager kann nur eine
|
||||||
der aktuell angebotenen Leistungen vorgeben. Lokale Schutz- und Komfortregeln
|
der aktuell angebotenen Leistungen vorgeben. Mindesttemperatur, Hysterese,
|
||||||
fuer Mindesttemperatur, Maximaltemperatur, Zeitplan und Legionellenfunktion
|
Zeitplan und Legionellenfunktion bestimmen dabei zustands- und
|
||||||
haben Vorrang vor einer Manager-Vorgabe.
|
betriebsartabhaengig das Leistungsangebot.
|
||||||
|
|
||||||
Das Modul ist eine gezielte Adaption des Enelix-1-Moduls
|
Das Modul ist eine gezielte Adaption des Enelix-1-Moduls
|
||||||
`Boiler_x_Stufig`. Zyklische Altlogik und alte Kommunikationsvariablen wurden
|
`Boiler_x_Stufig`. Zyklische Altlogik und alte Kommunikationsvariablen wurden
|
||||||
@@ -20,7 +20,7 @@ ausschliesslich den Enelix-2-Nachrichtenvertrag.
|
|||||||
| Modul-ID | `{B7C54AF4-AD7D-4FE4-B75D-203693906251}` |
|
| Modul-ID | `{B7C54AF4-AD7D-4FE4-B75D-203693906251}` |
|
||||||
| Modultyp | Geraeteinstanz (`3`) |
|
| Modultyp | Geraeteinstanz (`3`) |
|
||||||
| Funktionspraefix | `ENELIX` |
|
| Funktionspraefix | `ENELIX` |
|
||||||
| Enelix-Vertrag | `3.0` |
|
| Enelix-Vertrag | `4.0` |
|
||||||
|
|
||||||
## Funktionsumfang
|
## Funktionsumfang
|
||||||
|
|
||||||
@@ -65,22 +65,22 @@ Eine Neuberechnung wird insbesondere ausgeloest durch:
|
|||||||
vollstaendige Rueckmeldung sicher. Die Lastwechselsperre verwendet einen
|
vollstaendige Rueckmeldung sicher. Die Lastwechselsperre verwendet einen
|
||||||
einmaligen Timer und loest nach ihrem Ablauf genau eine Neuberechnung aus.
|
einmaligen Timer und loest nach ihrem Ablauf genau eine Neuberechnung aus.
|
||||||
|
|
||||||
Die Zielentscheidung folgt dieser Prioritaet:
|
Die Zielentscheidung folgt der Enelix-1-Zustandsmatrix:
|
||||||
|
|
||||||
1. Bei deaktivierter Instanz oder ungueltiger Temperatur ist der Verbraucher
|
1. Bei deaktivierter Instanz oder ungueltiger Temperatur ist der Verbraucher
|
||||||
nicht verfuegbar und wird ausgeschaltet. Eine ungueltige Konfiguration wird
|
nicht verfuegbar und wird ausgeschaltet. Eine ungueltige Konfiguration wird
|
||||||
bereits beim Uebernehmen mit Status `202` abgewiesen.
|
bereits beim Uebernehmen mit Status `202` abgewiesen.
|
||||||
2. Unterschreitet die Temperatur die wirksame Mindesttemperatur oder reicht die
|
2. Unter der wirksamen Mindesttemperatur sowie in der unteren Hysterese bei
|
||||||
verbleibende Zeit bis zum naechsten Zeitplanziel rechnerisch nicht aus, wird
|
aktiver Stufe gilt: PV erzwingt die Maximalleistung, Peak bietet
|
||||||
die hoechste Leistungsstufe lokal erzwungen.
|
`[0, ...Leistungsstufen]` an.
|
||||||
3. Ab der wirksamen Maximaltemperatur wird ausgeschaltet.
|
3. Ausserhalb dieses Mindesttemperaturbereichs bietet Peak nur `[0]` an.
|
||||||
4. Im Bereich unmittelbar unter der Maximaltemperatur wird eine bereits aktive
|
4. Im PV-Betrieb werden unterhalb von `Maximaltemperatur - Hysterese` alle
|
||||||
Stufe bis zum Erreichen der Abschaltgrenze gehalten.
|
Stufen angeboten. In der oberen Hysterese gilt dies nur, solange bereits
|
||||||
5. In allen anderen Zustaenden darf der Manager zwischen `0 W` und allen
|
eine Stufe aktiv ist.
|
||||||
konfigurierten Leistungsstufen waehlen.
|
5. An oder oberhalb der wirksamen Maximaltemperatur wird ausgeschaltet.
|
||||||
|
|
||||||
Lokale Zwangsvorgaben werden mit `AenderungMoeglich=false` und einem
|
Eine erzwungene PV-Maximalleistung wird mit `AenderungMoeglich=false`
|
||||||
entsprechend eingeschraenkten `Leistungswerte_W` gemeldet.
|
gemeldet. Das Peak-Array bleibt dagegen durch den Manager waehlbar.
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
@@ -129,10 +129,11 @@ Stufe konfiguriert werden.
|
|||||||
|
|
||||||
Der jeweils naechste Zeitplaneintrag wird fuer heute oder den folgenden Tag
|
Der jeweils naechste Zeitplaneintrag wird fuer heute oder den folgenden Tag
|
||||||
ermittelt. Reicht die verbleibende Zeit bei maximaler elektrischer Leistung
|
ermittelt. Reicht die verbleibende Zeit bei maximaler elektrischer Leistung
|
||||||
rechnerisch nicht mehr aus, wird sofort die hoechste Stufe angefordert. Die
|
rechnerisch nicht mehr aus, wird das Zeitplanziel zur wirksamen
|
||||||
Berechnung verwendet Wasser mit `4186 J/(kg K)` und beruecksichtigt keine
|
Mindesttemperatur. Im PV-Betrieb wird dann die hoechste Stufe erzwungen, im
|
||||||
Speicher- oder Leitungsverluste. Fuer Datum und Uhrzeit gilt die in IP-Symcon
|
Peakbetrieb werden alle Stufen angeboten. Die Berechnung verwendet Wasser mit
|
||||||
eingestellte lokale Zeitzone.
|
`4186 J/(kg K)` und beruecksichtigt keine Speicher- oder Leitungsverluste. Fuer
|
||||||
|
Datum und Uhrzeit gilt die in IP-Symcon eingestellte lokale Zeitzone.
|
||||||
|
|
||||||
### Erweiterte sonstige Einstellungen
|
### Erweiterte sonstige Einstellungen
|
||||||
|
|
||||||
@@ -179,14 +180,14 @@ nur erlaubt, wenn `EinstellungenInVisu=true` gesetzt ist.
|
|||||||
|
|
||||||
## Hysterese und lokales Nachladen
|
## Hysterese und lokales Nachladen
|
||||||
|
|
||||||
Sinkt die Temperatur unter die wirksame Mindesttemperatur, beginnt das lokale
|
Sinkt die Temperatur unter die wirksame Mindesttemperatur, erzwingt PV die
|
||||||
Nachladen mit der hoechsten Leistungsstufe. Es endet, sobald mindestens
|
hoechste Leistungsstufe; Peak bietet dagegen Aus und alle Stufen an. In der
|
||||||
`Mindesttemperatur + Hysterese` erreicht ist. Oberhalb der wirksamen
|
unteren Hysterese bis `Mindesttemperatur + Hysterese` bleibt dieses Verhalten
|
||||||
Maximaltemperatur wird ausgeschaltet.
|
nur erhalten, solange bereits eine Stufe aktiv ist.
|
||||||
|
|
||||||
Ist bereits eine Stufe aktiv und liegt die Temperatur bei mindestens
|
Im PV-Betrieb werden unter `Maximaltemperatur - Hysterese` alle Stufen
|
||||||
`Maximaltemperatur - Hysterese`, wird diese Stufe bis zur Abschaltgrenze
|
angeboten. Innerhalb der oberen Hysterese bleibt das Angebot nur bei einer
|
||||||
gehalten. Dadurch werden unnoetige Stufenwechsel kurz vor dem Ziel vermieden.
|
bereits aktiven Stufe erhalten. An der Maximaltemperatur wird ausgeschaltet.
|
||||||
|
|
||||||
## Legionellenfunktion
|
## Legionellenfunktion
|
||||||
|
|
||||||
@@ -196,8 +197,8 @@ Die Legionellenfunktion arbeitet zweistufig:
|
|||||||
wirksamen Maximaltemperatur. Der Manager kann die Aufheizung innerhalb des
|
wirksamen Maximaltemperatur. Der Manager kann die Aufheizung innerhalb des
|
||||||
verbleibenden Zeitfensters ermoeglichen.
|
verbleibenden Zeitfensters ermoeglichen.
|
||||||
2. Ab `LegionellenMaximalintervallTage` wird die Legionellentemperatur auch zur
|
2. Ab `LegionellenMaximalintervallTage` wird die Legionellentemperatur auch zur
|
||||||
wirksamen Mindesttemperatur. Die hoechste Stufe wird dadurch lokal
|
wirksamen Mindesttemperatur. PV erzwingt dadurch die Maximalleistung; Peak
|
||||||
erzwungen, bis das Ziel erreicht ist.
|
bietet das vollstaendige Stufenarray bis zum Ziel an.
|
||||||
|
|
||||||
Erreicht die gueltige Speichertemperatur die Legionellentemperatur, wird der
|
Erreicht die gueltige Speichertemperatur die Legionellentemperatur, wird der
|
||||||
Zeitpunkt als erfolgreicher Abschluss gespeichert und das Intervall beginnt
|
Zeitpunkt als erfolgreicher Abschluss gespeichert und das Intervall beginnt
|
||||||
@@ -268,7 +269,7 @@ Anzeigevariablen.
|
|||||||
|
|
||||||
Das Modul implementiert
|
Das Modul implementiert
|
||||||
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet den
|
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet den
|
||||||
Enelix-2-Vertrag `3.0`. Es besitzt keine Manager-ID-Property. Zugelassen sind
|
Enelix-2-Vertrag `4.0`. Es besitzt keine Manager-ID-Property. Zugelassen sind
|
||||||
nur Manager, in deren manueller oder automatischer Verbraucherzuordnung die
|
nur Manager, in deren manueller oder automatischer Verbraucherzuordnung die
|
||||||
Instanz aktiv eingetragen ist.
|
Instanz aktiv eingetragen ist.
|
||||||
|
|
||||||
@@ -382,7 +383,7 @@ PT1, Lastwechselsperre, Energieberechnung, thermische Prognose, Zeitplan und
|
|||||||
Legionellengrenzen.
|
Legionellengrenzen.
|
||||||
|
|
||||||
`WarmwassererwaermerModulstrukturTest.php` prueft Metadaten, Formular, alle 19
|
`WarmwassererwaermerModulstrukturTest.php` prueft Metadaten, Formular, alle 19
|
||||||
Properties, ereignisbasierte Lastwechselsperre, Vertrag `3.0`, bedarfsgesteuerte
|
Properties, ereignisbasierte Lastwechselsperre, Vertrag `4.0`, bedarfsgesteuerte
|
||||||
Diagnosevariablen, Legionellentemperatur, Temperatursollwerte und
|
Diagnosevariablen, Legionellentemperatur, Temperatursollwerte und
|
||||||
Break-before-make-Schaltung.
|
Break-before-make-Schaltung.
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ werden, wenn sie vor dem Lauf nicht existierten.
|
|||||||
- `single`: genau die als Auswahl übergebenen Module
|
- `single`: genau die als Auswahl übergebenen Module
|
||||||
- `affected`: die durch geänderte Pfade ermittelten Module
|
- `affected`: die durch geänderte Pfade ermittelten Module
|
||||||
|
|
||||||
Verfügbare Module: `Manager`, `VerbraucherEinStufig`, `Warmwassererwaermer`.
|
Verfügbare Module: `LadestationStandAlone`, `Manager`, `VerbraucherEinStufig`, `Warmwassererwaermer`.
|
||||||
|
|
||||||
Der Manager-Test enthält Manager ohne Verbraucher, jeden Verbrauchertyp einzeln und alle aktuell implementierten Verbrauchertypen gemeinsam.
|
Der Manager-Test enthält Manager ohne Verbraucher, jeden Verbrauchertyp einzeln und alle aktuell implementierten Verbrauchertypen gemeinsam.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const ENELIX_DEMO_ROOT_IDENT = 'ENELIX_DEMO_2';
|
|||||||
const ENELIX_DEMO_MANAGER_MODULE = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
const ENELIX_DEMO_MANAGER_MODULE = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
||||||
const ENELIX_DEMO_BOILER_MODULE = '{B7C54AF4-AD7D-4FE4-B75D-203693906251}';
|
const ENELIX_DEMO_BOILER_MODULE = '{B7C54AF4-AD7D-4FE4-B75D-203693906251}';
|
||||||
const ENELIX_DEMO_SINGLE_MODULE = '{15879A4E-D0C2-4495-83DE-46E1E462591E}';
|
const ENELIX_DEMO_SINGLE_MODULE = '{15879A4E-D0C2-4495-83DE-46E1E462591E}';
|
||||||
|
const ENELIX_DEMO_DUMMY_MODULE = '{485D0419-BE97-4548-AA9C-C083EB82E61E}';
|
||||||
const ENELIX_DEMO_TILE_MODULE = '{B5B875BB-9B76-45FD-4E67-2607E45B3AC4}';
|
const ENELIX_DEMO_TILE_MODULE = '{B5B875BB-9B76-45FD-4E67-2607E45B3AC4}';
|
||||||
const ENELIX_DEMO_ARCHIVE_MODULE = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
|
const ENELIX_DEMO_ARCHIVE_MODULE = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
|
||||||
const ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE = '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}';
|
const ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE = '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}';
|
||||||
@@ -37,12 +38,14 @@ function enelixDemoInstall(): array
|
|||||||
$boiler2Power = enelixDemoEnsureVariable($dashboard, 'Boiler2Leistung', 'Boiler 2 Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 70);
|
$boiler2Power = enelixDemoEnsureVariable($dashboard, 'Boiler2Leistung', 'Boiler 2 Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 70);
|
||||||
$dryerPower = enelixDemoEnsureVariable($dashboard, 'EntfeuchterLeistung', 'Entfeuchter Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 80);
|
$dryerPower = enelixDemoEnsureVariable($dashboard, 'EntfeuchterLeistung', 'Entfeuchter Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 80);
|
||||||
|
|
||||||
$powerChart = enelixDemoEnsureVariable($history, 'Leistungsdiagramm', 'Leistungsverlauf', 3, '~HTMLBox', '', 10);
|
$powerChart = enelixDemoEnsureVariable($history, 'Leistungsdiagramm', 'Leistungsverlauf (Legacy)', 3, '~HTMLBox', '', 90);
|
||||||
$energyChart = enelixDemoEnsureVariable($history, 'Energiediagramm', 'Kumulierte Energien', 3, '~HTMLBox', '', 20);
|
$energyChart = enelixDemoEnsureVariable($history, 'Energiediagramm', 'Kumulierte Energien (Legacy)', 3, '~HTMLBox', '', 100);
|
||||||
$pvEnergy = enelixDemoEnsureVariable($history, 'PVEnergie', 'PV-Energie', 2, 'ENELIX.Demo.Energy', 0.0, 30);
|
IPS_SetHidden($powerChart, true);
|
||||||
$houseEnergy = enelixDemoEnsureVariable($history, 'HausEnergie', 'Hausverbrauch Energie', 2, 'ENELIX.Demo.Energy', 0.0, 40);
|
IPS_SetHidden($energyChart, true);
|
||||||
$gridImportEnergy = enelixDemoEnsureVariable($history, 'NetzbezugEnergie', 'Netzbezug Energie', 2, 'ENELIX.Demo.Energy', 0.0, 50);
|
$pvEnergy = enelixDemoEnsureVariable($history, 'PVEnergie', 'PV-Energie', 2, '~Electricity', 0.0, 30);
|
||||||
$gridExportEnergy = enelixDemoEnsureVariable($history, 'EinspeisungEnergie', 'Einspeisung Energie', 2, 'ENELIX.Demo.Energy', 0.0, 60);
|
$houseEnergy = enelixDemoEnsureVariable($history, 'HausEnergie', 'Hausverbrauch Energie', 2, '~Electricity', 0.0, 40);
|
||||||
|
$gridImportEnergy = enelixDemoEnsureVariable($history, 'NetzbezugEnergie', 'Netzbezug Energie', 2, '~Electricity', 0.0, 50);
|
||||||
|
$gridExportEnergy = enelixDemoEnsureVariable($history, 'EinspeisungEnergie', 'Einspeisung Energie', 2, '~Electricity', 0.0, 60);
|
||||||
|
|
||||||
$auto = enelixDemoEnsureVariable($simulation, 'Automatik', 'Tageslauf automatisch', 0, '~Switch', true, 10);
|
$auto = enelixDemoEnsureVariable($simulation, 'Automatik', 'Tageslauf automatisch', 0, '~Switch', true, 10);
|
||||||
$hour = enelixDemoEnsureVariable($simulation, 'Tageszeit', 'Simulierte Tageszeit', 2, 'ENELIX.Demo.Hour', 8.0, 20);
|
$hour = enelixDemoEnsureVariable($simulation, 'Tageszeit', 'Simulierte Tageszeit', 2, 'ENELIX.Demo.Hour', 8.0, 20);
|
||||||
@@ -89,6 +92,21 @@ function enelixDemoInstall(): array
|
|||||||
$chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 170);
|
$chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 170);
|
||||||
IPS_SetHidden($chartUpdated, true);
|
IPS_SetHidden($chartUpdated, true);
|
||||||
|
|
||||||
|
$flowPv = enelixDemoEnsureVariable($technical, 'EnergieflussPVkW', 'PV-Leistung', 2, '~Power', 0.0, 300);
|
||||||
|
$flowGrid = enelixDemoEnsureVariable($technical, 'EnergieflussNetzkW', 'Netzleistung', 2, '~Power', 0.0, 310);
|
||||||
|
$flowBase = enelixDemoEnsureVariable($technical, 'EnergieflussGrundlastkW', 'Haus-Grundlast', 2, '~Power', 0.0, 320);
|
||||||
|
$flowBoiler1 = enelixDemoEnsureVariable($technical, 'EnergieflussBoiler1kW', 'Boiler 1', 2, '~Power', 0.0, 330);
|
||||||
|
$flowBoiler2 = enelixDemoEnsureVariable($technical, 'EnergieflussBoiler2kW', 'Boiler 2', 2, '~Power', 0.0, 340);
|
||||||
|
$flowDryer = enelixDemoEnsureVariable($technical, 'EnergieflussEntfeuchterkW', 'Entfeuchter', 2, '~Power', 0.0, 350);
|
||||||
|
$flowHouse = enelixDemoEnsureVariable($technical, 'DiagrammHausverbrauchkW', 'Hausverbrauch gesamt', 2, '~Power', 0.0, 360);
|
||||||
|
$flowLoads = enelixDemoEnsureVariable($technical, 'DiagrammVerbraucherkW', 'Geregelte Verbraucher', 2, '~Power', 0.0, 370);
|
||||||
|
foreach ([
|
||||||
|
$flowPv, $flowGrid, $flowBase, $flowBoiler1,
|
||||||
|
$flowBoiler2, $flowDryer, $flowHouse, $flowLoads,
|
||||||
|
] as $flowVariableId) {
|
||||||
|
IPS_SetHidden($flowVariableId, true);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact] as $actorId) {
|
foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact] as $actorId) {
|
||||||
IPS_SetVariableCustomAction($actorId, $actorScript);
|
IPS_SetVariableCustomAction($actorId, $actorScript);
|
||||||
}
|
}
|
||||||
@@ -148,19 +166,24 @@ function enelixDemoInstall(): array
|
|||||||
|
|
||||||
$energyDistribution = enelixDemoEnsureInstance(
|
$energyDistribution = enelixDemoEnsureInstance(
|
||||||
$dashboard,
|
$dashboard,
|
||||||
'Energieverteilung',
|
'EnergieverteilungNativ',
|
||||||
'Energieverteilung',
|
'Energiefluss',
|
||||||
ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE,
|
ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE,
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
enelixDemoConfigureEnergyDistribution($energyDistribution, [
|
enelixDemoConfigureEnergyDistribution($energyDistribution, [
|
||||||
enelixDemoEnergyNode($pvDisplay, 'PV-Anlage', 'solar'),
|
enelixDemoEnergyNode($flowPv, 'PV-Anlage', 'solar'),
|
||||||
enelixDemoEnergyNode($grid, 'Stromnetz', 'grid'),
|
enelixDemoEnergyNode($flowGrid, 'Stromnetz', 'grid'),
|
||||||
enelixDemoEnergyNode($base, 'Haus-Grundlast', 'consumer'),
|
enelixDemoEnergyNode($flowBase, 'Haus-Grundlast', 'consumer'),
|
||||||
enelixDemoEnergyNode($boiler1Power, 'Boiler 1', 'consumer', $temp1),
|
enelixDemoEnergyNode($flowBoiler1, 'Boiler 1', 'consumer', $temp1),
|
||||||
enelixDemoEnergyNode($boiler2Power, 'Boiler 2', 'consumer', $temp2),
|
enelixDemoEnergyNode($flowBoiler2, 'Boiler 2', 'consumer', $temp2),
|
||||||
enelixDemoEnergyNode($dryerPower, 'Entfeuchter', 'consumer'),
|
enelixDemoEnergyNode($flowDryer, 'Entfeuchter', 'consumer'),
|
||||||
]);
|
]);
|
||||||
|
IPS_SetHidden($energyDistribution, false);
|
||||||
|
$oldEnergyDistribution = enelixDemoFind($dashboard, 'Energieverteilung');
|
||||||
|
if ($oldEnergyDistribution > 0 && $oldEnergyDistribution !== $energyDistribution) {
|
||||||
|
IPS_SetHidden($oldEnergyDistribution, true);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ([$boiler1, $boiler2] as $boilerId) {
|
foreach ([$boiler1, $boiler2] as $boilerId) {
|
||||||
IPS_RequestAction($boilerId, 'Mindesttemperatur', 42.0);
|
IPS_RequestAction($boilerId, 'Mindesttemperatur', 42.0);
|
||||||
@@ -194,16 +217,37 @@ function enelixDemoInstall(): array
|
|||||||
}
|
}
|
||||||
IPS_RequestAction($manager, 'Aktiv', true);
|
IPS_RequestAction($manager, 'Aktiv', true);
|
||||||
|
|
||||||
enelixDemoLinkInstanceVariables($boiler1View, $boiler1, $temp1);
|
$boiler1List = enelixDemoEnsureInstance(
|
||||||
enelixDemoLinkInstanceVariables($boiler2View, $boiler2, $temp2);
|
$dashboard,
|
||||||
|
'Boiler1Liste',
|
||||||
|
'Boiler 1',
|
||||||
|
ENELIX_DEMO_DUMMY_MODULE,
|
||||||
|
110
|
||||||
|
);
|
||||||
|
$boiler2List = enelixDemoEnsureInstance(
|
||||||
|
$dashboard,
|
||||||
|
'Boiler2Liste',
|
||||||
|
'Boiler 2',
|
||||||
|
ENELIX_DEMO_DUMMY_MODULE,
|
||||||
|
120
|
||||||
|
);
|
||||||
|
enelixDemoConfigure($boiler1List, []);
|
||||||
|
enelixDemoConfigure($boiler2List, []);
|
||||||
|
enelixDemoLinkInstanceVariables($boiler1List, $boiler1, $temp1);
|
||||||
|
enelixDemoLinkInstanceVariables($boiler2List, $boiler2, $temp2);
|
||||||
IPS_SetHidden($boiler1View, true);
|
IPS_SetHidden($boiler1View, true);
|
||||||
IPS_SetHidden($boiler2View, true);
|
IPS_SetHidden($boiler2View, true);
|
||||||
IPS_SetHidden($boiler1, false);
|
IPS_SetHidden($boiler1, true);
|
||||||
IPS_SetHidden($boiler2, false);
|
IPS_SetHidden($boiler2, true);
|
||||||
|
|
||||||
|
foreach (['Boiler1Link', 'Boiler2Link'] as $oldLinkIdent) {
|
||||||
|
$oldLink = enelixDemoFind($dashboard, $oldLinkIdent);
|
||||||
|
if ($oldLink > 0) {
|
||||||
|
IPS_SetHidden($oldLink, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enelixDemoEnsureLink($dashboard, 'ManagerLink', 'Manager', $manager, 100);
|
enelixDemoEnsureLink($dashboard, 'ManagerLink', 'Manager', $manager, 100);
|
||||||
enelixDemoEnsureLink($dashboard, 'Boiler1Link', 'Boiler 1', $boiler1, 110);
|
|
||||||
enelixDemoEnsureLink($dashboard, 'Boiler2Link', 'Boiler 2', $boiler2, 120);
|
|
||||||
enelixDemoEnsureLink($dashboard, 'EntfeuchterLink', 'Entfeuchter', $dryer, 130);
|
enelixDemoEnsureLink($dashboard, 'EntfeuchterLink', 'Entfeuchter', $dryer, 130);
|
||||||
enelixDemoEnsureLink($simulation, 'Boiler1TemperaturLink', 'Boiler 1 Temperatur', $temp1, 70);
|
enelixDemoEnsureLink($simulation, 'Boiler1TemperaturLink', 'Boiler 1 Temperatur', $temp1, 70);
|
||||||
enelixDemoEnsureLink($simulation, 'Boiler2TemperaturLink', 'Boiler 2 Temperatur', $temp2, 80);
|
enelixDemoEnsureLink($simulation, 'Boiler2TemperaturLink', 'Boiler 2 Temperatur', $temp2, 80);
|
||||||
@@ -213,7 +257,21 @@ function enelixDemoInstall(): array
|
|||||||
$boiler1Power, $boiler2Power, $dryerPower,
|
$boiler1Power, $boiler2Power, $dryerPower,
|
||||||
$pvEnergy, $houseEnergy, $gridImportEnergy, $gridExportEnergy,
|
$pvEnergy, $houseEnergy, $gridImportEnergy, $gridExportEnergy,
|
||||||
$temp1, $temp2,
|
$temp1, $temp2,
|
||||||
|
$flowPv, $flowGrid, $flowBase, $flowBoiler1, $flowBoiler2, $flowDryer,
|
||||||
|
$flowHouse, $flowLoads,
|
||||||
]);
|
]);
|
||||||
|
enelixDemoEnsureChart($history, 'LeistungsdiagrammNativ', 'Leistungsverlauf', [
|
||||||
|
enelixDemoChartDataset($flowPv, 'PV', '#f4c542', '~Power'),
|
||||||
|
enelixDemoChartDataset($flowHouse, 'Hausverbrauch', '#2bb673', '~Power'),
|
||||||
|
enelixDemoChartDataset($flowLoads, 'Geregelte Verbraucher', '#2786ac', '~Power'),
|
||||||
|
enelixDemoChartDataset($flowGrid, 'Netzleistung', '#e95d5d', '~Power'),
|
||||||
|
], 10);
|
||||||
|
enelixDemoEnsureChart($history, 'EnergiediagrammNativ', 'Kumulierte Energien', [
|
||||||
|
enelixDemoChartDataset($pvEnergy, 'PV', '#f4c542', '~Electricity'),
|
||||||
|
enelixDemoChartDataset($houseEnergy, 'Hausverbrauch', '#2bb673', '~Electricity'),
|
||||||
|
enelixDemoChartDataset($gridImportEnergy, 'Netzbezug', '#e95d5d', '~Electricity'),
|
||||||
|
enelixDemoChartDataset($gridExportEnergy, 'Einspeisung', '#38a3db', '~Electricity'),
|
||||||
|
], 20);
|
||||||
enelixDemoConfigureVisualization($root, $energyDistribution);
|
enelixDemoConfigureVisualization($root, $energyDistribution);
|
||||||
enelixDemoTick('Install');
|
enelixDemoTick('Install');
|
||||||
|
|
||||||
@@ -297,6 +355,14 @@ function enelixDemoTick(string $source = 'Manual'): void
|
|||||||
SetValue(enelixDemoRequiredId($dashboard, 'Boiler1Leistung'), (float) $boiler1);
|
SetValue(enelixDemoRequiredId($dashboard, 'Boiler1Leistung'), (float) $boiler1);
|
||||||
SetValue(enelixDemoRequiredId($dashboard, 'Boiler2Leistung'), (float) $boiler2);
|
SetValue(enelixDemoRequiredId($dashboard, 'Boiler2Leistung'), (float) $boiler2);
|
||||||
SetValue(enelixDemoRequiredId($dashboard, 'EntfeuchterLeistung'), (float) $dryer);
|
SetValue(enelixDemoRequiredId($dashboard, 'EntfeuchterLeistung'), (float) $dryer);
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'EnergieflussPVkW'), round($pv / 1000.0, 3));
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'EnergieflussNetzkW'), round($grid / 1000.0, 3));
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'EnergieflussGrundlastkW'), round($base / 1000.0, 3));
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'EnergieflussBoiler1kW'), round($boiler1 / 1000.0, 3));
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'EnergieflussBoiler2kW'), round($boiler2 / 1000.0, 3));
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'EnergieflussEntfeuchterkW'), round($dryer / 1000.0, 3));
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'DiagrammHausverbrauchkW'), round($house / 1000.0, 3));
|
||||||
|
SetValue(enelixDemoRequiredId($technical, 'DiagrammVerbraucherkW'), round($power / 1000.0, 3));
|
||||||
|
|
||||||
$history = enelixDemoRequiredId($root, 'Historie');
|
$history = enelixDemoRequiredId($root, 'Historie');
|
||||||
$energyIds = [
|
$energyIds = [
|
||||||
@@ -313,11 +379,6 @@ function enelixDemoTick(string $source = 'Manual'): void
|
|||||||
enelixDemoAddEnergy($energyIds['export'], max(0.0, -$grid), $hours);
|
enelixDemoAddEnergy($energyIds['export'], max(0.0, -$grid), $hours);
|
||||||
}
|
}
|
||||||
|
|
||||||
$chartUpdated = enelixDemoRequiredId($technical, 'LetztesDiagrammUpdate');
|
|
||||||
if ($source !== 'Timer' || time() - (int) GetValue($chartUpdated) >= 10) {
|
|
||||||
enelixDemoUpdateCharts($history, [$pvId, $houseId, $gridId], $energyIds);
|
|
||||||
SetValue($chartUpdated, time());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enelixDemoRenderFlow(array $state): string
|
function enelixDemoRenderFlow(array $state): string
|
||||||
@@ -378,6 +439,7 @@ function enelixDemoValidateRuntime(): void
|
|||||||
ENELIX_DEMO_MANAGER_MODULE,
|
ENELIX_DEMO_MANAGER_MODULE,
|
||||||
ENELIX_DEMO_BOILER_MODULE,
|
ENELIX_DEMO_BOILER_MODULE,
|
||||||
ENELIX_DEMO_SINGLE_MODULE,
|
ENELIX_DEMO_SINGLE_MODULE,
|
||||||
|
ENELIX_DEMO_DUMMY_MODULE,
|
||||||
ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE,
|
ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE,
|
||||||
] as $moduleId) {
|
] as $moduleId) {
|
||||||
if (!in_array($moduleId, $available, true)) {
|
if (!in_array($moduleId, $available, true)) {
|
||||||
@@ -451,6 +513,53 @@ function enelixDemoEnsureVariable(int $parent, string $ident, string $name, int
|
|||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enelixDemoEnsureChart(
|
||||||
|
int $parent,
|
||||||
|
string $ident,
|
||||||
|
string $name,
|
||||||
|
array $datasets,
|
||||||
|
int $position
|
||||||
|
): int {
|
||||||
|
$id = enelixDemoFind($parent, $ident);
|
||||||
|
if ($id === 0) {
|
||||||
|
$id = IPS_CreateMedia(4);
|
||||||
|
IPS_SetParent($id, $parent);
|
||||||
|
IPS_SetIdent($id, $ident);
|
||||||
|
}
|
||||||
|
enelixDemoRequireType($id, 5);
|
||||||
|
if ((int) IPS_GetMedia($id)['MediaType'] !== 4) {
|
||||||
|
throw new RuntimeException('Medientyp-Konflikt bei ' . $ident);
|
||||||
|
}
|
||||||
|
IPS_SetName($id, $name);
|
||||||
|
IPS_SetPosition($id, $position);
|
||||||
|
IPS_SetHidden($id, false);
|
||||||
|
IPS_SetMediaFile($id, 'media/' . $id . '.chart', false);
|
||||||
|
IPS_SetMediaContent(
|
||||||
|
$id,
|
||||||
|
base64_encode(json_encode(['datasets' => $datasets], JSON_THROW_ON_ERROR))
|
||||||
|
);
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enelixDemoChartDataset(
|
||||||
|
int $variableId,
|
||||||
|
string $title,
|
||||||
|
string $strokeColor,
|
||||||
|
string $profile
|
||||||
|
): array {
|
||||||
|
return [
|
||||||
|
'variableID' => $variableId,
|
||||||
|
'fillColor' => 'clear',
|
||||||
|
'strokeColor' => $strokeColor,
|
||||||
|
'timeOffset' => 0,
|
||||||
|
'visible' => true,
|
||||||
|
'title' => $title,
|
||||||
|
'type' => 'line',
|
||||||
|
'side' => 'left',
|
||||||
|
'profile' => $profile,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function enelixDemoEnsureScript(int $parent, string $ident, string $name, string $content, int $position): int
|
function enelixDemoEnsureScript(int $parent, string $ident, string $name, string $content, int $position): int
|
||||||
{
|
{
|
||||||
$id = enelixDemoFind($parent, $ident);
|
$id = enelixDemoFind($parent, $ident);
|
||||||
@@ -642,12 +751,12 @@ function enelixDemoEnergyNode(
|
|||||||
return [
|
return [
|
||||||
'VariableConfigType' => 0,
|
'VariableConfigType' => 0,
|
||||||
'VariableID' => $variableId,
|
'VariableID' => $variableId,
|
||||||
'SecondaryVariableID' => 0,
|
'SecondaryVariableID' => 1,
|
||||||
'Name' => $name,
|
'Name' => $name,
|
||||||
'Type' => $type,
|
'Type' => $type,
|
||||||
'Grouped' => false,
|
'Grouped' => false,
|
||||||
'CalculationType' => 0,
|
'CalculationType' => 0,
|
||||||
'InfoVariableID' => $infoVariableId,
|
'InfoVariableID' => $infoVariableId > 0 ? $infoVariableId : 1,
|
||||||
'Invert' => false,
|
'Invert' => false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ final class EinStufigRegler
|
|||||||
int $istleistung,
|
int $istleistung,
|
||||||
bool $verfuegbar,
|
bool $verfuegbar,
|
||||||
bool $aenderungMoeglich,
|
bool $aenderungMoeglich,
|
||||||
bool $mindestlaufzeitErzwungen
|
bool $mindestlaufzeitErzwungen,
|
||||||
|
bool $peakbetrieb,
|
||||||
|
bool $peakSperreBeiMindestlaufzeitAnbieten = true
|
||||||
): array {
|
): array {
|
||||||
if ($nennleistung <= 0) {
|
if ($nennleistung <= 0) {
|
||||||
throw new InvalidArgumentException('Die Nennleistung muss groesser als 0 sein.');
|
throw new InvalidArgumentException('Die Nennleistung muss groesser als 0 sein.');
|
||||||
@@ -27,9 +29,17 @@ final class EinStufigRegler
|
|||||||
throw new InvalidArgumentException('Die Istleistung muss 0 oder der Nennleistung entsprechen.');
|
throw new InvalidArgumentException('Die Istleistung muss 0 oder der Nennleistung entsprechen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$verfuegbar || !$aenderungMoeglich || $mindestlaufzeitErzwungen) {
|
if (!$verfuegbar || !$aenderungMoeglich) {
|
||||||
return [$istleistung];
|
return [$istleistung];
|
||||||
}
|
}
|
||||||
|
if ($mindestlaufzeitErzwungen) {
|
||||||
|
return $peakbetrieb && $peakSperreBeiMindestlaufzeitAnbieten
|
||||||
|
? [0, $nennleistung]
|
||||||
|
: [$nennleistung];
|
||||||
|
}
|
||||||
|
if ($peakbetrieb) {
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
|
||||||
return [0, $nennleistung];
|
return [0, $nennleistung];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
final class LadestationAdapter
|
||||||
|
{
|
||||||
|
public const GO_E_ALT = 1;
|
||||||
|
public const GO_E_GEMINI = 2;
|
||||||
|
public const SMART_ME_PICO = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $konfiguration
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function statusAnfrage(int $geraetetyp, array $konfiguration): array
|
||||||
|
{
|
||||||
|
if ($geraetetyp === self::GO_E_ALT) {
|
||||||
|
return self::anfrage('GET', self::lokaleBasisUrl($konfiguration) . '/mqtt?payload=');
|
||||||
|
}
|
||||||
|
if ($geraetetyp === self::GO_E_GEMINI) {
|
||||||
|
return self::anfrage('GET', self::lokaleBasisUrl($konfiguration) . '/api/status');
|
||||||
|
}
|
||||||
|
if ($geraetetyp === self::SMART_ME_PICO) {
|
||||||
|
$id = self::pflichtwert($konfiguration, 'GeraeteID');
|
||||||
|
|
||||||
|
return self::anfrage(
|
||||||
|
'GET',
|
||||||
|
'https://api.smart-me.com/pico/charging/' . rawurlencode($id),
|
||||||
|
$konfiguration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $konfiguration
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public static function steuerAnfragen(
|
||||||
|
int $geraetetyp,
|
||||||
|
array $konfiguration,
|
||||||
|
int $ladestrom
|
||||||
|
): array {
|
||||||
|
if ($ladestrom < 0 || $ladestrom > 32 || ($ladestrom > 0 && $ladestrom < 6)) {
|
||||||
|
throw new InvalidArgumentException('Ladestrom muss 0 oder 6 bis 32 A betragen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($geraetetyp === self::GO_E_ALT) {
|
||||||
|
$basis = self::lokaleBasisUrl($konfiguration) . '/mqtt?payload=';
|
||||||
|
if ($ladestrom === 0) {
|
||||||
|
return [self::anfrage('GET', $basis . 'alw=0')];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
self::anfrage('GET', $basis . 'alw=1'),
|
||||||
|
self::anfrage('GET', $basis . 'amp=' . $ladestrom),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($geraetetyp === self::GO_E_GEMINI) {
|
||||||
|
$basis = self::lokaleBasisUrl($konfiguration) . '/api/set?';
|
||||||
|
if ($ladestrom === 0) {
|
||||||
|
return [self::anfrage('GET', $basis . 'frc=1&sua=false')];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
self::anfrage('GET', $basis . 'frc=2'),
|
||||||
|
self::anfrage('GET', $basis . 'amp=' . $ladestrom),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($geraetetyp === self::SMART_ME_PICO) {
|
||||||
|
$seriennummer = self::pflichtwert($konfiguration, 'Seriennummer');
|
||||||
|
|
||||||
|
return [self::anfrage(
|
||||||
|
'POST',
|
||||||
|
'https://api.smart-me.com/pico/loadmanagementgroup/current/'
|
||||||
|
. rawurlencode($seriennummer)
|
||||||
|
. '?current=' . ($ladestrom * 1000),
|
||||||
|
$konfiguration
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{Rohstatus: int, Istleistung_W: float}
|
||||||
|
*/
|
||||||
|
public static function dekodiereStatus(int $geraetetyp, string $antwort): array
|
||||||
|
{
|
||||||
|
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($daten)) {
|
||||||
|
throw new RuntimeException('Die Geraeteantwort ist kein JSON-Objekt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($geraetetyp === self::GO_E_ALT || $geraetetyp === self::GO_E_GEMINI) {
|
||||||
|
if (!isset($daten['car'], $daten['nrg'])
|
||||||
|
|| !is_numeric($daten['car'])
|
||||||
|
|| !is_array($daten['nrg'])
|
||||||
|
|| !isset($daten['nrg'][11])
|
||||||
|
|| !is_numeric($daten['nrg'][11])
|
||||||
|
) {
|
||||||
|
throw new RuntimeException('Die go-e-Antwort enthaelt nicht alle Statusfelder.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$faktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Rohstatus' => (int) $daten['car'],
|
||||||
|
'Istleistung_W' => (float) $daten['nrg'][11] * $faktor,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($geraetetyp === self::SMART_ME_PICO) {
|
||||||
|
if (!isset($daten['State'], $daten['ActiveChargingPower'])
|
||||||
|
|| !is_numeric($daten['State'])
|
||||||
|
|| !is_numeric($daten['ActiveChargingPower'])
|
||||||
|
) {
|
||||||
|
throw new RuntimeException('Die Pico-Antwort enthaelt nicht alle Statusfelder.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Rohstatus' => (int) $daten['State'],
|
||||||
|
'Istleistung_W' => round((float) $daten['ActiveChargingPower'] * 1000.0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $konfiguration
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function anfrage(
|
||||||
|
string $methode,
|
||||||
|
string $url,
|
||||||
|
array $konfiguration = []
|
||||||
|
): array {
|
||||||
|
$anfrage = ['Methode' => $methode, 'URL' => $url];
|
||||||
|
if (str_starts_with($url, 'https://api.smart-me.com/')) {
|
||||||
|
$anfrage['Benutzername'] = self::pflichtwert($konfiguration, 'Benutzername');
|
||||||
|
$anfrage['Passwort'] = self::pflichtwert($konfiguration, 'Passwort');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $anfrage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $konfiguration */
|
||||||
|
private static function lokaleBasisUrl(array $konfiguration): string
|
||||||
|
{
|
||||||
|
$adresse = trim(self::pflichtwert($konfiguration, 'Geraeteadresse'));
|
||||||
|
if (str_contains($adresse, '/') || str_contains($adresse, '?')) {
|
||||||
|
throw new InvalidArgumentException('Geraeteadresse muss ein Hostname oder eine IP-Adresse sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'http://' . $adresse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $konfiguration */
|
||||||
|
private static function pflichtwert(array $konfiguration, string $name): string
|
||||||
|
{
|
||||||
|
$wert = trim($konfiguration[$name] ?? '');
|
||||||
|
if ($wert === '') {
|
||||||
|
throw new InvalidArgumentException($name . ' darf nicht leer sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LadestationTransport
|
||||||
|
{
|
||||||
|
/** @var callable(array<string, mixed>): string */
|
||||||
|
private $anfrageAusfuehren;
|
||||||
|
|
||||||
|
/** @param callable(array<string, mixed>): string $anfrageAusfuehren */
|
||||||
|
public function __construct(callable $anfrageAusfuehren)
|
||||||
|
{
|
||||||
|
$this->anfrageAusfuehren = $anfrageAusfuehren;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $konfiguration */
|
||||||
|
public function statusLesen(int $geraetetyp, array $konfiguration): string
|
||||||
|
{
|
||||||
|
return ($this->anfrageAusfuehren)(
|
||||||
|
LadestationAdapter::statusAnfrage($geraetetyp, $konfiguration)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $konfiguration */
|
||||||
|
public function ladestromSetzen(
|
||||||
|
int $geraetetyp,
|
||||||
|
array $konfiguration,
|
||||||
|
int $ladestrom
|
||||||
|
): void {
|
||||||
|
foreach (LadestationAdapter::steuerAnfragen($geraetetyp, $konfiguration, $ladestrom) as $anfrage) {
|
||||||
|
($this->anfrageAusfuehren)($anfrage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class LadestationRegler
|
||||||
|
{
|
||||||
|
public const PHASEN_UNBEKANNT = 0;
|
||||||
|
public const PHASEN_EIN = 1;
|
||||||
|
public const PHASEN_DREI = 3;
|
||||||
|
|
||||||
|
/** @var list<int> */
|
||||||
|
private const LEISTUNGEN_EINPHASIG = [
|
||||||
|
1450, 1690, 1930, 2170, 2410, 2650, 2890, 3130, 3370,
|
||||||
|
3610, 3850, 4090, 4330, 4570, 4810, 5050, 5290, 5530,
|
||||||
|
5770, 6010, 6250, 6490, 6730, 6970, 7210, 7450, 7690,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var list<int> */
|
||||||
|
private const LEISTUNGEN_DREIPHASIG = [
|
||||||
|
4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700,
|
||||||
|
10300, 11000, 11750, 12450, 13150, 13850, 14550, 15250,
|
||||||
|
15900, 16600, 17300, 18000, 18700, 19400, 20100, 20800,
|
||||||
|
21500, 22000,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Schwelle, die Voll-Erkennung und der Zuschlag von 2.5 A stammen
|
||||||
|
* aus der Enelix-1-Ladestation und bleiben bewusst kompatibel.
|
||||||
|
*
|
||||||
|
* @param array{Rohstatus: int, Istleistung_W: float} $rohstatus
|
||||||
|
* @return array<string, int|float|bool>
|
||||||
|
*/
|
||||||
|
public static function normalisiereStatus(array $rohstatus, bool $warVerbunden): array
|
||||||
|
{
|
||||||
|
$verbunden = $rohstatus['Rohstatus'] !== 1;
|
||||||
|
$leistung = max(0.0, $rohstatus['Istleistung_W']);
|
||||||
|
$phasen = self::PHASEN_UNBEKANNT;
|
||||||
|
$ladestrom = 0.0;
|
||||||
|
$geladen = false;
|
||||||
|
|
||||||
|
if ($verbunden) {
|
||||||
|
$phasen = $leistung > 7500.0 ? self::PHASEN_DREI : self::PHASEN_EIN;
|
||||||
|
$nenner = $phasen === self::PHASEN_EIN ? 230.0 : 1.71 * 400.0;
|
||||||
|
$ladestrom = $leistung / $nenner;
|
||||||
|
$ermittelterMaximalstrom = 2.5 + $ladestrom;
|
||||||
|
$geladen = $warVerbunden && $ermittelterMaximalstrom < 6.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$verbunden) {
|
||||||
|
$fahrzeugstatus = 1;
|
||||||
|
} elseif ($geladen) {
|
||||||
|
$fahrzeugstatus = 4;
|
||||||
|
} elseif ($leistung > 0.0) {
|
||||||
|
$fahrzeugstatus = 3;
|
||||||
|
} else {
|
||||||
|
$fahrzeugstatus = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'FahrzeugVerbunden' => $verbunden,
|
||||||
|
'FahrzeugGeladen' => $geladen,
|
||||||
|
'Fahrzeugstatus' => $fahrzeugstatus,
|
||||||
|
'Phasenzahl' => $phasen,
|
||||||
|
'Istleistung_W' => $leistung,
|
||||||
|
'Ladestrom_A' => $ladestrom,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
public static function leistungsangebot(
|
||||||
|
bool $aktiv,
|
||||||
|
bool $ladefreigabe,
|
||||||
|
bool $verbunden,
|
||||||
|
bool $geladen,
|
||||||
|
int $phasenzahl,
|
||||||
|
int $maximalerLadestrom,
|
||||||
|
bool $solarladen,
|
||||||
|
bool $peakbetrieb
|
||||||
|
): array {
|
||||||
|
if (!$aktiv || !$ladefreigabe || !$verbunden || $geladen) {
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
|
||||||
|
if ($stufen === []) {
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
if ($solarladen) {
|
||||||
|
return $peakbetrieb ? [0] : array_merge([0], $stufen);
|
||||||
|
}
|
||||||
|
if ($peakbetrieb) {
|
||||||
|
return array_merge([0], $stufen);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$stufen[count($stufen) - 1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stromFuerLeistung(int $leistung, int $phasenzahl): int
|
||||||
|
{
|
||||||
|
if ($leistung <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stufen = self::stufenFuerPhasen($phasenzahl);
|
||||||
|
if ($leistung < $stufen[0]) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strom = 6;
|
||||||
|
foreach ($stufen as $index => $schwelle) {
|
||||||
|
if ($leistung < $schwelle) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$strom = 6 + $index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min(32, $strom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
private static function leistungsstufen(int $phasenzahl, int $maximalerLadestrom): array
|
||||||
|
{
|
||||||
|
if ($maximalerLadestrom < 6 || $maximalerLadestrom > 32) {
|
||||||
|
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice(self::stufenFuerPhasen($phasenzahl), 0, $maximalerLadestrom - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> */
|
||||||
|
private static function stufenFuerPhasen(int $phasenzahl): array
|
||||||
|
{
|
||||||
|
if ($phasenzahl === self::PHASEN_EIN) {
|
||||||
|
return self::LEISTUNGEN_EINPHASIG;
|
||||||
|
}
|
||||||
|
if ($phasenzahl === self::PHASEN_DREI) {
|
||||||
|
return self::LEISTUNGEN_DREIPHASIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('Phasenzahl muss 1 oder 3 sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ use InvalidArgumentException;
|
|||||||
final class ManagerRegler
|
final class ManagerRegler
|
||||||
{
|
{
|
||||||
public const BETRIEBSART_INAKTIV = 'Inaktiv';
|
public const BETRIEBSART_INAKTIV = 'Inaktiv';
|
||||||
public const BETRIEBSART_PV = 'PV';
|
public const BETRIEBSART_PV = Nachrichtenvertrag::BETRIEBSART_PV;
|
||||||
public const BETRIEBSART_PEAK = 'Peak';
|
public const BETRIEBSART_PEAK = Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel
|
* @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ use InvalidArgumentException;
|
|||||||
|
|
||||||
final class Nachrichtenvertrag
|
final class Nachrichtenvertrag
|
||||||
{
|
{
|
||||||
public const VERSION = '3.0';
|
public const VERSION = '4.0';
|
||||||
|
|
||||||
|
public const BETRIEBSART_PV = 'PV';
|
||||||
|
public const BETRIEBSART_PEAK = 'Peak';
|
||||||
|
|
||||||
public const LEISTUNGSQUELLE_NICHT_VORHANDEN = 0;
|
public const LEISTUNGSQUELLE_NICHT_VORHANDEN = 0;
|
||||||
public const LEISTUNGSQUELLE_BERECHNET = 1;
|
public const LEISTUNGSQUELLE_BERECHNET = 1;
|
||||||
@@ -19,11 +22,12 @@ final class Nachrichtenvertrag
|
|||||||
*/
|
*/
|
||||||
public static function pruefeManagerdaten(array $daten): void
|
public static function pruefeManagerdaten(array $daten): void
|
||||||
{
|
{
|
||||||
self::pruefeSchluessel($daten, ['Kopf', 'Sollleistung_W'], 'Managerdaten');
|
self::pruefeSchluessel($daten, ['Kopf', 'Betriebsart', 'Sollleistung_W'], 'Managerdaten');
|
||||||
self::pruefeKopf($daten['Kopf']);
|
self::pruefeKopf($daten['Kopf']);
|
||||||
|
self::pruefeBetriebsart($daten['Betriebsart']);
|
||||||
|
|
||||||
if (!is_int($daten['Sollleistung_W'])) {
|
if ($daten['Sollleistung_W'] !== null && !is_int($daten['Sollleistung_W'])) {
|
||||||
throw new InvalidArgumentException('Sollleistung_W muss eine Ganzzahl sein.');
|
throw new InvalidArgumentException('Sollleistung_W muss eine Ganzzahl oder null sein.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +40,7 @@ final class Nachrichtenvertrag
|
|||||||
$daten,
|
$daten,
|
||||||
[
|
[
|
||||||
'Kopf',
|
'Kopf',
|
||||||
|
'Betriebsart',
|
||||||
'PrioritaetPV',
|
'PrioritaetPV',
|
||||||
'PrioritaetPeak',
|
'PrioritaetPeak',
|
||||||
'Leistungswerte_W',
|
'Leistungswerte_W',
|
||||||
@@ -49,6 +54,7 @@ final class Nachrichtenvertrag
|
|||||||
);
|
);
|
||||||
|
|
||||||
self::pruefeKopf($daten['Kopf']);
|
self::pruefeKopf($daten['Kopf']);
|
||||||
|
self::pruefeBetriebsart($daten['Betriebsart']);
|
||||||
self::pruefePrioritaet($daten['PrioritaetPV'], 'PrioritaetPV');
|
self::pruefePrioritaet($daten['PrioritaetPV'], 'PrioritaetPV');
|
||||||
self::pruefePrioritaet($daten['PrioritaetPeak'], 'PrioritaetPeak');
|
self::pruefePrioritaet($daten['PrioritaetPeak'], 'PrioritaetPeak');
|
||||||
self::pruefeLeistungswerte($daten['Leistungswerte_W']);
|
self::pruefeLeistungswerte($daten['Leistungswerte_W']);
|
||||||
@@ -93,6 +99,18 @@ final class Nachrichtenvertrag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param mixed $betriebsart */
|
||||||
|
private static function pruefeBetriebsart($betriebsart): void
|
||||||
|
{
|
||||||
|
if (!is_string($betriebsart) || !in_array(
|
||||||
|
$betriebsart,
|
||||||
|
[self::BETRIEBSART_PV, self::BETRIEBSART_PEAK],
|
||||||
|
true
|
||||||
|
)) {
|
||||||
|
throw new InvalidArgumentException('Betriebsart muss PV oder Peak sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $prioritaet
|
* @param mixed $prioritaet
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class PufferspeicherRegler
|
||||||
|
{
|
||||||
|
public const MINIMUM_AUS = 0;
|
||||||
|
public const MINIMUM_STATISCH = 1;
|
||||||
|
public const MINIMUM_DIFFERENZ = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 solltemperatur(
|
||||||
|
float $aussentemperatur,
|
||||||
|
float $fusspunkt,
|
||||||
|
float $steigung,
|
||||||
|
float $minimum,
|
||||||
|
float $maximum
|
||||||
|
): float {
|
||||||
|
if ($steigung < 0.0) {
|
||||||
|
throw new InvalidArgumentException('HeizkurvenSteigung darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
if ($minimum > $maximum) {
|
||||||
|
throw new InvalidArgumentException('Heizkurvenminimum darf nicht groesser als das Maximum sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$soll = $fusspunkt + $steigung * (20.0 - $aussentemperatur);
|
||||||
|
|
||||||
|
return min($maximum, max($minimum, $soll));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function einschaltschwelle(
|
||||||
|
float $solltemperatur,
|
||||||
|
float $hysterese,
|
||||||
|
int $minimumModus,
|
||||||
|
float $statischeMindesttemperatur,
|
||||||
|
float $mindesttemperaturDifferenz
|
||||||
|
): float {
|
||||||
|
if ($hysterese <= 0.0) {
|
||||||
|
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($minimumModus === self::MINIMUM_AUS) {
|
||||||
|
return $solltemperatur - $hysterese;
|
||||||
|
}
|
||||||
|
if ($minimumModus === self::MINIMUM_STATISCH) {
|
||||||
|
return min($solltemperatur, $statischeMindesttemperatur);
|
||||||
|
}
|
||||||
|
if ($minimumModus === self::MINIMUM_DIFFERENZ) {
|
||||||
|
if ($mindesttemperaturDifferenz < 0.0) {
|
||||||
|
throw new InvalidArgumentException('MindesttemperaturDifferenz darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $solltemperatur - $mindesttemperaturDifferenz;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException('MindesttemperaturModus muss 0, 1 oder 2 sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public static function leistungsangebot(
|
||||||
|
bool $aktiv,
|
||||||
|
bool $peakbetrieb,
|
||||||
|
bool $temperaturenGueltig,
|
||||||
|
float $puffertemperatur,
|
||||||
|
float $einschaltschwelle,
|
||||||
|
array $stufen
|
||||||
|
): array {
|
||||||
|
if (!$aktiv || $peakbetrieb || !$temperaturenGueltig || $stufen === []) {
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
if ($puffertemperatur < $einschaltschwelle) {
|
||||||
|
return self::leistungswerte($stufen);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fusspunktAusWaermepumpenwert(
|
||||||
|
float $waermepumpenSolltemperatur,
|
||||||
|
float $aussentemperatur,
|
||||||
|
float $steigung
|
||||||
|
): float {
|
||||||
|
if ($steigung < 0.0) {
|
||||||
|
throw new InvalidArgumentException('HeizkurvenSteigung darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $waermepumpenSolltemperatur - $steigung * (20.0 - $aussentemperatur);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function pt1(float $bisher, float $eingang, int $deltaSekunden, int $zeitkonstante): float
|
||||||
|
{
|
||||||
|
if ($deltaSekunden <= 0 || $zeitkonstante <= 0) {
|
||||||
|
throw new InvalidArgumentException('PT1-Zeiten muessen 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('LastwechselSperrzeit darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
if ($letzterLastwechsel <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(0, $sperrzeitSekunden - max(0, $jetzt - $letzterLastwechsel));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function energieKWh(float $leistungW, int $dauerSekunden): float
|
||||||
|
{
|
||||||
|
if ($dauerSekunden < 0) {
|
||||||
|
throw new InvalidArgumentException('Dauer darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $leistungW * $dauerSekunden / 3600000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<mixed> $wert */
|
||||||
|
private static function istListe(array $wert): bool
|
||||||
|
{
|
||||||
|
return $wert === [] || array_keys($wert) === range(0, count($wert) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,40 @@ final class WarmwasserRegler
|
|||||||
return array_merge([0], array_column($stufen, 'Leistung_W'));
|
return array_merge([0], array_column($stufen, 'Leistung_W'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $leistungswerte
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public static function leistungsangebot(
|
||||||
|
bool $peakbetrieb,
|
||||||
|
float $temperatur,
|
||||||
|
float $mindesttemperatur,
|
||||||
|
float $maximaltemperatur,
|
||||||
|
float $hysterese,
|
||||||
|
bool $stufeAktiv,
|
||||||
|
array $leistungswerte
|
||||||
|
): array {
|
||||||
|
if ($hysterese < 0.0 || $leistungswerte === []) {
|
||||||
|
throw new InvalidArgumentException('Hysterese und Leistungswerte sind ungueltig.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mindestladung = $temperatur < $mindesttemperatur
|
||||||
|
|| ($temperatur < $mindesttemperatur + $hysterese && $stufeAktiv);
|
||||||
|
if ($peakbetrieb) {
|
||||||
|
return $mindestladung ? $leistungswerte : [0];
|
||||||
|
}
|
||||||
|
if ($mindestladung) {
|
||||||
|
return [max($leistungswerte)];
|
||||||
|
}
|
||||||
|
if ($temperatur < $maximaltemperatur - $hysterese
|
||||||
|
|| ($temperatur < $maximaltemperatur && $stufeAktiv)
|
||||||
|
) {
|
||||||
|
return $leistungswerte;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class DemoanlageBootstrapTest extends TestCase
|
|||||||
self::assertSame([
|
self::assertSame([
|
||||||
'VariableConfigType' => 0,
|
'VariableConfigType' => 0,
|
||||||
'VariableID' => 12345,
|
'VariableID' => 12345,
|
||||||
'SecondaryVariableID' => 0,
|
'SecondaryVariableID' => 1,
|
||||||
'Name' => 'PV-Anlage',
|
'Name' => 'PV-Anlage',
|
||||||
'Type' => 'solar',
|
'Type' => 'solar',
|
||||||
'Grouped' => false,
|
'Grouped' => false,
|
||||||
@@ -70,6 +70,69 @@ final class DemoanlageBootstrapTest extends TestCase
|
|||||||
'InfoVariableID' => 54321,
|
'InfoVariableID' => 54321,
|
||||||
'Invert' => false,
|
'Invert' => false,
|
||||||
], enelixDemoEnergyNode(12345, 'PV-Anlage', 'solar', 54321));
|
], enelixDemoEnergyNode(12345, 'PV-Anlage', 'solar', 54321));
|
||||||
|
self::assertSame(
|
||||||
|
1,
|
||||||
|
enelixDemoEnergyNode(12345, 'PV-Anlage', 'solar')['InfoVariableID']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNativeDiagrammDatenreiheHatSymconFormat(): void
|
||||||
|
{
|
||||||
|
self::assertSame([
|
||||||
|
'variableID' => 12345,
|
||||||
|
'fillColor' => 'clear',
|
||||||
|
'strokeColor' => '#f4c542',
|
||||||
|
'timeOffset' => 0,
|
||||||
|
'visible' => true,
|
||||||
|
'title' => 'PV',
|
||||||
|
'type' => 'line',
|
||||||
|
'side' => 'left',
|
||||||
|
'profile' => '~Power',
|
||||||
|
], enelixDemoChartDataset(12345, 'PV', '#f4c542', '~Power'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDashboardVerwendetNativeEnergieverteilungUndNeueBoilerlisten(): void
|
||||||
|
{
|
||||||
|
$source = file_get_contents(dirname(__DIR__) . '/examples/Demoanlage/bootstrap.php');
|
||||||
|
|
||||||
|
self::assertIsString($source);
|
||||||
|
self::assertStringContainsString("IPS_SetHidden(\$legacyFlow, true);", $source);
|
||||||
|
self::assertStringContainsString("'EnergieverteilungNativ'", $source);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'enelixDemoConfigureVisualization($root, $energyDistribution);',
|
||||||
|
$source
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('ENELIX_DEMO_DUMMY_MODULE', $source);
|
||||||
|
self::assertStringContainsString("'Boiler1Liste'", $source);
|
||||||
|
self::assertStringContainsString("'Boiler2Liste'", $source);
|
||||||
|
self::assertStringContainsString("'EnergieflussPVkW'", $source);
|
||||||
|
self::assertStringContainsString("'~Power'", $source);
|
||||||
|
self::assertStringContainsString("'~Electricity'", $source);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"enelixDemoEnergyNode(\$flowPv, 'PV-Anlage', 'solar')",
|
||||||
|
$source
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"enelixDemoEnergyNode(\$flowBase, 'Haus-Grundlast', 'consumer')",
|
||||||
|
$source
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"enelixDemoEnergyNode(\$flowDryer, 'Entfeuchter', 'consumer')",
|
||||||
|
$source
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"enelixDemoEnsureChart(\$history, 'LeistungsdiagrammNativ'",
|
||||||
|
$source
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('IPS_SetHidden($powerChart, true);', $source);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"enelixDemoLinkInstanceVariables(\$boiler1List, \$boiler1, \$temp1);",
|
||||||
|
$source
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"foreach (['Boiler1Link', 'Boiler2Link'] as \$oldLinkIdent)",
|
||||||
|
$source
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRollbackIstExplizitBestaetigungspflichtig(): void
|
public function testRollbackIstExplizitBestaetigungspflichtig(): void
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ final class DokumentationsstrukturTest extends TestCase
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['Batterie'],
|
['Batterie'],
|
||||||
['Pufferspeicher'],
|
|
||||||
['Waermepumpe'],
|
['Waermepumpe'],
|
||||||
['Ladestation-Stand-Alone'],
|
|
||||||
['Ladestation-Gateway'],
|
['Ladestation-Gateway'],
|
||||||
['Easee-Gateway'],
|
['Easee-Gateway'],
|
||||||
];
|
];
|
||||||
@@ -33,6 +31,23 @@ final class DokumentationsstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString('Properties', $inhalt);
|
self::assertStringContainsString('Properties', $inhalt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPufferspeicherDokumentiertDenImplementiertenStand(): void
|
||||||
|
{
|
||||||
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Pufferspeicher/README.md');
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: implementiert', $inhalt);
|
||||||
|
self::assertStringContainsString('Managerkommunikation', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLadestationStandAloneDokumentiertDenImplementiertenStand(): void
|
||||||
|
{
|
||||||
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Ladestation-Stand-Alone/README.md');
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: Implementiert', $inhalt);
|
||||||
|
self::assertStringContainsString('Properties', $inhalt);
|
||||||
|
self::assertStringContainsString('Fake-HTTP-Transport', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
public function testSchnittstellenDokumentiertSind(): void
|
public function testSchnittstellenDokumentiertSind(): void
|
||||||
{
|
{
|
||||||
self::assertFileExists(__DIR__ . '/../docs/Schnittstelle.md');
|
self::assertFileExists(__DIR__ . '/../docs/Schnittstelle.md');
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\LadestationAdapter;
|
||||||
|
use Belevo\EnelixEMS\LadestationTransport;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class LadestationAdapterTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider statusFaelle
|
||||||
|
* @param array<string, string> $konfiguration
|
||||||
|
* @param array{Rohstatus: int, Istleistung_W: float} $erwartet
|
||||||
|
*/
|
||||||
|
public function testStatusaufrufUndDekodierungFuerAlleGeraete(
|
||||||
|
int $geraetetyp,
|
||||||
|
array $konfiguration,
|
||||||
|
string $antwort,
|
||||||
|
string $erwarteteUrl,
|
||||||
|
array $erwartet
|
||||||
|
): void {
|
||||||
|
$aufrufe = [];
|
||||||
|
$transport = new LadestationTransport(static function (array $anfrage) use (
|
||||||
|
&$aufrufe,
|
||||||
|
$antwort
|
||||||
|
): string {
|
||||||
|
$aufrufe[] = $anfrage;
|
||||||
|
return $antwort;
|
||||||
|
});
|
||||||
|
|
||||||
|
$rohstatus = LadestationAdapter::dekodiereStatus(
|
||||||
|
$geraetetyp,
|
||||||
|
$transport->statusLesen($geraetetyp, $konfiguration)
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame($erwarteteUrl, $aufrufe[0]['URL']);
|
||||||
|
self::assertSame('GET', $aufrufe[0]['Methode']);
|
||||||
|
self::assertSame($erwartet, $rohstatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return iterable<string, array<mixed>> */
|
||||||
|
public function statusFaelle(): iterable
|
||||||
|
{
|
||||||
|
yield 'go-e alt' => [
|
||||||
|
LadestationAdapter::GO_E_ALT,
|
||||||
|
$this->lokaleKonfiguration(),
|
||||||
|
$this->goEAntwort(2, 1100),
|
||||||
|
'http://192.0.2.10/mqtt?payload=',
|
||||||
|
['Rohstatus' => 2, 'Istleistung_W' => 11000.0],
|
||||||
|
];
|
||||||
|
yield 'go-e Gemini' => [
|
||||||
|
LadestationAdapter::GO_E_GEMINI,
|
||||||
|
$this->lokaleKonfiguration(),
|
||||||
|
$this->goEAntwort(2, 2300),
|
||||||
|
'http://192.0.2.10/api/status',
|
||||||
|
['Rohstatus' => 2, 'Istleistung_W' => 2300.0],
|
||||||
|
];
|
||||||
|
yield 'Smart-me Pico' => [
|
||||||
|
LadestationAdapter::SMART_ME_PICO,
|
||||||
|
$this->picoKonfiguration(),
|
||||||
|
json_encode([
|
||||||
|
'State' => 2,
|
||||||
|
'ActiveChargingPower' => 11.0,
|
||||||
|
'MaxAllowedChargingCurrent' => 16,
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
'https://api.smart-me.com/pico/charging/pico-id',
|
||||||
|
['Rohstatus' => 2, 'Istleistung_W' => 11000.0],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider steuerFaelle
|
||||||
|
* @param array<string, string> $konfiguration
|
||||||
|
* @param list<array{Methode: string, URL: string}> $erwarteteAufrufe
|
||||||
|
*/
|
||||||
|
public function testSteueraufrufeWerdenUeberFakeTransportGeprueft(
|
||||||
|
int $geraetetyp,
|
||||||
|
array $konfiguration,
|
||||||
|
int $strom,
|
||||||
|
array $erwarteteAufrufe
|
||||||
|
): void {
|
||||||
|
$aufrufe = [];
|
||||||
|
$transport = new LadestationTransport(static function (array $anfrage) use (&$aufrufe): string {
|
||||||
|
unset($anfrage['Benutzername'], $anfrage['Passwort']);
|
||||||
|
$aufrufe[] = $anfrage;
|
||||||
|
return '{}';
|
||||||
|
});
|
||||||
|
|
||||||
|
$transport->ladestromSetzen($geraetetyp, $konfiguration, $strom);
|
||||||
|
|
||||||
|
self::assertSame($erwarteteAufrufe, $aufrufe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return iterable<string, array<mixed>> */
|
||||||
|
public function steuerFaelle(): iterable
|
||||||
|
{
|
||||||
|
yield 'go-e alt laden' => [
|
||||||
|
LadestationAdapter::GO_E_ALT,
|
||||||
|
$this->lokaleKonfiguration(),
|
||||||
|
16,
|
||||||
|
[
|
||||||
|
['Methode' => 'GET', 'URL' => 'http://192.0.2.10/mqtt?payload=alw=1'],
|
||||||
|
['Methode' => 'GET', 'URL' => 'http://192.0.2.10/mqtt?payload=amp=16'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
yield 'go-e Gemini stoppen' => [
|
||||||
|
LadestationAdapter::GO_E_GEMINI,
|
||||||
|
$this->lokaleKonfiguration(),
|
||||||
|
0,
|
||||||
|
[[
|
||||||
|
'Methode' => 'GET',
|
||||||
|
'URL' => 'http://192.0.2.10/api/set?frc=1&sua=false',
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
yield 'Smart-me Pico laden' => [
|
||||||
|
LadestationAdapter::SMART_ME_PICO,
|
||||||
|
$this->picoKonfiguration(),
|
||||||
|
16,
|
||||||
|
[[
|
||||||
|
'Methode' => 'POST',
|
||||||
|
'URL' => 'https://api.smart-me.com/pico/loadmanagementgroup/current/serial-1?current=16000',
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPicoAnfragenVerwendenBasicAuth(): void
|
||||||
|
{
|
||||||
|
$anfrage = LadestationAdapter::statusAnfrage(
|
||||||
|
LadestationAdapter::SMART_ME_PICO,
|
||||||
|
$this->picoKonfiguration()
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame('api-user', $anfrage['Benutzername']);
|
||||||
|
self::assertSame(hash('sha256', __FILE__), $anfrage['Passwort']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
private function lokaleKonfiguration(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Geraeteadresse' => '192.0.2.10',
|
||||||
|
'GeraeteID' => '',
|
||||||
|
'Seriennummer' => '',
|
||||||
|
'Benutzername' => '',
|
||||||
|
'Passwort' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
private function picoKonfiguration(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Geraeteadresse' => '',
|
||||||
|
'GeraeteID' => 'pico-id',
|
||||||
|
'Seriennummer' => 'serial-1',
|
||||||
|
'Benutzername' => 'api-user',
|
||||||
|
'Passwort' => hash('sha256', __FILE__),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function goEAntwort(int $status, float $leistung): string
|
||||||
|
{
|
||||||
|
$nrg = array_fill(0, 12, 0);
|
||||||
|
$nrg[11] = $leistung;
|
||||||
|
|
||||||
|
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\LadestationRegler;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class LadestationReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFahrzeugUndDreiPhasenWerdenWieInEnelixEinsErkannt(): void
|
||||||
|
{
|
||||||
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 11000.0,
|
||||||
|
], false);
|
||||||
|
|
||||||
|
self::assertTrue($status['FahrzeugVerbunden']);
|
||||||
|
self::assertFalse($status['FahrzeugGeladen']);
|
||||||
|
self::assertSame(3, $status['Fahrzeugstatus']);
|
||||||
|
self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEinphasigesFahrzeugWirdUnterhalb7500WErkannt(): void
|
||||||
|
{
|
||||||
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 2300.0,
|
||||||
|
], false);
|
||||||
|
|
||||||
|
self::assertSame(LadestationRegler::PHASEN_EIN, $status['Phasenzahl']);
|
||||||
|
self::assertEqualsWithDelta(10.0, $status['Ladestrom_A'], 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeladenWirdNachVerbindungUnterSechsAmpereErkannt(): void
|
||||||
|
{
|
||||||
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 0.0,
|
||||||
|
], true);
|
||||||
|
|
||||||
|
self::assertTrue($status['FahrzeugGeladen']);
|
||||||
|
self::assertSame(4, $status['Fahrzeugstatus']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBetriebsartUndSolarladenBestimmenDasLeistungsangebot(): void
|
||||||
|
{
|
||||||
|
$solarPV = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, true, false);
|
||||||
|
$normalPV = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, false, false);
|
||||||
|
$solarPeak = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, true, true);
|
||||||
|
$normalPeak = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, false, true);
|
||||||
|
|
||||||
|
self::assertSame(0, $solarPV[0]);
|
||||||
|
self::assertCount(12, $solarPV);
|
||||||
|
self::assertSame([3850], $normalPV);
|
||||||
|
self::assertSame([0], $solarPeak);
|
||||||
|
self::assertSame($solarPV, $normalPeak);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPeakKannJedesFahrzeugAufNullSetzen(): void
|
||||||
|
{
|
||||||
|
self::assertSame(0, LadestationRegler::stromFuerLeistung(0, 3));
|
||||||
|
self::assertSame(6, LadestationRegler::stromFuerLeistung(4150, 3));
|
||||||
|
self::assertSame(16, LadestationRegler::stromFuerLeistung(11000, 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class LadestationStandAloneModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
private const MODULPFAD = __DIR__ . '/../LadestationStandAlone/';
|
||||||
|
|
||||||
|
public function testModulIstVollstaendigUndMetadatenSindGueltig(): void
|
||||||
|
{
|
||||||
|
foreach (['module.json', 'form.json', 'module.php'] as $datei) {
|
||||||
|
self::assertFileExists(self::MODULPFAD . $datei);
|
||||||
|
}
|
||||||
|
|
||||||
|
$modul = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'module.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
self::assertSame('LadestationStandAlone', $modul['name']);
|
||||||
|
self::assertSame('8.0', json_decode(
|
||||||
|
(string) file_get_contents(__DIR__ . '/../library.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
)['compatibility']['version']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGemeinsamerVerbrauchervertragWirdVerwendet(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
|
||||||
|
self::assertStringContainsString('use VerbraucherBasisTrait;', $modul);
|
||||||
|
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
|
||||||
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeVerbraucherdaten', $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAlleDreiStandAloneGeraeteUndSichereZugangsdatenSindKonfiguriert(): void
|
||||||
|
{
|
||||||
|
$form = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'form.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$json = json_encode($form, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
self::assertStringContainsString('go-e Charger (alte API)', $json);
|
||||||
|
self::assertStringContainsString('go-e Charger Gemini', $json);
|
||||||
|
self::assertStringContainsString('Gemini flex', $json);
|
||||||
|
self::assertStringContainsString('smart-me Pico', $json);
|
||||||
|
self::assertStringContainsString('PasswordTextBox', $json);
|
||||||
|
self::assertStringNotContainsString('Testmodus', $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVisualisierungsbuttonsLoggingUndDiagnoseSindVorhanden(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString("EnableAction('Ladefreigabe')", $modul);
|
||||||
|
self::assertStringContainsString("EnableAction('Solarladen')", $modul);
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('EinstellungenInVisu')", $modul);
|
||||||
|
self::assertStringContainsString('UnregisterVariable($ident)', $modul);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,6 +168,7 @@ final class ManagerReglerTest extends TestCase
|
|||||||
'EmpfaengerID' => 1,
|
'EmpfaengerID' => 1,
|
||||||
'Zeitpunkt' => 1788825600,
|
'Zeitpunkt' => 1788825600,
|
||||||
],
|
],
|
||||||
|
'Betriebsart' => Nachrichtenvertrag::BETRIEBSART_PV,
|
||||||
'PrioritaetPV' => $prioritaet,
|
'PrioritaetPV' => $prioritaet,
|
||||||
'PrioritaetPeak' => $prioritaet,
|
'PrioritaetPeak' => $prioritaet,
|
||||||
'Leistungswerte_W' => $leistungswerte,
|
'Leistungswerte_W' => $leistungswerte,
|
||||||
|
|||||||
@@ -14,12 +14,28 @@ final class NachrichtenvertragTest extends TestCase
|
|||||||
{
|
{
|
||||||
Nachrichtenvertrag::pruefeManagerdaten([
|
Nachrichtenvertrag::pruefeManagerdaten([
|
||||||
'Kopf' => $this->kopf(10001, 20001),
|
'Kopf' => $this->kopf(10001, 20001),
|
||||||
|
'Betriebsart' => Nachrichtenvertrag::BETRIEBSART_PV,
|
||||||
'Sollleistung_W' => 1501,
|
'Sollleistung_W' => 1501,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Nachrichtenvertrag::pruefeManagerdaten([
|
||||||
|
'Kopf' => $this->kopf(10001, 20001),
|
||||||
|
'Betriebsart' => Nachrichtenvertrag::BETRIEBSART_PEAK,
|
||||||
|
'Sollleistung_W' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
self::assertTrue(true);
|
self::assertTrue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUngueltigeBetriebsartWirdAbgelehnt(): void
|
||||||
|
{
|
||||||
|
$daten = $this->verbraucherdaten();
|
||||||
|
$daten['Betriebsart'] = 'Unbekannt';
|
||||||
|
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
|
||||||
|
}
|
||||||
|
|
||||||
public function testGueltigeVerbraucherdatenMitEinzelwertenUndBereichenWerdenAkzeptiert(): void
|
public function testGueltigeVerbraucherdatenMitEinzelwertenUndBereichenWerdenAkzeptiert(): void
|
||||||
{
|
{
|
||||||
Nachrichtenvertrag::pruefeVerbraucherdaten($this->verbraucherdaten());
|
Nachrichtenvertrag::pruefeVerbraucherdaten($this->verbraucherdaten());
|
||||||
@@ -92,6 +108,7 @@ final class NachrichtenvertragTest extends TestCase
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'Kopf' => $this->kopf(20001, 10001),
|
'Kopf' => $this->kopf(20001, 10001),
|
||||||
|
'Betriebsart' => Nachrichtenvertrag::BETRIEBSART_PEAK,
|
||||||
'PrioritaetPV' => 0,
|
'PrioritaetPV' => 0,
|
||||||
'PrioritaetPeak' => 12,
|
'PrioritaetPeak' => 12,
|
||||||
'Leistungswerte_W' => [
|
'Leistungswerte_W' => [
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class PufferspeicherModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
private const MODULPFAD = __DIR__ . '/../Pufferspeicher/';
|
||||||
|
|
||||||
|
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('VerbraucherPufferspeicher', $metadaten['name']);
|
||||||
|
self::assertSame('Enelix', $metadaten['vendor']);
|
||||||
|
self::assertSame(['Pufferspeicher'], $metadaten['aliases']);
|
||||||
|
self::assertSame(3, $metadaten['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPropertiesSindRegistriertUndInGewuenschterReihenfolgeImFormular(): 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]));
|
||||||
|
$formularText = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
self::assertStringContainsString('"name": "' . $property . '"', $formularText, $property);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formular = json_decode($formularText, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Manager und Zeitverhalten',
|
||||||
|
'Pufferspeicher',
|
||||||
|
'Heizkurve',
|
||||||
|
'Erweiterte Einstellungen',
|
||||||
|
],
|
||||||
|
array_column($formular['elements'], 'caption')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModulVerwendetVertrag40UndBetriebsartabhaengigesAngebot(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
|
||||||
|
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
|
||||||
|
self::assertStringContainsString("['Betriebsart']", $modul);
|
||||||
|
self::assertStringContainsString('BETRIEBSART_PEAK', $modul);
|
||||||
|
self::assertStringContainsString("'Betriebsart' =>", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEinAusLoggingVariablenUndWaermepumpenaktionSindEnthalten(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
|
||||||
|
self::assertStringContainsString("case 'Aktiv':", $modul);
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
||||||
|
foreach ([
|
||||||
|
'Puffertemperatur',
|
||||||
|
'Aussentemperatur',
|
||||||
|
'Solltemperatur',
|
||||||
|
'Heizbedarf',
|
||||||
|
] as $ident) {
|
||||||
|
self::assertStringContainsString("RegisterVariable", $modul);
|
||||||
|
self::assertStringContainsString("'" . $ident . "'", $modul);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString('HeizkurveVonWaermepumpeUebernehmen', $modul);
|
||||||
|
self::assertStringContainsString('Wert von Waermepumpe uebernehmen', $formular);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchaltfolgeIstBreakBeforeMake(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
$aus = strpos($modul, "RequestAction(\$stufe['KontaktID'], false)");
|
||||||
|
$ein = strpos($modul, "RequestAction(\$stufe['KontaktID'], true)");
|
||||||
|
|
||||||
|
self::assertIsInt($aus);
|
||||||
|
self::assertIsInt($ein);
|
||||||
|
self::assertLessThan($ein, $aus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testManagerKenntDieModulIDGenauEinmal(): void
|
||||||
|
{
|
||||||
|
$manager = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||||
|
|
||||||
|
self::assertSame(1, substr_count(
|
||||||
|
$manager,
|
||||||
|
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\PufferspeicherRegler;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class PufferspeicherReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @return list<array{Stufe: int, Leistung_W: int, KontaktID: int}> */
|
||||||
|
private function stufen(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['Stufe' => 1, 'Leistung_W' => 1000, 'KontaktID' => 101],
|
||||||
|
['Stufe' => 2, 'Leistung_W' => 2000, 'KontaktID' => 102],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHeizkurveWirdBerechnetUndBegrenzt(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
50.0,
|
||||||
|
PufferspeicherRegler::solltemperatur(5.0, 35.0, 1.0, 20.0, 80.0)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
80.0,
|
||||||
|
PufferspeicherRegler::solltemperatur(-100.0, 35.0, 1.0, 20.0, 80.0)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
20.0,
|
||||||
|
PufferspeicherRegler::solltemperatur(100.0, 35.0, 1.0, 20.0, 80.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnterErrechneterEinschaltschwelleWerdenAlleStufenAngeboten(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0, 1000, 2000],
|
||||||
|
PufferspeicherRegler::leistungsangebot(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
44.9,
|
||||||
|
45.0,
|
||||||
|
$this->stufen()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInHystereseUndDarueberWirdNurNullAngeboten(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
PufferspeicherRegler::leistungsangebot(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
45.0,
|
||||||
|
45.0,
|
||||||
|
$this->stufen()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
PufferspeicherRegler::leistungsangebot(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
51.0,
|
||||||
|
45.0,
|
||||||
|
$this->stufen()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPeakbetriebBietetUnabhaengigVonDerTemperaturNurNull(): void
|
||||||
|
{
|
||||||
|
foreach ([-20.0, 44.9, 45.0, 100.0] as $temperatur) {
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
PufferspeicherRegler::leistungsangebot(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
$temperatur,
|
||||||
|
45.0,
|
||||||
|
$this->stufen()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAusgeschaltetWirdImmerNurNullAngeboten(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
PufferspeicherRegler::leistungsangebot(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
-20.0,
|
||||||
|
45.0,
|
||||||
|
$this->stufen()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMindesttemperaturIstOptionalStatischOderDifferenzbasiert(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
45.0,
|
||||||
|
PufferspeicherRegler::einschaltschwelle(
|
||||||
|
50.0,
|
||||||
|
5.0,
|
||||||
|
PufferspeicherRegler::MINIMUM_AUS,
|
||||||
|
30.0,
|
||||||
|
10.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
42.0,
|
||||||
|
PufferspeicherRegler::einschaltschwelle(
|
||||||
|
50.0,
|
||||||
|
5.0,
|
||||||
|
PufferspeicherRegler::MINIMUM_STATISCH,
|
||||||
|
42.0,
|
||||||
|
10.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
43.0,
|
||||||
|
PufferspeicherRegler::einschaltschwelle(
|
||||||
|
50.0,
|
||||||
|
5.0,
|
||||||
|
PufferspeicherRegler::MINIMUM_DIFFERENZ,
|
||||||
|
30.0,
|
||||||
|
7.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWaermepumpenwertKalibriertFusspunktAmAktuellenAussenwert(): void
|
||||||
|
{
|
||||||
|
$fusspunkt = PufferspeicherRegler::fusspunktAusWaermepumpenwert(50.0, 5.0, 1.0);
|
||||||
|
|
||||||
|
self::assertSame(35.0, $fusspunkt);
|
||||||
|
self::assertSame(
|
||||||
|
50.0,
|
||||||
|
PufferspeicherRegler::solltemperatur(5.0, $fusspunkt, 1.0, 0.0, 100.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,17 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'LadestationStandAlone' => [
|
||||||
|
'file' => __DIR__ . '/modules/LadestationStandAlone.php',
|
||||||
|
'paths' => [
|
||||||
|
'LadestationStandAlone/*',
|
||||||
|
'libs/LadestationAdapter.php',
|
||||||
|
'libs/LadestationRegler.php',
|
||||||
|
'libs/VerbraucherBasisTrait.php',
|
||||||
|
'libs/VerbraucherSchnittstelle.php',
|
||||||
|
'libs/Nachrichtenvertrag.php',
|
||||||
|
],
|
||||||
|
],
|
||||||
'Manager' => [
|
'Manager' => [
|
||||||
'file' => __DIR__ . '/modules/Manager.php',
|
'file' => __DIR__ . '/modules/Manager.php',
|
||||||
'paths' => [
|
'paths' => [
|
||||||
@@ -14,6 +25,16 @@ return [
|
|||||||
'libs/VerbraucherSchnittstelle.php',
|
'libs/VerbraucherSchnittstelle.php',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'Pufferspeicher' => [
|
||||||
|
'file' => __DIR__ . '/modules/Pufferspeicher.php',
|
||||||
|
'paths' => [
|
||||||
|
'Pufferspeicher/*',
|
||||||
|
'libs/PufferspeicherRegler.php',
|
||||||
|
'libs/VerbraucherBasisTrait.php',
|
||||||
|
'libs/VerbraucherSchnittstelle.php',
|
||||||
|
'libs/Nachrichtenvertrag.php',
|
||||||
|
],
|
||||||
|
],
|
||||||
'VerbraucherEinStufig' => [
|
'VerbraucherEinStufig' => [
|
||||||
'file' => __DIR__ . '/modules/VerbraucherEinStufig.php',
|
'file' => __DIR__ . '/modules/VerbraucherEinStufig.php',
|
||||||
'paths' => [
|
'paths' => [
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\SymconTest\TestContext;
|
||||||
|
|
||||||
|
return static function (TestContext $test): void {
|
||||||
|
$testPasswort = hash('sha256', __FILE__);
|
||||||
|
$goEAntwort = static function (int $status, float $leistung): string {
|
||||||
|
$nrg = array_fill(0, 12, 0);
|
||||||
|
$nrg[11] = $leistung;
|
||||||
|
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
|
||||||
|
};
|
||||||
|
|
||||||
|
$definitionen = [
|
||||||
|
'go-e alt' => [
|
||||||
|
'Geraetetyp' => 1,
|
||||||
|
'Geraeteadresse' => '192.0.2.10',
|
||||||
|
'Testantwort' => $goEAntwort(2, 1100),
|
||||||
|
'Phasenzahl' => 3,
|
||||||
|
'BefehlUrlTeil' => 'amp=16',
|
||||||
|
],
|
||||||
|
'go-e Gemini' => [
|
||||||
|
'Geraetetyp' => 2,
|
||||||
|
'Geraeteadresse' => '192.0.2.11',
|
||||||
|
'Testantwort' => $goEAntwort(2, 2300),
|
||||||
|
'Phasenzahl' => 1,
|
||||||
|
'BefehlUrlTeil' => 'amp=16',
|
||||||
|
],
|
||||||
|
'smart-me Pico' => [
|
||||||
|
'Geraetetyp' => 3,
|
||||||
|
'GeraeteID' => 'pico-id',
|
||||||
|
'Seriennummer' => 'serial-1',
|
||||||
|
'Benutzername' => 'api-user',
|
||||||
|
'Passwort' => $testPasswort,
|
||||||
|
'Testantwort' => json_encode([
|
||||||
|
'State' => 2,
|
||||||
|
'ActiveChargingPower' => 11.0,
|
||||||
|
'MaxAllowedChargingCurrent' => 16,
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
'Phasenzahl' => 3,
|
||||||
|
'BefehlUrlTeil' => 'current=16000',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$stationen = [];
|
||||||
|
foreach ($definitionen as $name => $definition) {
|
||||||
|
$properties = array_merge($definition, [
|
||||||
|
'MaximalerLadestrom' => 16,
|
||||||
|
'Ladefreigabe' => true,
|
||||||
|
'Solarladen' => true,
|
||||||
|
'EinstellungenInVisu' => true,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
'Testmodus' => true,
|
||||||
|
]);
|
||||||
|
unset($properties['Phasenzahl'], $properties['BefehlUrlTeil']);
|
||||||
|
$stationen[$name] = $test->createInstance(
|
||||||
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
|
'Ladestation ' . $name,
|
||||||
|
$properties
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$managerId = $test->createInstance(
|
||||||
|
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||||
|
'Hilfsmanager Ladestation',
|
||||||
|
[
|
||||||
|
'AutomatischeSuche' => false,
|
||||||
|
'VerbraucherZuordnung' => json_encode(array_map(
|
||||||
|
static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true],
|
||||||
|
array_values($stationen)
|
||||||
|
), JSON_THROW_ON_ERROR),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$anforderungSenden = static function (
|
||||||
|
int $stationId,
|
||||||
|
int $leistung,
|
||||||
|
string $betriebsart = 'PV'
|
||||||
|
) use ($managerId): void {
|
||||||
|
IPS_RequestAction($stationId, 'ManagerdatenEmpfangen', json_encode([
|
||||||
|
'Kopf' => [
|
||||||
|
'Version' => '4.0',
|
||||||
|
'AbsenderID' => $managerId,
|
||||||
|
'EmpfaengerID' => $stationId,
|
||||||
|
'Zeitpunkt' => time(),
|
||||||
|
],
|
||||||
|
'Betriebsart' => $betriebsart,
|
||||||
|
'Sollleistung_W' => $leistung,
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
};
|
||||||
|
|
||||||
|
$test->runCase('Alle drei Geraetevarianten erkennen Fahrzeug und Phasen', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($definitionen, $stationen): void {
|
||||||
|
foreach ($stationen as $name => $stationId) {
|
||||||
|
IPS_RequestAction($stationId, 'Aktiv', true);
|
||||||
|
$test->assertSame(true, GetValue($test->objectByIdent('FahrzeugVerbunden', $stationId)));
|
||||||
|
$test->assertSame(
|
||||||
|
$definitionen[$name]['Phasenzahl'],
|
||||||
|
GetValue($test->objectByIdent('Phasenzahl', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertSame(false, GetValue($test->objectByIdent('FahrzeugGeladen', $stationId)));
|
||||||
|
IPS_RequestAction($stationId, 'Solarladen', false);
|
||||||
|
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
|
||||||
|
$diagnose = json_encode([
|
||||||
|
'Aktiv' => GetValue($test->objectByIdent('Aktiv', $stationId)),
|
||||||
|
'Ladefreigabe' => GetValue($test->objectByIdent('Ladefreigabe', $stationId)),
|
||||||
|
'Solarladen' => GetValue($test->objectByIdent('Solarladen', $stationId)),
|
||||||
|
'FahrzeugGeladen' => GetValue($test->objectByIdent('FahrzeugGeladen', $stationId)),
|
||||||
|
'Phasenzahl' => GetValue($test->objectByIdent('Phasenzahl', $stationId)),
|
||||||
|
'Status' => IPS_GetInstance($stationId)['InstanceStatus'],
|
||||||
|
'Stoertext' => GetValue($test->objectByIdent('Stoertext', $stationId)),
|
||||||
|
], JSON_THROW_ON_ERROR);
|
||||||
|
$test->assertTrue(str_contains(
|
||||||
|
$befehl,
|
||||||
|
$definitionen[$name]['BefehlUrlTeil']
|
||||||
|
), $name . ': Der erwartete 16-A-Geraetebefehl fehlt. Erhalten: ' . $befehl
|
||||||
|
. '; Diagnose: ' . $diagnose);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Solar- und Peakbetrieb liefern die Enelix-1-Angebote', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($stationen, $anforderungSenden): void {
|
||||||
|
$stationId = $stationen['go-e alt'];
|
||||||
|
IPS_RequestAction($stationId, 'Solarladen', true);
|
||||||
|
$anforderungSenden($stationId, 4150);
|
||||||
|
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
|
||||||
|
$diagnose = json_encode([
|
||||||
|
'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $stationId)),
|
||||||
|
'SollwertGueltig' => GetValue($test->objectByIdent('SollwertGueltig', $stationId)),
|
||||||
|
'Status' => IPS_GetInstance($stationId)['InstanceStatus'],
|
||||||
|
'Stoertext' => GetValue($test->objectByIdent('Stoertext', $stationId)),
|
||||||
|
], JSON_THROW_ON_ERROR);
|
||||||
|
$test->assertTrue(str_contains(
|
||||||
|
$befehl,
|
||||||
|
'amp=6'
|
||||||
|
), 'Solarladen: Der 6-A-Befehl fehlt. Erhalten: ' . $befehl . '; Diagnose: ' . $diagnose);
|
||||||
|
|
||||||
|
IPS_RequestAction($stationId, 'Solarladen', false);
|
||||||
|
$angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId));
|
||||||
|
$test->assertSame('[11000]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.');
|
||||||
|
$abgewiesen = false;
|
||||||
|
try {
|
||||||
|
$anforderungSenden($stationId, 4150);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$abgewiesen = true;
|
||||||
|
}
|
||||||
|
if (!$abgewiesen) {
|
||||||
|
$abgewiesen = GetValue($test->objectByIdent('SollwertGueltig', $stationId)) !== true
|
||||||
|
|| GetValue($test->objectByIdent('Sollleistung', $stationId)) !== 4150;
|
||||||
|
}
|
||||||
|
$test->assertSame(true, $abgewiesen, 'Normalbetrieb muss eine Zwischenstufe ablehnen.');
|
||||||
|
|
||||||
|
$anforderungSenden($stationId, 4150, 'Peak');
|
||||||
|
$test->assertSame(
|
||||||
|
'[0,4150,4850,5550,6250,6950,7600,8300,9000,9700,10300,11000]',
|
||||||
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertTrue(str_contains(
|
||||||
|
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
|
||||||
|
'amp=6'
|
||||||
|
), 'Peak ohne Solarladen muss die Ladestufen annehmen.');
|
||||||
|
|
||||||
|
IPS_RequestAction($stationId, 'Solarladen', true);
|
||||||
|
$anforderungSenden($stationId, 0, 'Peak');
|
||||||
|
$test->assertSame(
|
||||||
|
'[0]',
|
||||||
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertTrue(str_contains(
|
||||||
|
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
|
||||||
|
'alw=0'
|
||||||
|
), 'Peak mit Solarladen muss die Ladestation stoppen.');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -44,9 +44,33 @@ return static function (TestContext $test): void {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$pufferTemperaturId = $test->createVariable(2, 'Puffertemperatur', 52.0);
|
||||||
|
$aussenTemperaturId = $test->createVariable(2, 'Aussentemperatur', 5.0);
|
||||||
|
$pufferKontakte = [
|
||||||
|
$test->createActionVariable('Pufferspeicher Stufe 1'),
|
||||||
|
$test->createActionVariable('Pufferspeicher Stufe 2'),
|
||||||
|
];
|
||||||
|
$pufferspeicherId = $test->createInstance(
|
||||||
|
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
|
||||||
|
'Pufferspeicher',
|
||||||
|
[
|
||||||
|
'PufferfuehlerVariableID' => $pufferTemperaturId,
|
||||||
|
'AussentemperaturVariableID' => $aussenTemperaturId,
|
||||||
|
'LeistungsStufen' => json_encode([
|
||||||
|
['Stufe' => 1, 'Leistung' => 1000, 'Schaltkontakt_Stufe' => $pufferKontakte[0]],
|
||||||
|
['Stufe' => 2, 'Leistung' => 2000, 'Schaltkontakt_Stufe' => $pufferKontakte[1]],
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
'LastwechselSperrzeit' => 0,
|
||||||
|
'PrioritaetPV' => 2,
|
||||||
|
'PrioritaetPeak' => 2,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$zuordnung = [
|
$zuordnung = [
|
||||||
['InstanzID' => $einStufigId, 'Aktiv' => true],
|
['InstanzID' => $einStufigId, 'Aktiv' => true],
|
||||||
['InstanzID' => $warmwasserId, 'Aktiv' => true],
|
['InstanzID' => $warmwasserId, 'Aktiv' => true],
|
||||||
|
['InstanzID' => $pufferspeicherId, 'Aktiv' => true],
|
||||||
];
|
];
|
||||||
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
|
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
|
||||||
$managerId = $test->createInstance(
|
$managerId = $test->createInstance(
|
||||||
@@ -67,8 +91,8 @@ return static function (TestContext $test): void {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$melden = static function () use ($einStufigId, $warmwasserId): void {
|
$melden = static function () use ($einStufigId, $warmwasserId, $pufferspeicherId): void {
|
||||||
foreach ([$einStufigId, $warmwasserId] as $instanzId) {
|
foreach ([$einStufigId, $warmwasserId, $pufferspeicherId] as $instanzId) {
|
||||||
IPS_RequestAction($instanzId, 'Aktiv', true);
|
IPS_RequestAction($instanzId, 'Aktiv', true);
|
||||||
IPS_RequestAction($instanzId, 'Melden', true);
|
IPS_RequestAction($instanzId, 'Melden', true);
|
||||||
}
|
}
|
||||||
@@ -77,10 +101,12 @@ return static function (TestContext $test): void {
|
|||||||
$netzleistungId,
|
$netzleistungId,
|
||||||
$managerId,
|
$managerId,
|
||||||
$einStufigId,
|
$einStufigId,
|
||||||
$warmwasserId
|
$warmwasserId,
|
||||||
|
$pufferspeicherId
|
||||||
): void {
|
): void {
|
||||||
IPS_RequestAction($einStufigId, 'Melden', true);
|
IPS_RequestAction($einStufigId, 'Melden', true);
|
||||||
IPS_RequestAction($warmwasserId, 'Melden', true);
|
IPS_RequestAction($warmwasserId, 'Melden', true);
|
||||||
|
IPS_RequestAction($pufferspeicherId, 'Melden', true);
|
||||||
SetValue($netzleistungId, $netzleistung);
|
SetValue($netzleistungId, $netzleistung);
|
||||||
IPS_RequestAction($managerId, 'Regeln', true);
|
IPS_RequestAction($managerId, 'Regeln', true);
|
||||||
};
|
};
|
||||||
@@ -88,6 +114,8 @@ return static function (TestContext $test): void {
|
|||||||
(bool) GetValue($einKontaktId),
|
(bool) GetValue($einKontaktId),
|
||||||
(bool) GetValue($warmKontakte[0]),
|
(bool) GetValue($warmKontakte[0]),
|
||||||
(bool) GetValue($warmKontakte[1]),
|
(bool) GetValue($warmKontakte[1]),
|
||||||
|
(bool) GetValue($pufferKontakte[0]),
|
||||||
|
(bool) GetValue($pufferKontakte[1]),
|
||||||
];
|
];
|
||||||
$warteAufKontakte = static function (array $erwartet, string $schritt) use (
|
$warteAufKontakte = static function (array $erwartet, string $schritt) use (
|
||||||
$test,
|
$test,
|
||||||
@@ -150,20 +178,20 @@ return static function (TestContext $test): void {
|
|||||||
|
|
||||||
$test->runCase('Manager und alle vorhandenen Verbrauchertypen sind verknüpft', static function (
|
$test->runCase('Manager und alle vorhandenen Verbrauchertypen sind verknüpft', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use ($managerId, $einStufigId, $warmwasserId, $melden): void {
|
) use ($managerId, $einStufigId, $warmwasserId, $pufferspeicherId, $melden): void {
|
||||||
IPS_RequestAction($managerId, 'Aktiv', true);
|
IPS_RequestAction($managerId, 'Aktiv', true);
|
||||||
$melden();
|
$melden();
|
||||||
IPS_RequestAction($managerId, 'Regeln', true);
|
IPS_RequestAction($managerId, 'Regeln', true);
|
||||||
|
|
||||||
foreach ([$managerId, $einStufigId, $warmwasserId] as $instanzId) {
|
foreach ([$managerId, $einStufigId, $warmwasserId, $pufferspeicherId] as $instanzId) {
|
||||||
$test->assertInstanceStatus($instanzId);
|
$test->assertInstanceStatus($instanzId);
|
||||||
}
|
}
|
||||||
$test->assertSame(
|
$test->assertSame(
|
||||||
2,
|
3,
|
||||||
GetValue($test->objectByIdent('VerbraucherAnzahl', $managerId))
|
GetValue($test->objectByIdent('VerbraucherAnzahl', $managerId))
|
||||||
);
|
);
|
||||||
$test->assertSame(
|
$test->assertSame(
|
||||||
2,
|
3,
|
||||||
GetValue($test->objectByIdent('VerbraucherVerfuegbar', $managerId))
|
GetValue($test->objectByIdent('VerbraucherVerfuegbar', $managerId))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -172,22 +200,22 @@ return static function (TestContext $test): void {
|
|||||||
TestContext $test
|
TestContext $test
|
||||||
) use ($managerId, $regeln, $warteAufKontakte): void {
|
) use ($managerId, $regeln, $warteAufKontakte): void {
|
||||||
$regeln(-3000.0);
|
$regeln(-3000.0);
|
||||||
$warteAufKontakte([true, false, true], 'Netz -3000 W');
|
$warteAufKontakte([true, false, true, false, false], 'Netz -3000 W');
|
||||||
$test->assertSame('PV', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
$test->assertSame('PV', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
||||||
|
|
||||||
$regeln(-1000.0);
|
$regeln(-1000.0);
|
||||||
$warteAufKontakte([true, false, true], 'Netz -1000 W');
|
$warteAufKontakte([true, false, true, false, false], 'Netz -1000 W');
|
||||||
$regeln(0.0);
|
$regeln(0.0);
|
||||||
$warteAufKontakte([true, false, true], 'Netz 0 W');
|
$warteAufKontakte([true, false, true, false, false], 'Netz 0 W');
|
||||||
|
|
||||||
IPS_Sleep(1100);
|
IPS_Sleep(1100);
|
||||||
$regeln(1000.0);
|
$regeln(1000.0);
|
||||||
$warteAufKontakte([true, true, false], 'Netz +1000 W');
|
$warteAufKontakte([false, false, false, false, false], 'Netz +1000 W');
|
||||||
$test->assertSame('Peak', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
$test->assertSame('Peak', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
||||||
|
|
||||||
IPS_Sleep(1100);
|
IPS_Sleep(1100);
|
||||||
$regeln(3000.0);
|
$regeln(3000.0);
|
||||||
$warteAufKontakte([false, false, false], 'Netz +3000 W');
|
$warteAufKontakte([false, false, false, false, false], 'Netz +3000 W');
|
||||||
});
|
});
|
||||||
|
|
||||||
$test->runCase('Keep-alive ist aktiv und erneuert unveränderte Sollwerte', static function (
|
$test->runCase('Keep-alive ist aktiv und erneuert unveränderte Sollwerte', static function (
|
||||||
@@ -216,7 +244,7 @@ return static function (TestContext $test): void {
|
|||||||
$melden();
|
$melden();
|
||||||
IPS_RequestAction($managerId, 'Regeln', false);
|
IPS_RequestAction($managerId, 'Regeln', false);
|
||||||
$daten = $verbraucherstatus();
|
$daten = $verbraucherstatus();
|
||||||
$test->assertSame(2, count($daten['Verbraucher'] ?? []));
|
$test->assertSame(3, count($daten['Verbraucher'] ?? []));
|
||||||
|
|
||||||
SetValue($temperaturId, 53.0);
|
SetValue($temperaturId, 53.0);
|
||||||
$test->waitUntil(static function () use (
|
$test->waitUntil(static function () use (
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\SymconTest\TestContext;
|
||||||
|
|
||||||
|
return static function (TestContext $test): void {
|
||||||
|
$pufferId = $test->createVariable(2, 'Puffertemperatur', 40.0, null, '~Temperature');
|
||||||
|
$aussenId = $test->createVariable(2, 'Aussentemperatur', 5.0, null, '~Temperature');
|
||||||
|
$wpSollId = $test->createVariable(2, 'WP Solltemperatur', 50.0, null, '~Temperature');
|
||||||
|
$kontakte = [
|
||||||
|
$test->createActionVariable('Puffer Stufe 1'),
|
||||||
|
$test->createActionVariable('Puffer Stufe 2'),
|
||||||
|
];
|
||||||
|
$stufen = [
|
||||||
|
['Stufe' => 1, 'Leistung' => 1000, 'Schaltkontakt_Stufe' => $kontakte[0]],
|
||||||
|
['Stufe' => 2, 'Leistung' => 2000, 'Schaltkontakt_Stufe' => $kontakte[1]],
|
||||||
|
];
|
||||||
|
$verbraucherId = $test->createInstance(
|
||||||
|
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
|
||||||
|
'Pufferspeicher',
|
||||||
|
[
|
||||||
|
'PufferfuehlerVariableID' => $pufferId,
|
||||||
|
'AussentemperaturVariableID' => $aussenId,
|
||||||
|
'WaermepumpenSolltemperaturVariableID' => $wpSollId,
|
||||||
|
'LeistungsStufen' => json_encode($stufen, JSON_THROW_ON_ERROR),
|
||||||
|
'FusspunktVorlauftemperatur' => 35.0,
|
||||||
|
'HeizkurvenSteigung' => 1.0,
|
||||||
|
'HeizkurveMinimaltemperatur' => 20.0,
|
||||||
|
'HeizkurveMaximaltemperatur' => 80.0,
|
||||||
|
'Hysterese' => 5.0,
|
||||||
|
'LastwechselSperrzeit' => 0,
|
||||||
|
'TemperaturMaxAlter' => 120,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$managerId = $test->createInstance(
|
||||||
|
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||||
|
'Hilfsmanager Pufferspeicher',
|
||||||
|
[
|
||||||
|
'AutomatischeSuche' => false,
|
||||||
|
'VerbraucherZuordnung' => json_encode([
|
||||||
|
['InstanzID' => $verbraucherId, 'Aktiv' => true],
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$managerdatenSenden = static function (
|
||||||
|
string $betriebsart,
|
||||||
|
?int $leistung
|
||||||
|
) use ($managerId, $verbraucherId): void {
|
||||||
|
IPS_RequestAction($verbraucherId, 'ManagerdatenEmpfangen', json_encode([
|
||||||
|
'Kopf' => [
|
||||||
|
'Version' => '4.0',
|
||||||
|
'AbsenderID' => $managerId,
|
||||||
|
'EmpfaengerID' => $verbraucherId,
|
||||||
|
'Zeitpunkt' => time(),
|
||||||
|
],
|
||||||
|
'Betriebsart' => $betriebsart,
|
||||||
|
'Sollleistung_W' => $leistung,
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
};
|
||||||
|
$kontaktzustand = static fn (): array => array_map(
|
||||||
|
static fn (int $id): bool => (bool) GetValue($id),
|
||||||
|
$kontakte
|
||||||
|
);
|
||||||
|
|
||||||
|
$test->runCase('Instanz und Variablen werden angelegt', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($verbraucherId): void {
|
||||||
|
$test->assertInstanceStatus($verbraucherId);
|
||||||
|
foreach ([
|
||||||
|
'Aktiv',
|
||||||
|
'Puffertemperatur',
|
||||||
|
'Aussentemperatur',
|
||||||
|
'Solltemperatur',
|
||||||
|
'Heizbedarf',
|
||||||
|
'Istleistung',
|
||||||
|
'Sollleistung',
|
||||||
|
'AktiveStufe',
|
||||||
|
'TemperaturenGueltig',
|
||||||
|
] as $ident) {
|
||||||
|
$test->objectByIdent($ident, $verbraucherId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Unter der Einschaltschwelle kann PV eine Stufe schalten', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($verbraucherId, $managerdatenSenden, $kontaktzustand): void {
|
||||||
|
IPS_RequestAction($verbraucherId, 'Aktiv', true);
|
||||||
|
$managerdatenSenden('PV', null);
|
||||||
|
$test->assertSame(true, GetValue($test->objectByIdent('Heizbedarf', $verbraucherId)));
|
||||||
|
|
||||||
|
$managerdatenSenden('PV', 2000);
|
||||||
|
$test->waitUntil(static fn (): bool => $kontaktzustand() === [false, true]);
|
||||||
|
$test->assertSame([false, true], $kontaktzustand());
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('In der Hysterese und oberhalb des Sollwerts wird ausgeschaltet', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($pufferId, $verbraucherId, $kontaktzustand): void {
|
||||||
|
SetValue($pufferId, 47.0);
|
||||||
|
IPS_RequestAction($verbraucherId, 'Melden', true);
|
||||||
|
$test->waitUntil(static fn (): bool => $kontaktzustand() === [false, false]);
|
||||||
|
$test->assertSame(false, GetValue($test->objectByIdent('Heizbedarf', $verbraucherId)));
|
||||||
|
|
||||||
|
SetValue($pufferId, 55.0);
|
||||||
|
IPS_RequestAction($verbraucherId, 'Melden', true);
|
||||||
|
$test->assertSame([false, false], $kontaktzustand());
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Peakbetrieb sperrt den Pufferspeicher unabhaengig von der Temperatur', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($pufferId, $verbraucherId, $managerdatenSenden, $kontaktzustand): void {
|
||||||
|
SetValue($pufferId, 20.0);
|
||||||
|
$managerdatenSenden('Peak', null);
|
||||||
|
$test->assertSame(false, GetValue($test->objectByIdent('Heizbedarf', $verbraucherId)));
|
||||||
|
$test->waitUntil(static fn (): bool => $kontaktzustand() === [false, false]);
|
||||||
|
|
||||||
|
$abgewiesen = false;
|
||||||
|
try {
|
||||||
|
$managerdatenSenden('Peak', 1000);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$abgewiesen = true;
|
||||||
|
}
|
||||||
|
if (!$abgewiesen) {
|
||||||
|
$abgewiesen = GetValue($test->objectByIdent('SollwertGueltig', $verbraucherId)) !== true
|
||||||
|
|| GetValue($test->objectByIdent('Sollleistung', $verbraucherId)) !== 1000;
|
||||||
|
}
|
||||||
|
$test->assertSame(true, $abgewiesen, 'Peak darf keine Heizstufe anbieten.');
|
||||||
|
$test->assertSame([false, false], $kontaktzustand());
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Lokales Ausschalten schaltet jede Stufe aus', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($verbraucherId, $kontaktzustand): void {
|
||||||
|
IPS_RequestAction($verbraucherId, 'Aktiv', false);
|
||||||
|
$test->waitUntil(static fn (): bool => $kontaktzustand() === [false, false]);
|
||||||
|
$test->assertSame([false, false], $kontaktzustand());
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('WP-Button kalibriert den Heizkurven-Fusspunkt', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($verbraucherId): void {
|
||||||
|
IPS_SetProperty($verbraucherId, 'FusspunktVorlauftemperatur', 30.0);
|
||||||
|
IPS_ApplyChanges($verbraucherId);
|
||||||
|
IPS_RequestAction($verbraucherId, 'HeizkurveVonWaermepumpeUebernehmen', 0);
|
||||||
|
|
||||||
|
$test->assertEquals(
|
||||||
|
35.0,
|
||||||
|
(float) IPS_GetProperty($verbraucherId, 'FusspunktVorlauftemperatur'),
|
||||||
|
0.001
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -32,17 +32,21 @@ return static function (TestContext $test): void {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$anforderungSenden = static function (int $leistung) use (
|
$anforderungSenden = static function (
|
||||||
|
?int $leistung,
|
||||||
|
string $betriebsart = 'PV'
|
||||||
|
) use (
|
||||||
$managerId,
|
$managerId,
|
||||||
$verbraucherId
|
$verbraucherId
|
||||||
): void {
|
): void {
|
||||||
$paket = [
|
$paket = [
|
||||||
'Kopf' => [
|
'Kopf' => [
|
||||||
'Version' => '3.0',
|
'Version' => '4.0',
|
||||||
'AbsenderID' => $managerId,
|
'AbsenderID' => $managerId,
|
||||||
'EmpfaengerID' => $verbraucherId,
|
'EmpfaengerID' => $verbraucherId,
|
||||||
'Zeitpunkt' => time(),
|
'Zeitpunkt' => time(),
|
||||||
],
|
],
|
||||||
|
'Betriebsart' => $betriebsart,
|
||||||
'Sollleistung_W' => $leistung,
|
'Sollleistung_W' => $leistung,
|
||||||
];
|
];
|
||||||
IPS_RequestAction(
|
IPS_RequestAction(
|
||||||
@@ -64,6 +68,28 @@ return static function (TestContext $test): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$test->runCase('PV und Peak liefern unterschiedliche Leistungsangebote', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($verbraucherId, $anforderungSenden): void {
|
||||||
|
IPS_RequestAction($verbraucherId, 'Aktiv', true);
|
||||||
|
IPS_Sleep(2100);
|
||||||
|
$anforderungSenden(null, 'Peak');
|
||||||
|
|
||||||
|
$abgewiesen = false;
|
||||||
|
try {
|
||||||
|
$anforderungSenden(1200, 'Peak');
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$abgewiesen = true;
|
||||||
|
}
|
||||||
|
if (!$abgewiesen) {
|
||||||
|
$abgewiesen = GetValue($test->objectByIdent('SollwertGueltig', $verbraucherId)) !== true
|
||||||
|
|| GetValue($test->objectByIdent('Sollleistung', $verbraucherId)) !== 1200;
|
||||||
|
}
|
||||||
|
$test->assertSame(true, $abgewiesen, 'Peak darf die Nennleistung nicht anbieten.');
|
||||||
|
|
||||||
|
$anforderungSenden(null, 'PV');
|
||||||
|
});
|
||||||
|
|
||||||
$test->runCase('Nur angebotene Leistungsanforderungen werden angenommen', static function (
|
$test->runCase('Nur angebotene Leistungsanforderungen werden angenommen', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use (
|
) use (
|
||||||
@@ -143,7 +169,7 @@ return static function (TestContext $test): void {
|
|||||||
SetValue($rueckmeldungId, true);
|
SetValue($rueckmeldungId, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
$test->runCase('Tägliche Mindestlaufzeit wird spätestens rechtzeitig erzwungen', static function (
|
$test->runCase('Peak kann die Tagesmindestlaufzeit ohne angebotene Sperre erzwingen', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use (
|
) use (
|
||||||
$verbraucherId,
|
$verbraucherId,
|
||||||
@@ -155,7 +181,13 @@ return static function (TestContext $test): void {
|
|||||||
$anforderungSenden(0);
|
$anforderungSenden(0);
|
||||||
$test->waitUntil(static fn (): bool => GetValue($kontaktId) === false);
|
$test->waitUntil(static fn (): bool => GetValue($kontaktId) === false);
|
||||||
SetValue($rueckmeldungId, false);
|
SetValue($rueckmeldungId, false);
|
||||||
|
$test->waitUntil(static fn (): bool => GetValue(
|
||||||
|
$test->objectByIdent('Schaltzustand', $verbraucherId)
|
||||||
|
) === false);
|
||||||
|
IPS_Sleep(2100);
|
||||||
|
|
||||||
|
$anforderungSenden(null, 'Peak');
|
||||||
|
IPS_SetProperty($verbraucherId, 'PeakSperreBeiMindestlaufzeitAnbieten', false);
|
||||||
IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 86400);
|
IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 86400);
|
||||||
IPS_ApplyChanges($verbraucherId);
|
IPS_ApplyChanges($verbraucherId);
|
||||||
$test->waitUntil(static fn (): bool => GetValue($kontaktId) === true);
|
$test->waitUntil(static fn (): bool => GetValue($kontaktId) === true);
|
||||||
@@ -163,9 +195,14 @@ return static function (TestContext $test): void {
|
|||||||
$test->objectByIdent('Sollleistung', $verbraucherId)
|
$test->objectByIdent('Sollleistung', $verbraucherId)
|
||||||
));
|
));
|
||||||
SetValue($rueckmeldungId, true);
|
SetValue($rueckmeldungId, true);
|
||||||
|
$test->waitUntil(static fn (): bool => GetValue(
|
||||||
|
$test->objectByIdent('Schaltzustand', $verbraucherId)
|
||||||
|
) === true);
|
||||||
|
|
||||||
IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 0);
|
IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 0);
|
||||||
|
IPS_SetProperty($verbraucherId, 'PeakSperreBeiMindestlaufzeitAnbieten', true);
|
||||||
IPS_ApplyChanges($verbraucherId);
|
IPS_ApplyChanges($verbraucherId);
|
||||||
|
$anforderungSenden(null, 'PV');
|
||||||
});
|
});
|
||||||
|
|
||||||
$test->runCase('Physische Rückmeldung bestätigt und überwacht den Zustand', static function (
|
$test->runCase('Physische Rückmeldung bestätigt und überwacht den Zustand', static function (
|
||||||
|
|||||||
@@ -38,11 +38,13 @@ return static function (TestContext $test): void {
|
|||||||
'DiagnosevariablenAnzeigen' => true,
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
|
||||||
$managerId = $test->createInstance(
|
$managerId = $test->createInstance(
|
||||||
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||||
'Hilfsmanager',
|
'Hilfsmanager',
|
||||||
[
|
[
|
||||||
'NetzleistungVariableID' => $netzleistungId,
|
'NetzleistungVariableID' => $netzleistungId,
|
||||||
|
'Lizenzcode' => $lizenzcode,
|
||||||
'AutomatischeSuche' => false,
|
'AutomatischeSuche' => false,
|
||||||
'VerbraucherZuordnung' => json_encode([
|
'VerbraucherZuordnung' => json_encode([
|
||||||
['InstanzID' => $verbraucherId, 'Aktiv' => true],
|
['InstanzID' => $verbraucherId, 'Aktiv' => true],
|
||||||
@@ -88,6 +90,17 @@ return static function (TestContext $test): void {
|
|||||||
IPS_RequestAction($managerId, 'Regeln', true);
|
IPS_RequestAction($managerId, 'Regeln', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($lizenzcode === '') {
|
||||||
|
$test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($verbraucherId, $managerId): void {
|
||||||
|
$test->assertInstanceStatus($verbraucherId);
|
||||||
|
$test->assertInstanceStatus($managerId, 203);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$test->runCase('Instanzen und Diagnosevariablen werden angelegt', static function (
|
$test->runCase('Instanzen und Diagnosevariablen werden angelegt', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use ($verbraucherId, $managerId): void {
|
) use ($verbraucherId, $managerId): void {
|
||||||
@@ -119,7 +132,7 @@ return static function (TestContext $test): void {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$test->runCase('Unter Minimum wird in PV und Peak bis über die Hysterese geheizt', static function (
|
$test->runCase('Unter Minimum erzwingt PV das Maximum und Peak bietet alle Stufen', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use (
|
) use (
|
||||||
$temperaturSetzen,
|
$temperaturSetzen,
|
||||||
@@ -132,17 +145,19 @@ return static function (TestContext $test): void {
|
|||||||
$warteAufKontakte([false, false, true]);
|
$warteAufKontakte([false, false, true]);
|
||||||
$test->assertSame('PV', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
$test->assertSame('PV', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
||||||
|
|
||||||
$managerRegeln(3000.0);
|
IPS_Sleep(1100);
|
||||||
$warteAufKontakte([false, false, true]);
|
$managerRegeln(1000.0);
|
||||||
|
$warteAufKontakte([false, true, false]);
|
||||||
$test->assertSame('Peak', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
$test->assertSame('Peak', GetValue($test->objectByIdent('Betriebsart', $managerId)));
|
||||||
|
|
||||||
$temperaturSetzen(49.9);
|
IPS_Sleep(1100);
|
||||||
$managerRegeln(3000.0);
|
$temperaturSetzen(47.0);
|
||||||
$warteAufKontakte([false, false, true]);
|
$managerRegeln(1000.0);
|
||||||
|
$warteAufKontakte([true, false, false]);
|
||||||
|
|
||||||
IPS_Sleep(1100);
|
IPS_Sleep(1100);
|
||||||
$temperaturSetzen(50.0);
|
$temperaturSetzen(50.0);
|
||||||
$managerRegeln(3000.0);
|
$managerRegeln(1000.0);
|
||||||
$warteAufKontakte([false, false, false]);
|
$warteAufKontakte([false, false, false]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,12 +185,12 @@ return static function (TestContext $test): void {
|
|||||||
$managerRegeln(-3000.0);
|
$managerRegeln(-3000.0);
|
||||||
$warteAufKontakte([false, false, false]);
|
$warteAufKontakte([false, false, false]);
|
||||||
|
|
||||||
$temperaturSetzen(55.0);
|
$temperaturSetzen(54.9);
|
||||||
$managerRegeln(-3000.0);
|
$managerRegeln(-3000.0);
|
||||||
$warteAufKontakte([false, false, true]);
|
$warteAufKontakte([false, false, true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
$test->runCase('Legionellenziel erlaubt Peak-Stufen und lädt in PV maximal', static function (
|
$test->runCase('Erhöhtes Maximalziel bleibt im freien Peakbetrieb gesperrt', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use (
|
) use (
|
||||||
$verbraucherId,
|
$verbraucherId,
|
||||||
@@ -190,10 +205,10 @@ return static function (TestContext $test): void {
|
|||||||
|
|
||||||
IPS_Sleep(1100);
|
IPS_Sleep(1100);
|
||||||
$managerRegeln(2000.0);
|
$managerRegeln(2000.0);
|
||||||
$warteAufKontakte([true, false, false]);
|
$warteAufKontakte([false, false, false]);
|
||||||
});
|
});
|
||||||
|
|
||||||
$test->runCase('Zeitplan erzwingt rechtzeitiges Aufheizen wie ein Minimum', static function (
|
$test->runCase('Zeitplan bietet in Peak alle Stufen und erzwingt in PV das Maximum', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use (
|
) use (
|
||||||
$verbraucherId,
|
$verbraucherId,
|
||||||
@@ -211,6 +226,12 @@ return static function (TestContext $test): void {
|
|||||||
['Uhrzeit' => date('H:i', time() + 60), 'Solltemperatur' => 60],
|
['Uhrzeit' => date('H:i', time() + 60), 'Solltemperatur' => 60],
|
||||||
], JSON_THROW_ON_ERROR));
|
], JSON_THROW_ON_ERROR));
|
||||||
IPS_ApplyChanges($verbraucherId);
|
IPS_ApplyChanges($verbraucherId);
|
||||||
|
$test->assertSame(true, GetValue(
|
||||||
|
$test->objectByIdent('AenderungMoeglich', $verbraucherId)
|
||||||
|
));
|
||||||
|
$warteAufKontakte([false, false, false]);
|
||||||
|
|
||||||
|
$managerRegeln(-3000.0);
|
||||||
$warteAufKontakte([false, false, true]);
|
$warteAufKontakte([false, false, true]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class SymconTestContractTest extends TestCase
|
|||||||
public function testAffectedModuleResolverSelectsAllMatches(): void
|
public function testAffectedModuleResolverSelectsAllMatches(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
['Manager', 'VerbraucherEinStufig', 'Warmwassererwaermer'],
|
['LadestationStandAlone', 'Manager', 'Pufferspeicher', 'VerbraucherEinStufig', 'Warmwassererwaermer'],
|
||||||
$this->resolveAffectedModules(['libs/Nachrichtenvertrag.php'])
|
$this->resolveAffectedModules(['libs/Nachrichtenvertrag.php'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ final class SymconTestContractTest extends TestCase
|
|||||||
public function testAffectedModuleResolverSelectsAllModulesForFrameworkChanges(): void
|
public function testAffectedModuleResolverSelectsAllModulesForFrameworkChanges(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
['Manager', 'VerbraucherEinStufig', 'Warmwassererwaermer'],
|
['LadestationStandAlone', 'Manager', 'Pufferspeicher', 'VerbraucherEinStufig', 'Warmwassererwaermer'],
|
||||||
$this->resolveAffectedModules(['tests/Symcon/TestContext.php'])
|
$this->resolveAffectedModules(['tests/Symcon/TestContext.php'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,23 +16,43 @@ final class EinStufigReglerTest extends TestCase
|
|||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[0, 3500],
|
[0, 3500],
|
||||||
EinStufigRegler::leistungsangebot(3500, 0, true, true, false)
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, false, false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGesperrtesOderErzwungenesAngebotEnthaeltNurIstleistung(): void
|
public function testPeakbetriebBietetBeiFreierSchaltungNurAus(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, false, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTechnischGesperrtesAngebotEnthaeltNurIstleistung(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[3500],
|
[3500],
|
||||||
EinStufigRegler::leistungsangebot(3500, 3500, true, false, false)
|
EinStufigRegler::leistungsangebot(3500, 3500, true, false, false, true)
|
||||||
);
|
|
||||||
self::assertSame(
|
|
||||||
[0],
|
|
||||||
EinStufigRegler::leistungsangebot(3500, 0, true, true, true)
|
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[3500],
|
[3500],
|
||||||
EinStufigRegler::leistungsangebot(3500, 3500, false, true, false)
|
EinStufigRegler::leistungsangebot(3500, 3500, false, true, false, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPeakSperreBeiMindestlaufzeitIstKonfigurierbar(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0, 3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, true, true, true)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, true, true, false)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, true, false, true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +60,7 @@ final class EinStufigReglerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->expectException(InvalidArgumentException::class);
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
EinStufigRegler::leistungsangebot(3500, 1200, true, true, false);
|
EinStufigRegler::leistungsangebot(3500, 1200, true, true, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFreigabeVerwendetZustandsabhaengigeMindestdauer(): void
|
public function testFreigabeVerwendetZustandsabhaengigeMindestdauer(): void
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
|
|||||||
'SchaltkontaktInvertiert',
|
'SchaltkontaktInvertiert',
|
||||||
'RueckmeldungVariableID',
|
'RueckmeldungVariableID',
|
||||||
'Mindestlaufzeit',
|
'Mindestlaufzeit',
|
||||||
|
'PeakSperreBeiMindestlaufzeitAnbieten',
|
||||||
'Mindesteinschaltdauer',
|
'Mindesteinschaltdauer',
|
||||||
'Mindestausschaltdauer',
|
'Mindestausschaltdauer',
|
||||||
'DiagnosevariablenAnzeigen',
|
'DiagnosevariablenAnzeigen',
|
||||||
@@ -130,7 +131,7 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testModulVerwendetAusschliesslichDenVertrag30(): void
|
public function testModulVerwendetVertrag40UndBetriebsartabhaengigesAngebot(): void
|
||||||
{
|
{
|
||||||
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
@@ -138,6 +139,9 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
|
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
|
||||||
self::assertStringContainsString('Nachrichtenvertrag::pruefeManagerdaten', $modul);
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeManagerdaten', $modul);
|
||||||
self::assertStringContainsString('Nachrichtenvertrag::pruefeVerbraucherdaten', $modul);
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeVerbraucherdaten', $modul);
|
||||||
|
self::assertStringContainsString("['Betriebsart']", $modul);
|
||||||
|
self::assertStringContainsString('BETRIEBSART_PEAK', $modul);
|
||||||
|
self::assertStringContainsString("'Betriebsart' =>", $modul);
|
||||||
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
|
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
|
||||||
self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $modul);
|
self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $modul);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,46 @@ final class WarmwasserReglerTest extends TestCase
|
|||||||
self::assertSame(0, WarmwasserRegler::stufeFuerLeistung($stufen, 0));
|
self::assertSame(0, WarmwasserRegler::stufeFuerLeistung($stufen, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPeakBietetUnterMinimumUndBeiAktiverUntererHystereseAlleStufen(): void
|
||||||
|
{
|
||||||
|
$werte = [0, 1000, 2000];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
$werte,
|
||||||
|
WarmwasserRegler::leistungsangebot(true, 44.0, 50.0, 60.0, 5.0, false, $werte)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
$werte,
|
||||||
|
WarmwasserRegler::leistungsangebot(true, 52.0, 50.0, 60.0, 5.0, true, $werte)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
WarmwasserRegler::leistungsangebot(true, 52.0, 50.0, 60.0, 5.0, false, $werte)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPvErzwingtUnterMinimumMaximumUndBietetSonstZustandsabhaengigAn(): void
|
||||||
|
{
|
||||||
|
$werte = [0, 1000, 2000];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[2000],
|
||||||
|
WarmwasserRegler::leistungsangebot(false, 44.0, 50.0, 60.0, 5.0, false, $werte)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
$werte,
|
||||||
|
WarmwasserRegler::leistungsangebot(false, 52.0, 50.0, 60.0, 5.0, false, $werte)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
$werte,
|
||||||
|
WarmwasserRegler::leistungsangebot(false, 58.0, 50.0, 60.0, 5.0, true, $werte)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
WarmwasserRegler::leistungsangebot(false, 58.0, 50.0, 60.0, 5.0, false, $werte)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testDoppelteKontakteWerdenAbgewiesen(): void
|
public function testDoppelteKontakteWerdenAbgewiesen(): void
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidArgumentException::class);
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|||||||
Reference in New Issue
Block a user