$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"; }