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
+27 -18
View File
@@ -35,10 +35,11 @@ namespace Belevo\EnelixEMS {
public array $op=[];
public function __construct($id,$op) {
$this->op=$op;
$this->props=['NetzfahrplanV4EmpfangAktiv'=>false,'NetzfahrplanV4SchattenAktiv'=>true,
$this->props=['PrognoseAktiv'=>false,'NetzfahrplanRegelungAktiv'=>false,'NetzfahrplanV4SchattenAktiv'=>true,
'NetzfahrplanAktiv'=>false,'MesswertMaxAlter'=>60,
'AnlagenBatterien'=>json_encode([['ID'=>'b','LeistungVariableID'=>100,'Leistungsfaktor'=>1]])];
$this->attrs=['LizenzInstallationID'=>$id,'PrognoseInstallationsToken'=>str_repeat('x',43)];
$this->attrs=['LizenzInstallationID'=>$id,'PrognoseInstallationsToken'=>str_repeat('x',43),
'PrognoseV4Serverstatus'=>'{}'];
$this->registriereNetzfahrplanV4Empfang();
}
public function receive() {$this->empfangeNetzfahrplanV4();}
@@ -47,6 +48,7 @@ namespace Belevo\EnelixEMS {
public function configure() {$this->konfiguriereNetzfahrplanV4Vorschau();}
public function GetNetzfahrplanV4Diagnose() { return json_encode(['operation'=>$this->op]); }
private function aktiveVerbraucherIDs() {return [];}
private function prognoseLernenAktiv() {return $this->props['PrognoseAktiv'];}
private function v4ManagerTestSession() {return $this->session;}
private function berechtigungLizenziert($key) {return $this->licensed;}
private function ReadPropertyBoolean($key) {return (bool)$this->props[$key];}
@@ -55,7 +57,7 @@ namespace Belevo\EnelixEMS {
private function ReadAttributeString($key) {return $this->attrs[$key]??'';}
private function ReadAttributeInteger($key) {return (int)($this->attrs[$key]??0);}
private function WriteAttributeString($key,$val) {
if (!str_starts_with($key,'NetzfahrplanV4')) throw new \RuntimeException('Unexpected external attribute');
if (!str_starts_with($key,'NetzfahrplanV4') && $key!=='PrognoseV4Serverstatus') throw new \RuntimeException('Unexpected external attribute');
$this->attrs[$key]=$val;
}
private function WriteAttributeInteger($key,$val) { $this->attrs[$key]=$val; }
@@ -86,8 +88,9 @@ namespace {
$GLOBALS['clock']+=$seconds;$GLOBALS['response']['checkedAt']=gmdate('c',$GLOBALS['clock']);
$h->op['observedAt']=gmdate('c',$GLOBALS['clock']);$h->op['batteries'][0]['measuredAt']=$h->op['observedAt'];
}
$h=fixture();$h->receive();check(count($requests)===0,'default disabled no network');
$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->configure();check($h->timers['NetzfahrplanV4VorschauAktualisieren']===10000,'watchdog without network');
$h=fixture();$h->receive();check(count($requests)===0,'learning disabled makes no network request');
$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->receive();check(count($requests)===0,'retired receiver flag cannot enable learning or network');
$h->props['PrognoseAktiv']=true;$h->configure();check($h->timers['NetzfahrplanV4VorschauAktualisieren']===10000,'watchdog without network');
$h->receive();$v=json_decode($h->GetNetzfahrplanV4Vorschau(),true);
check($v['status']==='preview_only' && $v['previewBatteryW']===3000 && $v['canDispatch']===false,'read and show only');
check(count($requests)===2 && str_ends_with($requests[1]['url'],'/ack'),'one read and one acknowledgement');
@@ -97,25 +100,25 @@ namespace {
advance($h,91);$h->update();$v=json_decode($h->vars['NetzfahrplanV4VorschauJSON'],true);
check($v['status']==='unavailable' && $v['previewBatteryW']===null,'expired cache clears numeric targets, not fake zero');
check(!str_contains($h->vars['NetzfahrplanV4Vorschau'],'3000'),'expired HTML clears previous setpoint');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->receive();advance($h,61);$GLOBALS['getCode']=429;$h->receive();
$h=fixture();$h->props['PrognoseAktiv']=true;$h->receive();advance($h,61);$GLOBALS['getCode']=429;$h->receive();
check($h->attrs['NetzfahrplanV4Empfang']==='{}' && json_decode($h->GetNetzfahrplanV4Vorschau(),true)['previewBatteryW']===null,'429 invalidates cache');
check(!str_contains(json_encode($h->vars),str_repeat('x',43)),'no credentials in diagnosis');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->props['NetzfahrplanAktiv']=true;$h->receive();check(count($requests)===0,'legacy execution on blocks preview acquisition');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$GLOBALS['response']['installationId']='00000000-0000-4000-8000-000000000001';$h->receive();
$h=fixture();$h->props['PrognoseAktiv']=true;$h->props['NetzfahrplanAktiv']=true;$h->receive();check(count($requests)===2 && $h->props['NetzfahrplanRegelungAktiv']===false,'obsolete legacy flag neither blocks preview nor enables V4 control');
$h=fixture();$h->props['PrognoseAktiv']=true;$GLOBALS['response']['installationId']='00000000-0000-4000-8000-000000000001';$h->receive();
check(count($requests)===1 && $h->attrs['NetzfahrplanV4Empfang']==='{}','foreign response not acknowledged');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$GLOBALS['ackCode']=503;$h->receive();$v=json_decode($h->GetNetzfahrplanV4Vorschau(),true);
$h=fixture();$h->props['PrognoseAktiv']=true;$GLOBALS['ackCode']=503;$h->receive();$v=json_decode($h->GetNetzfahrplanV4Vorschau(),true);
check($v['status']==='preview_only' && !$v['reception']['acknowledged'] && isset($v['reception']['acknowledgementError']),'ack failure is visible, no false execution confirmation');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->receive();$h->op['batteries'][0]['minSocPercent']=30.;$h->update();
$h=fixture();$h->props['PrognoseAktiv']=true;$h->receive();$h->op['batteries'][0]['minSocPercent']=30.;$h->update();
check(json_decode($h->vars['NetzfahrplanV4VorschauJSON'],true)['previewBatteryW']===null,'local policy change invalidates cache');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->receive();$h->stop();check($h->attrs['NetzfahrplanV4Empfang']==='{}' && $h->timers['NetzfahrplanV4VorschauAktualisieren']===0,'apply/restart invalidates cached plan');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->licensed=false;$h->receive();check(count($requests)===0,'no license no read');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$GLOBALS['lock']=true;$h->receive();check(count($requests)===0,'concurrent receiver does not send');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->receive();$h->props['NetzfahrplanV4EmpfangAktiv']=false;$h->update();check(json_decode($h->vars['NetzfahrplanV4VorschauJSON'],true)['previewBatteryW']===null,'disable clears displayed target');
$h=fixture();$h->props['PrognoseAktiv']=true;$h->receive();$h->stop();check($h->attrs['NetzfahrplanV4Empfang']==='{}' && $h->timers['NetzfahrplanV4VorschauAktualisieren']===0,'apply/restart invalidates cached plan');
$h=fixture();$h->props['PrognoseAktiv']=true;$h->licensed=false;$h->receive();check(count($requests)===0,'no license no read');
$h=fixture();$h->props['PrognoseAktiv']=true;$GLOBALS['lock']=true;$h->receive();check(count($requests)===0,'concurrent receiver does not send');
$h=fixture();$h->props['PrognoseAktiv']=true;$h->receive();$h->props['PrognoseAktiv']=false;$h->update();check(json_decode($h->vars['NetzfahrplanV4VorschauJSON'],true)['previewBatteryW']===null,'disable clears displayed target');
$source=file_get_contents(__DIR__.'/../../libs/ManagerNetzfahrplanV4EmpfangTrait.php');
check(!str_contains($source, 'netzfahrplanV4EmpfangFormular'), 'receiver no longer owns a separate form panel');
check(!preg_match('/\b(?:SendDataToChildren|SendDataToParent|sendeManagerdaten|regeln|SetStatus|EnableAction|IPS_SetProperty|IPS_ApplyChanges)\s*\(/',$source),'no control or external configuration call in receiver');
check(!str_contains($source,"'status' => 'applied'"),'never acknowledges applied');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->configure();
$h=fixture();$h->props['PrognoseAktiv']=true;$h->configure();
check($h->timers['NetzfahrplanV4EmpfangAktualisieren']===10000,'refresh has an independent timer');
$h->session=['kind'=>'local_active_test_consent'];$h->receive();$saved=$h->attrs['NetzfahrplanV4Empfang'];
check($h->attrs['NetzfahrplanV4NaechsterAbruf']===$clock+45,'active refresh due after 45 seconds');
@@ -125,16 +128,22 @@ namespace {
advance($h,46);$h->receive();
check($h->attrs['NetzfahrplanV4Empfang']==='{}','active envelope older than 90 seconds is discarded');
$h->stop();check($h->timers['NetzfahrplanV4EmpfangAktualisieren']===0,'stop disables independent refresh timer');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$h->session=['kind'=>'local_active_test_consent'];$h->receive();
$h=fixture();$h->props['PrognoseAktiv']=true;$h->session=['kind'=>'local_active_test_consent'];$h->receive();
$future=json_decode($h->attrs['NetzfahrplanV4Empfang'],true);$future['receivedAt']=$clock+120;
$h->attrs['NetzfahrplanV4Empfang']=json_encode($future);advance($h,45);$GLOBALS['getCode']=503;$h->receive();
check($h->attrs['NetzfahrplanV4Empfang']==='{}','future receipt cannot survive failed refresh');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$GLOBALS['response']['pending']=['sequence'=>1];$GLOBALS['response']['fresh']=false;$h->receive();
$h=fixture();$h->props['PrognoseAktiv']=true;$GLOBALS['response']['pending']=['sequence'=>1];$GLOBALS['response']['fresh']=false;$h->receive();
check($h->attrs['NetzfahrplanV4NaechsterAbruf']===$clock+15 && $h->attrs['NetzfahrplanV4Empfang']==='{}',
'pending startup retries after 15 seconds without creating a usable envelope');
$v=json_decode($h->GetNetzfahrplanV4Vorschau(),true);
check(($v['reception']['status']??null)==='rejected_or_unavailable','unavailable preview preserves receiver rejection detail');
$h=fixture();$h->props['NetzfahrplanV4EmpfangAktiv']=true;$GLOBALS['getCode']=429;$h->receive();
$h=fixture();$h->props['PrognoseAktiv']=true;$GLOBALS['getCode']=429;$h->receive();
check($h->attrs['NetzfahrplanV4NaechsterAbruf']===$clock+60,'startup rate-limit response retains ordinary backoff');
$h=fixture();$h->props['PrognoseAktiv']=true;$h->props['NetzfahrplanRegelungAktiv']=true;$h->receive();
check($h->attrs['NetzfahrplanV4NaechsterAbruf']===$clock+45,'normal control switch uses active refresh without a test session');
$h=fixture();$h->props['PrognoseAktiv']=true;
$GLOBALS['response']['workflow']=['state'=>'ready','planningReady'=>true,'controlActive'=>false];$h->receive();
$workflow=json_decode($h->attrs['PrognoseV4Serverstatus'],true);
check($workflow['receivedAt']===$clock && $workflow['workflow']===$GLOBALS['response']['workflow'],'server workflow cached separately without inferring control activity');
echo "Total: $count checks. Mocked IPS and HTTP only.\n";
}