2,'VariableUpdated'=>$sensor[$id]['updated'],'VariableChanged'=>$sensor[$id]['changed']];} function IPS_GetObject($id):array {return ['ParentID'=>200,'ObjectIdent'=>'s'.$id];} function GetValue($id) {global $sensor;return $sensor[$id]['value'];} foreach(['CURLOPT_POST','CURLOPT_RETURNTRANSFER','CURLOPT_FOLLOWLOCATION','CURLOPT_CONNECTTIMEOUT','CURLOPT_TIMEOUT','CURLOPT_SSL_VERIFYPEER','CURLOPT_SSL_VERIFYHOST','CURLOPT_HTTPHEADER','CURLOPT_POSTFIELDS','CURLINFO_HTTP_CODE'] as $i=>$k)if(!defined($k))define($k,$i+100); function curl_init($url){$o=new \stdClass();$o->url=$url;return $o;} function curl_setopt_array($h,$options){$h->options=$options;return true;} function curl_exec($h){global $sent,$mode;$p=json_decode($h->options[CURLOPT_POSTFIELDS],true);$sent[]=$p; return json_encode(['status'=>'stored','datasetId'=>$p['datasetId'],'acceptedThrough'=>$mode==='wrong_ack'?'2000-01-01T00:00:00+00:00':end($p['records'])['capturedAt']]);} function curl_getinfo($h,$info){global $mode;return $mode==='429'?429:200;} function curl_close($h):void {} class Lizenzpruefung {const NETZFAHRPLAN='test-only';} class FakeManager { use ManagerNetzfahrplanV4DatenTrait; public int $InstanceID=17004;public array $p=[],$a=[],$vars=[],$timers=[];public bool $licensed=true; public function boot():void {$this->registriereNetzfahrplanV4Daten();} public function configure():void {$this->konfiguriereNetzfahrplanV4Daten();} public function folder():string {return $this->v4DatenVerzeichnis();} public function send():array {return $this->v4DatenUebertragen($this->folder());} public function RegisterPropertyBoolean($k,$v){$this->p[$k]??=$v;} public function RegisterPropertyString($k,$v){$this->p[$k]??=$v;} public function RegisterAttributeString($k,$v){$this->a[$k]??=$v;} public function RegisterAttributeInteger($k,$v){$this->a[$k]??=$v;} public function RegisterVariableString($k,...$args){$this->vars[$k]='';} public function RegisterTimer($k,$v,$code){$this->timers[$k]=$v;} public function SetTimerInterval($k,$v){$this->timers[$k]=$v;} public function ReadPropertyBoolean($k){return $this->p[$k];} public function ReadPropertyString($k){return $this->p[$k];} public function ReadAttributeString($k){return $this->a[$k];} public function ReadAttributeInteger($k){return $this->a[$k];} public function WriteAttributeString($k,$v){$this->a[$k]=$v;} public function WriteAttributeInteger($k,$v){$this->a[$k]=$v;} public function SetValue($k,$v){if($k!=='NetzfahrplanV4Datenstatus')throw new \RuntimeException('Unexpected variable write');$this->vars[$k]=$v;} private function berechtigungLizenziert($k){return $this->licensed;} } $c=['version'=>1,'installationId'=>'e3a08f9e-af12-4695-99bd-8b51c0520021','managerId'=>17004, 'reportedInventorySha256'=>str_repeat('a',64),'accounting'=>['version'=>1,'maxSkewSeconds'=>30,'splitToleranceW'=>100.,'sources'=>[]],'extraSources'=>[]]; foreach(['grid','pv','physical_storage','controlled_virtual','external_virtual'] as $i=>$role){$id=101+$i; $c['accounting']['sources'][]=['key'=>'s'.$id,'role'=>$role,'variableId'=>$id,'parentId'=>200,'ident'=>'s'.$id,'factorToW'=>1,'maxAgeSeconds'=>60,'dependsOn'=>[]]; $sensor[$id]=['value'=>[3000,5000,2000,2000,0][$i],'updated'=>$clock,'changed'=>$clock];} $m=new FakeManager();$m->boot();$m->a['LizenzInstallationID']=$c['installationId'];$m->a['PrognoseInstallationsToken']='synthetic-not-a-real-token'; try { $m->ErfasseNetzfahrplanV4Daten();check($sent===[],'disabled by default makes no network request'); $m->p['NetzfahrplanV4MessdatenAktiv']=true;$m->p['NetzfahrplanV4Datensatz']='physical-v1';$m->p['NetzfahrplanV4Messkonfiguration']=json_encode($c); $m->configure();check($m->timers['NetzfahrplanV4DatenErfassen']===30000,'only application acquisition timer enabled'); $m->ErfasseNetzfahrplanV4Daten();check(count($sent)===1,'native capture delivered to existing device API'); $cursor=$m->a['NetzfahrplanV4DatenCursor'];check(json_decode($cursor,true)['offset']>0,'matching ack commits durable byte cursor'); check($sent[0]['records'][0]['raw']['s101']['value']===3000,'physical reading retained'); check($sent[0]['records'][0]['raw']['s101']['sourceUpdatedAt']===$clock,'original timestamp retained'); check(!str_contains(json_encode($sent),'synthetic-not-a-real-token'),'token never included in data payload'); check($sent[0]['records'][0]['controlEligible']===false,'data delivery is not actuator permission'); $expected=hash('sha256',json_encode(NetzfahrplanV4Messaufnahme::configuration($c),JSON_THROW_ON_ERROR|JSON_PRESERVE_ZERO_FRACTION)); check($sent[0]['records'][0]['mappingSha256']===$expected,'property roundtrip preserves approved mapping identity'); check(json_decode($m->a['NetzfahrplanV4LetzteDatenquittung'],true)['acceptedThrough']===end($sent[0]['records'])['capturedAt'],'successful acknowledgement persisted independently'); $clock+=30;$m->ErfasseNetzfahrplanV4Daten();check(count($sent)===1,'30-second collection does not double request rate'); check($m->a['NetzfahrplanV4DatenCursor']===$cursor,'unsent rows remain behind cursor'); check(isset(json_decode($m->vars['NetzfahrplanV4Datenstatus'],true)['lastAcknowledgement']['acceptedThrough']),'scheduled status retains last successful receipt'); $clock+=30;$mode='429';$m->ErfasseNetzfahrplanV4Daten();check($m->a['NetzfahrplanV4DatenCursor']===$cursor,'429 never advances acknowledgement cursor'); check(json_decode($m->vars['NetzfahrplanV4Datenstatus'],true)['delivery']['dataRetained'],'offline data retained'); check(json_decode($m->vars['NetzfahrplanV4Datenstatus'],true)['delivery']['httpStatus']===429,'safe HTTP error code visible without credentials'); $mode='wrong_ack';$clock+=120;$m->ErfasseNetzfahrplanV4Daten();check($m->a['NetzfahrplanV4DatenCursor']===$cursor,'wrong ack timestamp rejected'); $mode='ok';$clock+=240;$m->ErfasseNetzfahrplanV4Daten();check($m->a['NetzfahrplanV4DatenCursor']!==$cursor,'later success sends the preserved backlog'); check(count(end($sent)['records'])===4,'all previously unacknowledged rows included once'); $file=glob($m->folder().'/raw-*.jsonl')[0];$count=count(file($file));check($count===5,'raw journal never cleared by upload'); $lastCursor=$m->a['NetzfahrplanV4DatenCursor'];file_put_contents($file,'incomplete',FILE_APPEND); check($m->send()['status']==='up_to_date','in-progress tail not transmitted');check($m->a['NetzfahrplanV4DatenCursor']===$lastCursor,'partial tail not acknowledged'); // Only temporary test journal: turn the prior incomplete tail into an additional complete queued record. $buf=file_get_contents($file);$buf=substr($buf,0,-strlen('incomplete')); $extra=$sent[0]['records'][0];$clock+=60;$extra['capturedAt']=gmdate('c',$clock);$extra['captureStartedAt']=$extra['capturedAt']; file_put_contents($file,$buf.json_encode($extra)."\n".'crash-tail'); $clock+=60;$beforeCount=count($sent);$m->ErfasseNetzfahrplanV4Daten(); $err=json_decode($m->vars['NetzfahrplanV4Datenstatus'],true); check($err['status']==='capture_error'&&$err['delivery']['status']==='acknowledged','capture failure does not deadlock pending delivery'); check(count($sent)===$beforeCount+1&&str_ends_with(file_get_contents($file),'crash-tail'),'complete prefix sent while original incomplete tail retained'); $m->p['NetzfahrplanV4MessdatenAktiv']=false;$m->configure();check($m->timers['NetzfahrplanV4DatenErfassen']===0,'disable affects acquisition only'); check(count($m->vars)===1,'only own manager data-status variable written'); $text=file_get_contents(__DIR__.'/../../libs/ManagerNetzfahrplanV4DatenTrait.php'); check(!preg_match('/\b(?:RequestAction\(|IPS_ApplyChanges\(|MC_Reload\(|schreibeRegister\()/',$text),'data path contains no actuator/module reload calls'); echo "TOTAL $checks application-data checks passed; mocked IPS and HTTP only.\n"; } finally { foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base,\FilesystemIterator::SKIP_DOTS),\RecursiveIteratorIterator::CHILD_FIRST)as$p){$p->isDir()&&!$p->isLink()?rmdir($p->getPathname()):unlink($p->getPathname());}rmdir($base); }