feat(application): integrate measured-load ingestion training and planner source

This commit is contained in:
ENELIX Agent
2026-10-02 21:00:05 +00:00
parent 44362e4bf5
commit fc024fcfba
96 changed files with 8933 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use PHPUnit\Framework\TestCase;
final class NetzfahrplanV4DatenTest extends TestCase
{
public function testApplicationDataPathWithIsolatedRuntime(): void
{
$pipes=[];
$process=proc_open([PHP_BINARY,__DIR__.'/V4ApplicationData/checks.php'],
[0=>['pipe','r'],1=>['pipe','w'],2=>['pipe','w']],$pipes);
self::assertIsResource($process);fclose($pipes[0]);
$out=stream_get_contents($pipes[1]);$err=stream_get_contents($pipes[2]);
fclose($pipes[1]);fclose($pipes[2]);
self::assertSame(0,proc_close($process),$out.$err);
self::assertStringContainsString('TOTAL 21 application-data checks passed',$out);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
require_once __DIR__.'/../../libs/ManagerNetzfahrplanV4DatenTrait.php';
$checks=0;$clock=1791028800;$sent=[];$mode='ok';$sensor=[];$locks=[];
$base=sys_get_temp_dir().'/v4-app-data-'.bin2hex(random_bytes(5));mkdir($base,0700);
function check(bool $v,string $name):void {global $checks;if(!$v)throw new \RuntimeException('FAIL '.$name);$checks++;echo 'PASS '.$name."\n";}
function time():int {global $clock;return $clock;}
function IPS_GetKernelDir():string {global $base;return $base.'/';}
function IPS_SemaphoreEnter($key,$timeout):bool {global $locks;if($locks[$key]??false)return false;$locks[$key]=true;return true;}
function IPS_SemaphoreLeave($key):void {global $locks;$locks[$key]=false;}
function IPS_VariableExists($id):bool {global $sensor;return isset($sensor[$id]);}
function IPS_GetVariable($id):array {global $sensor;return ['VariableType'=>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');
$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');
$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');
$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');
$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);
}