feat(manager): integrate forecast controls and merge V4 development
Tests / test (push) Failing after 49s
Tests / test (push) Failing after 49s
Merge the V4 development history with current manager, SDL and setpoint fixes. Retain guarded trial behavior and consolidate controls in Prognose / Forecast. Exclude the unverified accounting-evidence change; no runtime deployment or new dispatch permission. Validated: 357 PHPUnit tests / 1758 assertions, 150 PHP syntax checks, 310 backend tests, 16 portal tests and isolated UI/receiver checks. develop and beta publication explicitly approved by Daniel Haefliger.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
namespace Belevo\EnelixEMS {
|
||||
require_once __DIR__ . '/../../libs/ManagerNetzfahrplanV4EmpfangTrait.php';
|
||||
class Lizenzpruefung { public const NETZFAHRPLAN = 'grid_schedule'; }
|
||||
foreach (['CURLOPT_CUSTOMREQUEST','CURLOPT_FOLLOWLOCATION','CURLOPT_CONNECTTIMEOUT','CURLOPT_TIMEOUT',
|
||||
'CURLOPT_SSL_VERIFYPEER','CURLOPT_SSL_VERIFYHOST','CURLOPT_PROTOCOLS','CURLOPT_HTTPHEADER',
|
||||
'CURLOPT_WRITEFUNCTION','CURLOPT_POSTFIELDS','CURLINFO_HTTP_CODE','CURLPROTO_HTTPS'] as $i=>$name) {
|
||||
if (!defined($name)) define($name,50000+$i);
|
||||
}
|
||||
function time() { return $GLOBALS['clock']; }
|
||||
function IPS_SemaphoreEnter($name,$wait) { if ($GLOBALS['lock']) return false;$GLOBALS['lock']=true;return true; }
|
||||
function IPS_SemaphoreLeave($name) { $GLOBALS['lock']=false; }
|
||||
function IPS_VariableExists($id) {return $id===100;}
|
||||
function IPS_GetVariable($id) {return ['VariableUpdated'=>$GLOBALS['clock']];}
|
||||
function GetValue($id) {return $GLOBALS['actual'];}
|
||||
function curl_init($url) { return (object)['url'=>$url,'options'=>[],'code'=>0]; }
|
||||
function curl_setopt_array($h,$options) { $h->options=$options;return true; }
|
||||
function curl_getinfo($h,$key) { return $h->code; }
|
||||
function curl_close($h) {}
|
||||
function curl_exec($h) {
|
||||
$GLOBALS['requests'][]=['url'=>$h->url,'options'=>$h->options];
|
||||
$ack=str_ends_with($h->url,'/ack');
|
||||
$h->code=$ack?$GLOBALS['ackCode']:$GLOBALS['getCode'];
|
||||
$body=$ack?['status'=>'recorded']:$GLOBALS['response'];
|
||||
$json=json_encode($body,JSON_THROW_ON_ERROR);
|
||||
$callback=$h->options[CURLOPT_WRITEFUNCTION];
|
||||
return $callback($h,$json)===strlen($json);
|
||||
}
|
||||
final class ReceiverHarness {
|
||||
use ManagerNetzfahrplanV4EmpfangTrait;
|
||||
public int $InstanceID=17004;
|
||||
public array $props=[],$attrs=[],$vars=[],$timers=[];
|
||||
public bool $licensed=true;
|
||||
public array $session=[];
|
||||
public array $op=[];
|
||||
public function __construct($id,$op) {
|
||||
$this->op=$op;
|
||||
$this->props=['NetzfahrplanV4EmpfangAktiv'=>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->registriereNetzfahrplanV4Empfang();
|
||||
}
|
||||
public function receive() {$this->empfangeNetzfahrplanV4();}
|
||||
public function update() {$this->aktualisiereNetzfahrplanV4Vorschau();}
|
||||
public function stop() {$this->stoppeNetzfahrplanV4Vorschau();}
|
||||
public function configure() {$this->konfiguriereNetzfahrplanV4Vorschau();}
|
||||
public function GetNetzfahrplanV4Diagnose() { return json_encode(['operation'=>$this->op]); }
|
||||
private function aktiveVerbraucherIDs() {return [];}
|
||||
private function v4ManagerTestSession() {return $this->session;}
|
||||
private function berechtigungLizenziert($key) {return $this->licensed;}
|
||||
private function ReadPropertyBoolean($key) {return (bool)$this->props[$key];}
|
||||
private function ReadPropertyString($key) {return $this->props[$key];}
|
||||
private function ReadPropertyInteger($key) {return (int)$this->props[$key];}
|
||||
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');
|
||||
$this->attrs[$key]=$val;
|
||||
}
|
||||
private function WriteAttributeInteger($key,$val) { $this->attrs[$key]=$val; }
|
||||
private function RegisterPropertyBoolean($key,$default) {$this->props[$key]??=$default;}
|
||||
private function RegisterAttributeString($key,$default) {$this->attrs[$key]??=$default;}
|
||||
private function RegisterAttributeInteger($key,$default) {$this->attrs[$key]??=$default;}
|
||||
private function RegisterVariableString($key,$name,$profile,$pos) {$this->vars[$key]='';}
|
||||
private function RegisterTimer($key,$interval,$script) {$this->timers[$key]=$interval;}
|
||||
private function SetTimerInterval($key,$interval) {$this->timers[$key]=$interval;}
|
||||
private function SetValue($key,$value) {
|
||||
if (!in_array($key,['NetzfahrplanV4Vorschau','NetzfahrplanV4VorschauJSON'],true)) throw new \RuntimeException('Actuator/foreign variable write');
|
||||
$this->vars[$key]=$value;
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace {
|
||||
require_once __DIR__.'/fixture.php';
|
||||
use Belevo\EnelixEMS\ReceiverHarness;
|
||||
$count=0;
|
||||
function check($ok,$name) {global $count;if (!$ok) throw new RuntimeException('FAIL '.$name);$count++;echo 'PASS '.$name."\n";}
|
||||
function fixture() {
|
||||
[$r,$id,$op,$now]=receiverFixture();
|
||||
$GLOBALS['response']=$r;$GLOBALS['clock']=$now;$GLOBALS['actual']=0.;$GLOBALS['requests']=[];
|
||||
$GLOBALS['lock']=false;$GLOBALS['getCode']=200;$GLOBALS['ackCode']=200;
|
||||
return new ReceiverHarness($id,$op);
|
||||
}
|
||||
function advance($h,$seconds) {
|
||||
$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->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');
|
||||
$body=json_decode($requests[1]['options'][CURLOPT_POSTFIELDS],true);check($body['status']==='shadow_seen' && $body['step']===$response['plan']['points'][0]['time'],'ack is shadow_seen, never applied');
|
||||
$h->receive();check(count($requests)===2,'repeated click throttled');
|
||||
advance($h,60);$h->receive();check(count($requests)===3,'same plan step not acknowledged twice');
|
||||
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();
|
||||
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();
|
||||
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);
|
||||
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();
|
||||
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');
|
||||
$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();
|
||||
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');
|
||||
advance($h,45);$GLOBALS['getCode']=503;$h->receive();
|
||||
check($h->attrs['NetzfahrplanV4Empfang']===$saved,'temporary error retains exact original active envelope');
|
||||
check($h->attrs['NetzfahrplanV4NaechsterAbruf']===$clock+15,'temporary error retries after 15 seconds');
|
||||
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();
|
||||
$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();
|
||||
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();
|
||||
check($h->attrs['NetzfahrplanV4NaechsterAbruf']===$clock+60,'startup rate-limit response retains ordinary backoff');
|
||||
echo "Total: $count checks. Mocked IPS and HTTP only.\n";
|
||||
}
|
||||
Reference in New Issue
Block a user