feat(manager): transmit estimated meter samples and SOC recovery state
This commit is contained in:
@@ -27,12 +27,22 @@ hinzugefuegt. SDL-/Grundlastbereinigung bleibt eine separate, noch offene Aufgab
|
||||
BMS-Grenzen werden mit Topologieleistungen begrenzt. Normale Betriebsreserve
|
||||
und technisches Minimum werden nicht verwechselt: die hoehere Grenze gilt.
|
||||
|
||||
Bei aktueller Entladehysterese wird die Entladeleistung konservativ auf 0
|
||||
begrenzt. Eine spaetere Freigabe innerhalb der 48h wird noch nicht modelliert.
|
||||
Das ist sichtbar diagnostiziert und kann den Schattenplan wirtschaftlich
|
||||
beschraenken; eine zeitabhaengige Hysterese-Modellierung bleibt offen.
|
||||
SOC unterhalb der Reserve wird nicht kuenstlich angehoben: Diagnosefehler,
|
||||
kein angeblich gueltiger Plan. Recovery-/Nachladungsmodell bleibt offen.
|
||||
Der erweiterte Sender uebermittelt Entladesperre und Wiederfreigabe-SOC separat
|
||||
von der maximalen Entladeleistung. Der neue Python-Rechenkern darf die Sperre
|
||||
im Zukunftsplan erst nach vorherigem Laden bis zur Wiederfreigabe aufheben.
|
||||
SOC unterhalb der Betriebsreserve, aber oberhalb des technischen Minimums,
|
||||
bleibt unveraendert erhalten: Es ist Wiederaufladung statt erfundener Energie
|
||||
vorgesehen. Unterhalb des technischen Minimums wird kein normaler Plan erzeugt.
|
||||
|
||||
Zusaetzlich wird meterObservation mit frischen Empfangswerten der Netzleistung
|
||||
und T1/T2-Summe uebermittelt. Dessen laufende Viertelstundenintegration ist eine
|
||||
explizite Planungsschaetzung, KEIN verifizierter Abrechnungsnachweis. Verwendung
|
||||
von Schaetzungen muss im V4-Dienst ausdruecklich aktiviert werden. Historische
|
||||
Peakannahmen werden getrennt von verifizierten Maxima gespeichert.
|
||||
|
||||
Diese Protokollerweiterung benoetigt zuerst den passenden Backendstand. Der
|
||||
bisherige laufende V4-Container und die Testanlage sind noch nicht aktualisiert.
|
||||
Offline-Szenarien und Quelltextpruefungen ersetzen keinen Symcon-Laufzeittest.
|
||||
|
||||
## Diagnose ohne Netzwerk oder Stellbefehl
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ trait ManagerNetzfahrplanV4Trait
|
||||
foreach (['Netzleistungsfaktor', 'Lastspitzengrenze', 'Einspeisegrenze'] as $key) {
|
||||
$manager[$key] = $this->ReadPropertyFloat($key);
|
||||
}
|
||||
$manager['V4ControlPlanSource'] = $this->leseJsonAttribut('Netzfahrplan')['sourceModel'] ?? 'legacy-unspecified';
|
||||
$manager['EinspeisebegrenzungAktiv'] = $this->ReadPropertyBoolean('EinspeisebegrenzungAktiv');
|
||||
$manager['NetzladenErlaubt'] = $this->ReadPropertyBoolean('NetzfahrplanV4NetzladenErlaubt');
|
||||
$manager['Monatsgrenzen'] = json_decode($this->ReadPropertyString('Monatsgrenzen'), true, 512, JSON_THROW_ON_ERROR);
|
||||
@@ -50,7 +51,7 @@ trait ManagerNetzfahrplanV4Trait
|
||||
}
|
||||
$c = ['InstanzID' => $id];
|
||||
foreach (['LadezustandVariableID', 'MaxLadeleistungVariableID', 'MaxEntladeleistungVariableID',
|
||||
'IstleistungVariableID', 'MindestLadezustand', 'ReserveLadezustand', 'MesswertMaxAlter', 'Batteriemanagement'] as $key) {
|
||||
'IstleistungVariableID', 'MindestLadezustand', 'ReserveLadezustand', 'LadezustandHysterese', 'MesswertMaxAlter', 'Batteriemanagement'] as $key) {
|
||||
$c[$key] = IPS_GetProperty($id, $key);
|
||||
}
|
||||
$entry = $cache[(string) $id] ?? [];
|
||||
|
||||
@@ -161,7 +161,8 @@ final class NetzfahrplanV4Betriebsdaten
|
||||
self::number($c['ReserveLadezustand'], 'Betriebsreserve', 0, 100));
|
||||
$max = self::number($opts['MaxSOC_Prozent'] ?? 100.0, 'Maximal-SOC', $min, 100);
|
||||
$socValue = self::number($soc['value'], 'SOC', 0, 100);
|
||||
if ($socValue < $min || $socValue > $max) {
|
||||
$physicalMin = self::number($c['MindestLadezustand'], 'Technisches Minimum', 0, $min);
|
||||
if ($socValue < $physicalMin || $socValue > $max) {
|
||||
throw new InvalidArgumentException($id . ': SOC ausserhalb des Planungsbereichs; kein kuenstliches Anheben.');
|
||||
}
|
||||
if (($c['EmpfangenAm'] ?? 0) > $now || $now - ($c['EmpfangenAm'] ?? 0) > (int) ($manager['VerbraucherTimeout'] ?? 60)) {
|
||||
@@ -173,14 +174,20 @@ final class NetzfahrplanV4Betriebsdaten
|
||||
$maxCharge = $maxDischarge = 0.0;
|
||||
$warnings[] = $id . ': nicht fuer Managerregelung verfuegbar; keine Batterieleistung eingeplant.';
|
||||
}
|
||||
if (!empty($c['HystereseAktiv'])) {
|
||||
$maxDischarge = 0.0;
|
||||
$warnings[] = $id . ': aktuelle Entladesperre konservativ uebernommen; keine spaetere Freigabe erfunden.';
|
||||
$rearm = min($max, $min + self::number($c['LadezustandHysterese'] ?? 0.0, 'Entladehysterese', 0, 100));
|
||||
$blocked = !empty($c['HystereseAktiv']) || $socValue <= $min;
|
||||
if ($blocked) {
|
||||
$warnings[] = $id . ': Entladesperre im Modell aktiv bis zur Wiederfreigabe nach vorheriger Ladung.';
|
||||
}
|
||||
if ($socValue < $min) {
|
||||
$warnings[] = $id . ': unter Betriebsreserve; realen SOC behalten und nur zulassige Wiederaufladung planen.';
|
||||
}
|
||||
$result[] = ['id' => $id, 'capacityKwh' => $capacity, 'socPercent' => $socValue,
|
||||
'minSocPercent' => $min, 'maxSocPercent' => $max, 'maxChargeW' => $maxCharge,
|
||||
'maxDischargeW' => $maxDischarge, 'measuredAt' => gmdate('c', min($soc['updated'], $charge['updated'], $discharge['updated'])),
|
||||
'gridCharging' => ($manager['NetzladenErlaubt'] ?? false) === true];
|
||||
'gridCharging' => ($manager['NetzladenErlaubt'] ?? false) === true,
|
||||
'physicalMinSocPercent' => $physicalMin, 'recoveryAllowed' => true,
|
||||
'dischargeBlocked' => $blocked, 'rearmSocPercent' => $rearm];
|
||||
}
|
||||
if ($result === []) {
|
||||
throw new InvalidArgumentException('Keine eindeutige steuerbare Batterie konfiguriert.');
|
||||
@@ -190,6 +197,22 @@ final class NetzfahrplanV4Betriebsdaten
|
||||
'meteringBoundary' => 'common_pcc', 'batteries' => $result,
|
||||
'limits' => ['importW' => $import, 'exportW' => $export, 'managerMonthLimitsW' => (object) $months],
|
||||
'measuredPeaks' => $peaks, 'quarterPast' => $past];
|
||||
// A fresh acquisition sample is an operational estimate. It does NOT
|
||||
// claim a calibrated billing-period boundary or full historical coverage.
|
||||
$policy = [
|
||||
'adapterVersion' => 'v4-native-estimated-meter-1',
|
||||
'sourcePlan' => $manager['V4ControlPlanSource'] ?? 'legacy-unspecified',
|
||||
'mode' => $mode, 'importW' => $import, 'exportW' => $export,
|
||||
'months' => $months,
|
||||
'batteryPolicy' => array_map(static function (array $b): array {
|
||||
return array_intersect_key($b, array_flip(['id', 'capacityKwh', 'minSocPercent', 'maxSocPercent', 'physicalMinSocPercent', 'rearmSocPercent']));
|
||||
}, $result),
|
||||
];
|
||||
$payload['meterObservation'] = [
|
||||
'meterId' => $meter['meterId'], 'sampleAt' => gmdate('c', $now),
|
||||
'powerW' => $gridW, 'totalImportKwh' => $meter['totalKwh'],
|
||||
'controlPolicyId' => hash('sha256', json_encode($policy, JSON_THROW_ON_ERROR)),
|
||||
];
|
||||
$payload['eventId'] = 'symcon-operation-' . hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
|
||||
return ['status' => $warnings === [] ? 'ready_shadow' : 'incomplete_shadow', 'warnings' => $warnings, 'bezugszaehler' => $meter, 'operation' => $payload];
|
||||
}
|
||||
|
||||
Vendored
+4
-3
@@ -86,9 +86,10 @@ final class NativeV4Scenarios
|
||||
$cases['stale_consumer_cache_fails'] = static function () use ($build, $c, $now, $fail): void {
|
||||
$c[0]['EmpfangenAm'] = $now - 61; $fail(static fn() => $build(null, null, $c));
|
||||
};
|
||||
$cases['soc_not_lifted_to_minimum'] = static function () use ($build, $read, $fail): void {
|
||||
$cases['soc_below_reserve_is_not_fabricated'] = static function () use ($build, $read, $check): void {
|
||||
$r = static function ($id) use ($read) { $v = $read($id); if ($id === 2) { $v['value'] = 5.; } return $v; };
|
||||
$fail(static fn() => $build(null, null, null, $r));
|
||||
$b = $build(null, null, null, $r)['operation']['batteries'][0];
|
||||
$check($b['socPercent'] === 5.0 && $b['minSocPercent'] === 15.0 && $b['physicalMinSocPercent'] === 3.0 && $b['recoveryAllowed'] === true && $b['dischargeBlocked'] === true);
|
||||
};
|
||||
$cases['unavailable_asset_no_power'] = static function () use ($build, $c, $check): void {
|
||||
$c[0]['Verfuegbar'] = false; $x = $build(null, null, $c)['operation']['batteries'][0];
|
||||
@@ -96,7 +97,7 @@ final class NativeV4Scenarios
|
||||
};
|
||||
$cases['hysteresis_not_silently_ignored'] = static function () use ($build, $c, $check): void {
|
||||
$c[0]['HystereseAktiv'] = true; $x = $build(null, null, $c);
|
||||
$check($x['operation']['batteries'][0]['maxDischargeW'] === 0.0 && count($x['warnings']) > 1);
|
||||
$check($x['operation']['batteries'][0]['dischargeBlocked'] === true && $x['operation']['batteries'][0]['maxDischargeW'] === 30000.0 && count($x['warnings']) > 1);
|
||||
};
|
||||
$cases['grid_charge_explicit_opt_in'] = static function () use ($build, $m, $check): void {
|
||||
$check($build()['operation']['batteries'][0]['gridCharging'] === false);
|
||||
|
||||
Reference in New Issue
Block a user