['ModuleID'=>'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}']];} function IPS_GetChildrenIDs($parent){return array_keys(array_filter($GLOBALS['objects'],fn($o)=>$o['ParentID']===$parent));} function IPS_GetObject($id){return $GLOBALS['objects'][$id];} function IPS_GetVariable($id){return $GLOBALS['variables'][$id];} function IPS_VariableExists($id){return isset($GLOBALS['variables'][$id]);} function GetValue($id){return $GLOBALS['values'][$id];} function SetValue($id,$v){if($id<50000||!is_string($v))throw new RuntimeException('FORBIDDEN source/actuator write');$GLOBALS['values'][$id]=$v;return true;} function IPS_SemaphoreEnter($name,$wait){if($GLOBALS['locks'][$name]??false)return false;$GLOBALS['locks'][$name]=true;return true;} function IPS_SemaphoreLeave($name){$GLOBALS['locks'][$name]=false;} function makeObject($type){$id=++$GLOBALS['next'];$GLOBALS['objects'][$id]=['ParentID'=>0,'ObjectIdent'=>'','ObjectType'=>$type,'ObjectInfo'=>''];return $id;} function IPS_CreateCategory(){return makeObject(0);} function IPS_CreateVariable($type){ck($type===3,'only new textual observation variables');$id=makeObject(2);$GLOBALS['variables'][$id]=['VariableType'=>3];$GLOBALS['values'][$id]='';return $id;} function IPS_CreateScript($type){if($type!==0)throw new RuntimeException('script type');$id=makeObject(3);$GLOBALS['scripts'][$id]='';return $id;} function changeObject($id,$key,$v){if($id<50000)throw new RuntimeException('FORBIDDEN existing object change');$GLOBALS['objects'][$id][$key]=$v;return true;} function IPS_SetParent($id,$v){return changeObject($id,'ParentID',$v);} function IPS_SetIdent($id,$v){return changeObject($id,'ObjectIdent',$v);} function IPS_SetName($id,$v){return changeObject($id,'ObjectName',$v);} function IPS_SetInfo($id,$v){return changeObject($id,'ObjectInfo',$v);} function IPS_GetScriptContent($id){return $GLOBALS['scripts'][$id];} function IPS_SetScriptContent($id,$s){if($id<50000)throw new RuntimeException('existing script');$GLOBALS['scripts'][$id]=$s;return true;} function IPS_SetScriptTimer($id,$seconds){if($id<50000||!isset($GLOBALS['scripts'][$id])||!in_array($seconds,[0,30],true))throw new RuntimeException('FORBIDDEN timer change');$GLOBALS['timers'][$id]=$seconds;return true;} [$c,$p,$r,$ignored]=separatedFixture();$now=time();$objects=[];$variables=[];$values=[];$scripts=[];$timers=[];$locks=[];$next=50000; foreach($r as$id=>$v){$objects[$id]=['ParentID'=>$v['parentID'],'ObjectIdent'=>$v['ident'],'ObjectType'=>2,'ObjectInfo'=>'original'];$variables[$id]=['VariableType'=>is_int($v['value'])?1:2,'VariableUpdated'=>$now,'VariableChanged'=>$now-30];$values[$id]=$v['value'];} $sourceValues=$values;$base=sys_get_temp_dir().'/v4views-test-'.bin2hex(random_bytes(6));mkdir($base,0700);mkdir($base.'/data',0700); $files=['source/libs/NetzfahrplanV4Bilanzierung.php','source/libs/NetzfahrplanV4Messaufnahme.php','source/libs/NetzfahrplanV4Messsicht.php','source/examples/V4SeparatedObservation/runtime.php','source/examples/V4SeparatedObservation/install.php']; foreach($files as$name){$dest=$base.'/'.$name;if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);copy(__DIR__.'/../../'.substr($name,7),$dest);} file_put_contents($base.'/INVENTORY.json','{"effectiveEvCapacityKwh":161.44,"evPowerKw":39}');$c['reportedInventorySha256']=hash_file('sha256',$base.'/INVENTORY.json'); file_put_contents($base.'/config.json',json_encode($c,JSON_THROW_ON_ERROR|JSON_PRESERVE_ZERO_FRACTION));file_put_contents($base.'/policy.json',json_encode($p,JSON_THROW_ON_ERROR|JSON_PRESERVE_ZERO_FRACTION)); $manifest=['scope'=>'passive_separated_observation','hashes'=>[]];foreach(array_merge($files,['config.json','policy.json','INVENTORY.json'])as$name)$manifest['hashes'][$name]=hash_file('sha256',$base.'/'.$name);file_put_contents($base.'/MANIFEST.json',json_encode($manifest)); require_once $base.'/source/examples/V4SeparatedObservation/install.php'; try{ ob_start();$report=v4viewsInstall($base);ob_end_clean();ck($report['status']==='observer_installed','native installation completes');$cat=$report['categoryId']; ck(count($report['createdObjects'])===6,'one new category three read-only variables two scripts'); ck($timers===[$report['scriptId']=>30],'only new passive observation timer'); ck($report['firstObservation']['status']==='recorded','actual first record precedes timer'); $journal=$report['firstObservation']['journal'];$original=file_get_contents($journal); ck((fileperms($journal)&0777)===0644&&(fileperms(dirname($journal))&0077)===0,'selected new output readable inside private directory'); foreach($sourceValues as$id=>$value)ck($values[$id]===$value,'source unchanged '.$id); ob_start();$again=v4viewsInstall($base);ob_end_clean();ck($again['createdObjects']===[],'idempotent no duplicate scripts or variables'); ck(str_starts_with(file_get_contents($journal),$original),'repeat preserves historical observations'); $before=file_get_contents($journal);$locks['ENELIX.V4.SeparatedView.'.$c['installationId']]=true;$busy=v4viewsRun($base,$cat);ck($busy['status']==='busy'&&file_get_contents($journal)===$before,'parallel observer does not duplicate writes');$locks=[]; unset($variables[101]);$failed=v4viewsRun($base,$cat);ck($failed['status']==='recorded','missing measurement explicitly recorded'); $latest=json_decode(file_get_contents($base.'/data/LATEST.json'),true);ck($latest['views']['physicalLoad']['loadCandidateW']===null&&!$latest['controlEnabled'],'bad source never old target or fake zero');$variables[101]=['VariableType'=>1,'VariableUpdated'=>$now,'VariableChanged'=>$now-30]; $before=file_get_contents($journal);v4viewsStop($base,$cat);ck($timers[$report['scriptId']]===0&&file_get_contents($journal)===$before,'stop only observer no deletion'); ck(json_decode(file_get_contents($base.'/data/LATEST.json'),true)['status']==='stopped','stopped view clears latest numbers'); $scripts[$report['scriptId']].='// parallel edit';ob_start();$bad=v4viewsInstall($base);ob_end_clean();ck($bad['status']==='needs_review'&&$timers[$report['scriptId']]===0,'parallel script edit not overwritten or activated'); $old=file_get_contents($base.'/config.json');file_put_contents($base.'/config.json',$old.' ');try{v4viewsConfig($base);$refused=false;}catch(Throwable $e){$refused=true;}ck($refused,'changed config refused');file_put_contents($base.'/config.json',$old); chmod($base,0755);clearstatcache();try{v4viewsConfig($base);$refused=false;}catch(Throwable $e){$refused=true;}ck($refused,'public stage refused before reading output');chmod($base,0700); foreach(['runtime.php','install.php']as$file){$text=file_get_contents($base.'/source/examples/V4SeparatedObservation/'.$file);ck(!preg_match('/\b(?:RequestAction|IPS_ApplyChanges|IPS_SetProperty|IPS_SetConfiguration|MC_Reload|curl_exec|shell_exec)\s*\(/',$text),'no actuation or configuration calls in '.$file);} }finally{ foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST)as$f){$f->isDir()&&!$f->isLink()?rmdir($f->getPathname()):unlink($f->getPathname());}rmdir($base); } echo "TOTAL $n native-mock checks passed. No actual Symcon calls.\n";