feat(workflow): publish verified forecast controls and consolidated docs
Tests / test (push) Successful in 1m1s

Approved by Daniel Haefliger for develop and beta. Author dh_Agent, authenticated account dh. Preserve published battery, charging and overall Energy Pie changes. No deployment or plant control authorization.
This commit is contained in:
dh
2026-10-08 10:02:58 +00:00
parent af69151165
commit 74a2d6c685
82 changed files with 5054 additions and 617 deletions
+17 -4
View File
@@ -29,6 +29,13 @@ final class NetzfahrplanV4Bilanzierung
public static function konfigurieren(array $c): array
{
$fields = ['version','sources','maxSkewSeconds','splitToleranceW'];
$physicalOnly = ($c['storageMode'] ?? null) === 'physical_only';
if (array_key_exists('storageMode', $c)) {
if (!$physicalOnly) {
throw new InvalidArgumentException('Unbekannte Speicherbilanzierung.');
}
$fields[] = 'storageMode';
}
if (count($c) !== count($fields) || array_diff($fields,array_keys($c)) || $c['version'] !== 1
|| !is_array($c['sources']) || !self::listArray($c['sources']) || count($c['sources']) < 3 || count($c['sources']) > 80
|| !is_int($c['maxSkewSeconds']) || $c['maxSkewSeconds'] < 0 || $c['maxSkewSeconds'] > 60) {
@@ -57,9 +64,13 @@ final class NetzfahrplanV4Bilanzierung
sort($s['dependsOn']);$keys[$s['key']]=true;$seen[$s['variableId']]=true;$roles[$s['role']]=($roles[$s['role']]??0)+1;
}
unset($s);
if (($roles['grid']??0)!==1 || !isset($roles['pv'],$roles['physical_storage'],$roles['controlled_virtual'])) {
if (($roles['grid']??0)!==1 || !isset($roles['pv'],$roles['physical_storage'])
|| (!$physicalOnly && !isset($roles['controlled_virtual']))) {
throw new InvalidArgumentException('Netz, PV, physische und steuerbare virtuelle Batterie muessen explizit zugeordnet sein.');
}
if ($physicalOnly && (isset($roles['controlled_virtual']) || isset($roles['external_virtual']))) {
throw new InvalidArgumentException('Physische Speicherbilanz darf keine virtuellen Anteile enthalten.');
}
$graph=[];foreach ($c['sources'] as $s) $graph[$s['key']]=$s['dependsOn'];
$visit=function($key,$stack=[]) use (&$visit,$graph): void {
if (!array_key_exists($key,$graph) || isset($stack[$key])) throw new InvalidArgumentException('Fehlende oder zyklische Ursprungsquelle.');
@@ -101,11 +112,13 @@ final class NetzfahrplanV4Bilanzierung
$load=$totals['grid']+$totals['pv']-$totals['physical_storage'];
$base=$load-$totals['flexible_load'];
// This channel preserves unallocated dynamics instead of hiding them in house load.
$residual=$totals['physical_storage']-$totals['controlled_virtual']-$totals['external_virtual'];
$external=$totals['external_virtual']+$residual;
$physicalOnly = ($c['storageMode'] ?? null) === 'physical_only';
$residual=$physicalOnly ? 0.0 : $totals['physical_storage']-$totals['controlled_virtual']-$totals['external_virtual'];
$external=$physicalOnly ? 0.0 : $totals['external_virtual']+$residual;
if ($base<0) $issues[]='negative_base_load';
if (abs($residual)>$c['splitToleranceW']) $issues[]='virtual_physical_allocation_mismatch';
$reconstructed=$base+$totals['flexible_load']+$totals['controlled_virtual']+$external-$totals['pv'];
$storage=$physicalOnly ? $totals['physical_storage'] : $totals['controlled_virtual']+$external;
$reconstructed=$base+$totals['flexible_load']+$storage-$totals['pv'];
$identity=hash('sha256',json_encode($c,JSON_THROW_ON_ERROR|JSON_PRESERVE_ZERO_FRACTION));
return ['version'=>1,'status'=>$issues===[]?'consistent_candidate':'quality_hold',
'capturedAt'=>gmdate('c',$now),'sourceObservedFrom'=>gmdate('c',min($times)),'sourceObservedUntil'=>gmdate('c',max($times)),