feat(v4): confirm feedback with device reads
This commit is contained in:
@@ -19,6 +19,7 @@ final class NetzfahrplanV4RueckmeldungTest extends TestCase
|
||||
self::assertSame('',$err);self::assertStringContainsString('TOTAL ',$out);
|
||||
}
|
||||
public function testPhysicalFeedback(): void {$this->checkScript('checks.php');}
|
||||
public function testConfirmedDeviceReads(): void {$this->checkScript('device_read_checks.php');}
|
||||
public function testActualReceiverIntegration(): void {$this->checkScript('receiver_checks.php');}
|
||||
public function testActualFallbackWrapper(): void {$this->checkScript('fallback_checks.php');}
|
||||
public function testMessageWithoutVisibleDiagnostics(): void {$this->checkScript('message_checks.php');}
|
||||
|
||||
@@ -10,9 +10,13 @@ namespace Belevo\EnelixEMS {
|
||||
function hrtime($asNumber=true) { return (int)($GLOBALS['mono']*1e9); }
|
||||
function IPS_SemaphoreEnter($key,$wait) { if (!empty($GLOBALS['locks'][$key])) return false;$GLOBALS['locks'][$key]=true;return true; }
|
||||
function IPS_SemaphoreLeave($key) { unset($GLOBALS['locks'][$key]); }
|
||||
function IPS_InstanceExists($id) { return isset($GLOBALS['objects'][$id]); }
|
||||
function IPS_GetInstance($id) { return ['InstanceStatus'=>$GLOBALS['objects'][$id]->ready?102:201,
|
||||
'ModuleInfo'=>['ModuleID'=>$id===44234?'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}':'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}']]; }
|
||||
function IPS_InstanceExists($id) { return $id===200 || isset($GLOBALS['objects'][$id]); }
|
||||
function IPS_GetInstance($id) {
|
||||
if ($id===200) return ['InstanceStatus'=>102,'ModuleInfo'=>['ModuleID'=>'{90BF9234-B44C-7EB3-280B-FBD6075ADDAA}']];
|
||||
return ['InstanceStatus'=>$GLOBALS['objects'][$id]->ready?102:201,
|
||||
'ModuleInfo'=>['ModuleID'=>$id===44234?'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}':'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}']];
|
||||
}
|
||||
function ModBus_RequestRead($id) { return $id===200 && ($GLOBALS['deviceReadOk']??true); }
|
||||
function IPS_GetProperty($id,$key) { return $GLOBALS['objects'][$id]->props[$key]; }
|
||||
function IPS_VariableExists($id) { return in_array($id,[50001,50002],true); }
|
||||
function IPS_GetVariable($id) { $t=$GLOBALS['feedbackAt'][$id]??$GLOBALS['clock'];return ['VariableUpdated'=>$t,'VariableChanged'=>$t]; }
|
||||
@@ -102,7 +106,7 @@ namespace {
|
||||
function check($ok,$name) { global $count;if (!$ok) throw new RuntimeException('FAIL '.$name);$count++;echo "PASS $name\n"; }
|
||||
function rejects(callable $call,$name) { try {$call();}catch(Throwable $e){check(true,$name);return;}throw new RuntimeException('FAIL did not reject '.$name); }
|
||||
function fixture($permissions=true,$serverAuthority=true): array {
|
||||
[$r,$id,$op,$now]=receiverFixture();$GLOBALS['clock']=$now;$GLOBALS['mono']=1000.;$GLOBALS['locks']=[];$GLOBALS['calls']=[];
|
||||
[$r,$id,$op,$now]=receiverFixture();$GLOBALS['clock']=$now;$GLOBALS['mono']=1000.;$GLOBALS['locks']=[];$GLOBALS['calls']=[];$GLOBALS['deviceReadOk']=true;
|
||||
$r['plan']['inputQuality']=['loadBasis'=>'base_load','accountingEvidenceId'=>'synthetic-meter-boundary-v1'];
|
||||
$r['plan']['peakCostIsEstimate']=true;
|
||||
$a=['kind'=>'controlled_trial_authority','version'=>1,'sourcePlanRemainsShadow'=>true,
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Run through actual Manager/Battery trial traits, fresh read helper, and simulated register driver.
|
||||
[$m,$b,$s]=fixture();$b->props['NetzfahrplanV4RueckmeldungKonfiguration']='{}';
|
||||
rejects(fn()=>start($m,$s),'trial cannot use old filtered feedback when mapping missing');check($b->writes===[],'missing corrected feedback sends no command');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$feedbackAt[50002]=$clock-61;
|
||||
$a=tick($m);check(!$m->active()&&!$b->active()&&isset($a['Sollwerte'][44234]),'physical feedback expiry returns battery to normal allocation');
|
||||
check(end($b->writes)===0,'expired physical feedback revokes prior command');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$feedbackAt[50002]=$clock-61;$b->watchdog();
|
||||
check(!$b->active()&&end($b->writes)===0,'independent battery read catches staleness without manager tick');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$deviceReadOk=false;
|
||||
$a=tick($m);check(!$m->active()&&!$b->active()&&isset($a['Sollwerte'][44234]),'failed confirmed read returns battery to normal allocation');
|
||||
check(end($b->writes)===0,'failed confirmed read revokes prior command');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$deviceReadOk=false;$b->watchdog();
|
||||
check(!$b->active()&&end($b->writes)===0,'independent battery read catches device failure without manager tick');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$c=json_decode($b->props['NetzfahrplanV4RueckmeldungKonfiguration'],true);$c['trackingToleranceW']=250;
|
||||
$b->props['NetzfahrplanV4RueckmeldungKonfiguration']=json_encode($c);$b->watchdog();
|
||||
check(!$b->active()&&end($b->writes)===0,'mapping revision change revokes active battery session');
|
||||
|
||||
@@ -11,7 +11,7 @@ function fx($mode='physical_sum') {
|
||||
$now=1791032400;$c=['version'=>1,'installationId'=>'e3a08f9e-af12-4695-99bd-8b51c0520021','assetId'=>'ev','managerId'=>17004,'batteryInstanceId'=>44234,
|
||||
'mode'=>$mode,'allowEstimatedForTrial'=>false,'maxSkewSeconds'=>30,'maxAgeSeconds'=>60,'idleToleranceW'=>50.0,'trackingToleranceW'=>200.0,'sources'=>[]];
|
||||
$roles=['grid','physical','physical'];if($mode==='virtual_split')$roles=array_merge($roles,['ev_request','sdl_request','gateway_active']);$values=[3000.0,-2000.0,-1000.0,-3000.0,0.0,true];$r=[];
|
||||
foreach($roles as$i=>$role){$id=100+$i;$c['sources'][]=['key'=>'s'.$id,'role'=>$role,'variableId'=>$id,'parentId'=>200,'ident'=>'q'.$id,'factorToW'=>1];$r[$id]=['value'=>$values[$i],'updated'=>$now,'changed'=>$now-60,'parentID'=>200,'ident'=>'q'.$id];}
|
||||
foreach($roles as$i=>$role){$id=100+$i;$c['sources'][]=['key'=>'s'.$id,'role'=>$role,'variableId'=>$id,'parentId'=>200,'ident'=>'q'.$id,'factorToW'=>1];$r[$id]=['value'=>$values[$i],'updated'=>$now,'changed'=>$now-60,'parentID'=>200,'ident'=>'q'.$id,'confirmedAt'=>$now];}
|
||||
return [$c,$r,$now];
|
||||
}
|
||||
function readit($c,$r,$n){return F::read($c,fn($id)=>$r[$id],$n);}
|
||||
@@ -23,8 +23,9 @@ check(F::fingerprint($c)===F::fingerprint(json_decode(json_encode($c),true)),'co
|
||||
$d=$c;$d['sources']=array_reverse($d['sources']);check(F::fingerprint($c)===F::fingerprint($d),'source list order canonical');
|
||||
$d['trackingToleranceW']=201;check(F::fingerprint($c)!==F::fingerprint($d),'changed tolerance invalidates session hash');
|
||||
foreach([true,'1',NAN,INF,null]as$bad){$x=$r;$x[101]['value']=$bad;fails(fn()=>readit($c,$x,$n),'invalid physical number '.gettype($bad));}
|
||||
$x=$r;$x[101]['updated']=$n-61;$x[101]['changed']=$n-90;fails(fn()=>readit($c,$x,$n),'physical age not refreshed by read');
|
||||
$x=$r;$x[101]['updated']=$n-31;fails(fn()=>readit($c,$x,$n),'incoherent grid battery timestamps rejected');
|
||||
$x=$r;$x[101]['updated']=$n-61;$x[101]['changed']=$n-90;$f=readit($c,$x,$n);check($f['sourceVariableOldestAt']===$n-61,'confirmed read does not manufacture VariableUpdated');
|
||||
$x=$r;unset($x[101]['confirmedAt']);fails(fn()=>readit($c,$x,$n),'physical source without confirmed device read rejected');
|
||||
$x=$r;$x[101]['confirmedAt']=$n-31;fails(fn()=>readit($c,$x,$n),'incoherent confirmed device timestamps rejected');
|
||||
$x=$r;$x[101]['updated']=$n+1;fails(fn()=>readit($c,$x,$n),'future original sample rejected');
|
||||
$x=$r;$x[101]['parentID']=201;fails(fn()=>readit($c,$x,$n),'reused variable wrong parent rejected');
|
||||
$x=$r;$x[101]['ident']='other';fails(fn()=>readit($c,$x,$n),'reused variable wrong ident rejected');
|
||||
@@ -44,8 +45,8 @@ $r[101]['value']=20.0;$f=readit($c,$r,$n);check($f['batteryW']===20.0,'idle resi
|
||||
$r[101]['value']=60.0;fails(fn()=>readit($c,$r,$n),'unexplained idle power refused');
|
||||
$r[101]['value']=-2000.0;$r[102]['value']=-1000.0;$r[103]['value']=2000.0;$r[103]['updated']=$n;$r[103]['changed']=$n;
|
||||
$f=readit($c,$r,$n);check($f['batteryW']===-3000.0,'single EV feedback follows physics not desired sign or held filter');
|
||||
$r[104]['value']=1000.0;$r[104]['updated']=$n;$r[104]['changed']=$n;$r[101]['updated']=$n-1;
|
||||
fails(fn()=>readit($c,$r,$n),'SDL transition after physical sample cannot be guessed');
|
||||
$r[104]['value']=1000.0;$r[104]['updated']=$n;$r[104]['changed']=$n;$r[101]['confirmedAt']=$n-1;
|
||||
fails(fn()=>readit($c,$r,$n),'SDL transition after physical confirmation cannot be guessed');
|
||||
[$c,$r,$n]=fx('virtual_split');$c['allowEstimatedForTrial']=true;$r[103]['value']=3000.0;$r[104]['value']=1000.0;$r[101]['value']=1900.0;$r[102]['value']=2000.0;$f=readit($c,$r,$n);
|
||||
check($f['batteryW']===2925.0&&$f['externalModelW']===975.0,'small tracking error allocated but explicitly estimated');
|
||||
check(!$f['separatelyMeasured']&&$f['heldFilterUsed']===false,'partition never labelled separately measured');
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
// Offline only, no IP-Symcon or real device access.
|
||||
declare(strict_types=1);
|
||||
if (function_exists('IPS_GetKernelVersion')) throw new RuntimeException('Offline test only');
|
||||
require_once __DIR__.'/../../libs/NetzfahrplanV4Rueckmeldung.php';
|
||||
require_once __DIR__.'/../../libs/NetzfahrplanV4Geraeteabruf.php';
|
||||
|
||||
use Belevo\EnelixEMS\NetzfahrplanV4Geraeteabruf as D;
|
||||
|
||||
$count=0;
|
||||
function dcheck($ok,$name){global $count;if(!$ok)throw new RuntimeException('FAIL '.$name);$count++;echo "PASS $name\n";}
|
||||
function dfails($fn,$name){try{$fn();}catch(Throwable $e){dcheck(true,$name);return;}throw new RuntimeException('FAIL no rejection '.$name);}
|
||||
function dfixture(){
|
||||
$now=1791032400;
|
||||
$config=['version'=>1,'installationId'=>'e3a08f9e-af12-4695-99bd-8b51c0520021','assetId'=>'ev','managerId'=>17004,'batteryInstanceId'=>44234,
|
||||
'mode'=>'physical_sum','allowEstimatedForTrial'=>false,'maxSkewSeconds'=>30,'maxAgeSeconds'=>60,'idleToleranceW'=>50.0,'trackingToleranceW'=>200.0,
|
||||
'sources'=>[
|
||||
['key'=>'grid','role'=>'grid','variableId'=>10,'parentId'=>20,'ident'=>'grid','factorToW'=>1.0],
|
||||
['key'=>'battery_a','role'=>'physical','variableId'=>11,'parentId'=>21,'ident'=>'a','factorToW'=>1.0],
|
||||
['key'=>'battery_b','role'=>'physical','variableId'=>12,'parentId'=>21,'ident'=>'b','factorToW'=>1.0],
|
||||
]];
|
||||
$records=[
|
||||
10=>['value'=>3000.0,'updated'=>$now-100,'changed'=>$now-200,'parentID'=>20,'ident'=>'grid'],
|
||||
11=>['value'=>-1000.0,'updated'=>$now-100,'changed'=>$now-200,'parentID'=>21,'ident'=>'a'],
|
||||
12=>['value'=>-2000.0,'updated'=>$now-100,'changed'=>$now-200,'parentID'=>21,'ident'=>'b'],
|
||||
];
|
||||
return [$config,$records,$now];
|
||||
}
|
||||
[$c,$r,$n]=dfixture();$calls=[];$times=[$n-1,$n-1,$n];
|
||||
$f=D::snapshot($c,fn($id)=>$id===20?D::MBUS_DEVICE:D::MODBUS_DEVICE,
|
||||
function($id,$module)use(&$calls){$calls[]=$id;return true;},
|
||||
fn($id)=>$r[$id],function()use(&$times){return array_shift($times);});
|
||||
sort($calls);dcheck($calls===[20,21],'one refresh per physical parent');
|
||||
dcheck($f['deviceReadConfirmed']===true&&$f['sourceOldestAt']===$n-1,'successful read confirmation is freshness basis');
|
||||
dcheck($f['sourceVariableOldestAt']===$n-100,'original VariableUpdated retained separately');
|
||||
dcheck($f['batteryW']===-3000.0&&$f['usableForTrial'],'unchanged values accepted after confirmed device response');
|
||||
|
||||
[$c,$r,$n]=dfixture();$times=[$n,$n,$n];
|
||||
dfails(fn()=>D::snapshot($c,fn($id)=>D::MODBUS_DEVICE,fn()=>false,fn($id)=>$r[$id],function()use(&$times){return array_shift($times);}),
|
||||
'failed device read rejected');
|
||||
|
||||
[$c,$r,$n]=dfixture();$times=[$n,$n,$n];$reads=0;
|
||||
dfails(fn()=>D::snapshot($c,fn($id)=>D::MODBUS_DEVICE,fn()=>true,
|
||||
function($id)use(&$r,&$reads){$reads++;$x=$r[$id];if($reads>3&&$id===10)$x['value']++;return $x;},
|
||||
function()use(&$times){return array_shift($times);}), 'change after confirmed response rejected');
|
||||
|
||||
[$c,$r,$n]=dfixture();$times=[$n,$n,$n];
|
||||
dfails(fn()=>D::snapshot($c,fn($id)=>'{00000000-0000-0000-0000-000000000000}',fn()=>true,fn($id)=>$r[$id],
|
||||
function()use(&$times){return array_shift($times);}), 'unsupported device module rejected');
|
||||
|
||||
echo "TOTAL $count confirmed-device-read checks passed. Synthetic calls only.\n";
|
||||
Reference in New Issue
Block a user