feat(control): connect physical feedback to bounded V4 command and fallback paths
This commit is contained in:
@@ -11,6 +11,6 @@ final class NetzfahrplanV4BilanzierungTest extends TestCase
|
||||
$p=proc_open([PHP_BINARY,__DIR__.'/V4Accounting/checks.php'],[0=>['pipe','r'],1=>['pipe','w'],2=>['pipe','w']],$pipes);
|
||||
self::assertIsResource($p);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($p),$out.$err);self::assertSame('',$err);
|
||||
self::assertStringContainsString('TOTAL 34 checks passed',$out);
|
||||
self::assertStringContainsString('TOTAL 42 checks passed',$out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@ final class NetzfahrplanV4DatenTest extends TestCase
|
||||
$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);
|
||||
self::assertStringContainsString('TOTAL 27 application-data checks passed',$out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ final class NetzfahrplanV4RegeltestTest extends TestCase
|
||||
fclose($pipes[1]);fclose($pipes[2]);
|
||||
self::assertSame(0, proc_close($process), $out . $err);
|
||||
self::assertSame('', $err);
|
||||
self::assertStringContainsString('TOTAL 59 checks passed.', $out);
|
||||
self::assertStringContainsString('TOTAL 71 checks passed.', $out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace Belevo\EnelixEMS\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class NetzfahrplanV4RueckmeldungTest extends TestCase
|
||||
{
|
||||
private function checkScript(string $name): void
|
||||
{
|
||||
$pipes=[];
|
||||
$p=proc_open([PHP_BINARY,__DIR__.'/V4Feedback/'.$name],
|
||||
[0=>['pipe','r'],1=>['pipe','w'],2=>['pipe','w']],$pipes);
|
||||
self::assertIsResource($p);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($p),$out.$err);
|
||||
self::assertSame('',$err);self::assertStringContainsString('TOTAL ',$out);
|
||||
}
|
||||
public function testPhysicalFeedback(): void {$this->checkScript('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');}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ namespace Belevo\EnelixEMS {
|
||||
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_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]; }
|
||||
function IPS_GetObject($id) { return ['ParentID'=>200,'ObjectIdent'=>'source'.$id]; }
|
||||
function GetValue($id) { return $id===50001?$GLOBALS['objects'][17004]->op['gridW']:$GLOBALS['objects'][17004]->actual['b']; }
|
||||
|
||||
function IPS_RequestAction($id,$key,$value) {
|
||||
$GLOBALS['calls'][]=[$id,$key];$b=$GLOBALS['objects'][$id];
|
||||
if ($key==='NetzfahrplanV4TestStart') $b->V4BatterieTestStarten($value);
|
||||
@@ -76,7 +81,10 @@ namespace Belevo\EnelixEMS {
|
||||
private function berechtigungLizenziert($k) { return $this->licensed; }
|
||||
private function actieveDummy() {}
|
||||
private function aktiveVerbraucherIDs() { return [44234]; }
|
||||
private function netzfahrplanV4LokaleVorschauwerte() { return [$this->op,$this->actual]; }
|
||||
private function netzfahrplanV4LokaleVorschauwerte() {
|
||||
$op=$this->op;$f=json_decode($GLOBALS['objects'][44234]->GetV4BatterieRueckmeldung(),true);
|
||||
$op['localFeedback']=['b'=>$f];return [$op,$this->actual];
|
||||
}
|
||||
private function regeln($renew) { $this->fallback++; }
|
||||
public function tick(&$allocation,$consumers=null,$sync=true) {
|
||||
$this->v4ManagerTestUebergabe($allocation,$consumers??[['InstanzID'=>44234,'Daten'=>[]]],$sync);
|
||||
@@ -105,7 +113,14 @@ namespace {
|
||||
'maxChargeW'=>5000.,'maxDischargeW'=>5000.,'actuatorWatchdogEvidenceId'=>'synthetic-hardware-watchdog',
|
||||
'accountingEvidenceId'=>'synthetic-meter-boundary-v1','acceptEstimatedPeak'=>true,'controlContext'=>$r['plan']['controlContext']];
|
||||
$r['controlledTrial']=$serverAuthority?$a:null;
|
||||
$m=new ManagerHarness($r,$op);$b=new BatteryHarness();$GLOBALS['objects']=[17004=>$m,44234=>$b];
|
||||
$m=new ManagerHarness($r,$op);$b=new BatteryHarness();$GLOBALS['objects']=[17004=>$m,44234=>$b];$GLOBALS['feedbackAt']=[];
|
||||
$fc=['version'=>1,'installationId'=>$id,'assetId'=>'b','managerId'=>17004,'batteryInstanceId'=>44234,
|
||||
'mode'=>'physical_sum','allowEstimatedForTrial'=>false,'maxSkewSeconds'=>30,'maxAgeSeconds'=>60,
|
||||
'idleToleranceW'=>50.0,'trackingToleranceW'=>200.0,'sources'=>[]];
|
||||
foreach ([50001=>'grid',50002=>'physical'] as $vid=>$role) $fc['sources'][]=['key'=>'source'.$vid,'role'=>$role,'variableId'=>$vid,'parentId'=>200,'ident'=>'source'.$vid,'factorToW'=>1.0];
|
||||
$b->props['NetzfahrplanV4RueckmeldungKonfiguration']=json_encode($fc);
|
||||
$op['localFeedback']=['b'=>json_decode($b->GetV4BatterieRueckmeldung(),true)];
|
||||
|
||||
$m->props['NetzfahrplanV4RegeltestErlaubt']=$permissions;$b->props['NetzfahrplanV4RegeltestErlaubt']=$permissions;
|
||||
$b->props['NetzfahrplanV4GeraeteWatchdogNachweis']='synthetic-hardware-watchdog';
|
||||
$start=['confirmation'=>'START_BOUNDED_V4_CONTROL_TRIAL','sessionId'=>$a['sessionId'],'durationSeconds'=>300,'maxChargeW'=>5000.,'maxDischargeW'=>5000.];
|
||||
@@ -157,5 +172,6 @@ namespace {
|
||||
$bad=$r;$bad['controlledTrial']['validUntil']=gmdate('c',$now+120);rejects(fn()=>Gate::befehl($bad,$op,['b'=>0.],$local,$now,$now),'oversized server authorization lease');
|
||||
$bad=$r;$bad['controlledTrial']['acceptEstimatedPeak']=false;rejects(fn()=>Gate::befehl($bad,$op,['b'=>0.],$local,$now,$now),'peak uncertainty needs explicit acceptance');
|
||||
require __DIR__ . '/additional_checks.php';
|
||||
require __DIR__ . '/feedback_checks.php';
|
||||
echo "TOTAL $count checks passed. Mocked IPS, clocks and register writes only.\n";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// 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);$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');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$c=$m->command();$c['sequence']++;$c['feedbackConfigHash']=str_repeat('f',64);
|
||||
rejects(fn()=>$b->V4BatterieTestBefehl(json_encode($c)),'changed feedback command binding rejected');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$c=$m->command();check(isset($c['feedbackConfigHash'],$c['feedbackSourceOldestAt'])&&array_key_exists('importLimitW',$c),'command includes source provenance and limits');
|
||||
$c['sequence']++;unset($c['exportLimitW']);rejects(fn()=>$b->V4BatterieTestBefehl(json_encode($c)),'missing explicit grid limit not interpreted as unlimited');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$c=$m->command();$c['sequence']++;$c['importLimitW']=1000.;$c['watts']=0;
|
||||
$m->op['gridW']=1500.;$b->measurements['Ladezustand']=15.;
|
||||
rejects(fn()=>$b->V4BatterieTestBefehl(json_encode($c)),'final device clipping cannot silently violate accepted grid boundary');
|
||||
check(!$b->active()&&end($b->writes)===0,'unreachable final limit aborts software trial');
|
||||
[$m,$b,$s]=fixture();start($m,$s);tick($m);$feedbackAt[50002]=$clock+1;tick($m);
|
||||
check(!$m->active(),'future physical sample is not smoothed into control');
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// Offline only, no actual Symcon calls.
|
||||
declare(strict_types=1);
|
||||
if (function_exists('IPS_GetKernelVersion')) throw new RuntimeException('Offline test only');
|
||||
require_once __DIR__.'/../../libs/NetzfahrplanV4Rueckmeldung.php';
|
||||
use Belevo\EnelixEMS\NetzfahrplanV4Rueckmeldung as F;
|
||||
$count=0;
|
||||
function check($b,$s){global $count;if(!$b)throw new RuntimeException('FAIL '.$s);$count++;echo "PASS $s\n";}
|
||||
function fails($f,$s){try{$f();}catch(Throwable $e){check(true,$s);return;}throw new RuntimeException('FAIL no rejection '.$s);}
|
||||
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];}
|
||||
return [$c,$r,$now];
|
||||
}
|
||||
function readit($c,$r,$n){return F::read($c,fn($id)=>$r[$id],$n);}
|
||||
[$c,$r,$n]=fx();$f=readit($c,$r,$n);
|
||||
check($f['batteryW']===-3000.0&&$f['gridW']===3000.0,'physical total and grid signs retained');
|
||||
check($f['usableForTrial']&&!$f['estimated']&&!$f['canDispatch'],'usable feedback alone never grants dispatch');
|
||||
F::requireTrial($f,$c,$n);check(true,'matching trial binding accepted');
|
||||
check(F::fingerprint($c)===F::fingerprint(json_decode(json_encode($c),true)),'configuration hash stable over numeric property roundtrip');
|
||||
$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+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');
|
||||
$d=$c;$d['sources'][1]['variableId']=$d['sources'][0]['variableId'];fails(fn()=>F::configuration($d),'duplicate physical source rejected');
|
||||
$d=$c;$d['maxAgeSeconds']=300;fails(fn()=>F::configuration($d),'historical freshness not accepted for live control');
|
||||
$calls=[];fails(function()use($c,$r,$n,&$calls){F::read($c,function($id)use($r,&$calls){$calls[$id]=($calls[$id]??0)+1;$x=$r[$id];if($calls[$id]>1)$x['value']+=1;return$x;},$n);},'same-second change during double read rejected');
|
||||
$d=$c;$d['sources'][1]['factorToW']=-1;check(readit($d,$r,$n)['batteryW']===1000.0,'vendor sign conversion once');
|
||||
$f=readit($c,$r,$n);fails(fn()=>F::requireTrial($f,$c,$n+3),'old validated snapshot not reused');
|
||||
$d=$c;$d['managerId']=20000;fails(fn()=>F::requireTrial($f,$d,$n),'different manager cannot use feedback');
|
||||
$d=$c;$d['feedbackConfigHash']=str_repeat('a',64);fails(fn()=>F::requireTrial($f,$d,$n),'changed mapping aborts trial');
|
||||
[$c,$r,$n]=fx('virtual_split');$r[103]['updated']=$r[104]['updated']=$r[105]['updated']=$n-86400;$r[103]['changed']=$r[104]['changed']=$r[105]['changed']=$n-172800;
|
||||
$r[101]['value']=$r[102]['value']=$r[103]['value']=$r[104]['value']=0.0;$f=readit($c,$r,$n);
|
||||
check($f['batteryW']===0.0&&$f['estimated'],'unchanged idle commands need no artificial heartbeat');
|
||||
check(!$f['usableForTrial'],'virtual estimate requires explicit acceptance');
|
||||
$c['allowEstimatedForTrial']=true;$f=readit($c,$r,$n);check($f['usableForTrial']&&!$f['gatewayWatchdogVerified'],'idle acceptance is not gateway watchdog proof');
|
||||
$r[101]['value']=20.0;$f=readit($c,$r,$n);check($f['batteryW']===20.0,'idle residual not clipped to zero');
|
||||
$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');
|
||||
[$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');
|
||||
$r[101]['value']=1000.0;fails(fn()=>readit($c,$r,$n),'large tracking error prevents stale virtual correction');
|
||||
$r[103]['value']=42000.0;$r[104]['value']=-40000.0;$r[101]['value']=0.0;$r[102]['value']=2000.0;$f=readit($c,$r,$n);
|
||||
check(!$f['usableForTrial']&&in_array('opposing_virtual_flows_not_identifiable',$f['reasons'],true),'counterflow cannot be independently attributed');
|
||||
$r[105]['value']=false;fails(fn()=>readit($c,$r,$n),'disabled gateway blocks feedback');
|
||||
$src=file_get_contents(__DIR__.'/../../libs/NetzfahrplanV4Rueckmeldung.php');
|
||||
check(!preg_match('/\b(?:SetValue|IPS_RequestAction|curl_exec|IPS_SetProperty|AC_SetLoggingStatus)\s*\(/',$src),'pure feedback has no control side effects');
|
||||
echo "TOTAL $count physical-feedback checks passed. Synthetic source readings only.\n";
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// Exercise the EXACT serialized fallback wrapper from Manager/module.php in an isolated harness.
|
||||
if (function_exists('IPS_GetKernelVersion')) throw new RuntimeException('Offline only');
|
||||
$source=file_get_contents(__DIR__.'/../../Manager/module.php');
|
||||
$a=strpos($source,' private function regeln(bool $erneuern): void');$b=strpos($source,' private function regelnIntern(bool $erneuern): void',$a);
|
||||
if($a===false||$b===false)throw new RuntimeException('Manager wrapper source changed');
|
||||
$method=substr($source,$a,$b-$a);$calls=[];$locked=false;
|
||||
function IPS_SemaphoreEnter($k,$t){global$locked;if($locked)return false;$locked=true;return true;}
|
||||
function IPS_SemaphoreLeave($k){global$locked;$locked=false;}
|
||||
function IPS_GetInstance($id){return ['InstanceStatus'=>$GLOBALS['instance']->status];}
|
||||
eval('class FallbackHarness { const STATUS_AKTIV=102; public $InstanceID=17004,$trial=true,$active=true,$status=102,$abortFirst=true,$throwFirst=false,$passes=[],$attributes=[],$stops=[];'.
|
||||
'public function run(){ $this->regeln(false); } private function v4ManagerTestSession(){return $this->trial?[1]:[];} private function GetValue($k){return $this->active;}'.
|
||||
'private function WriteAttributeString($k,$v){$this->attributes[$k]=$v;} private function v4ManagerTestAbbrechen($s){$this->stops[]=$s;$this->trial=false;}'.
|
||||
'private function regelnIntern(bool $r):void { $this->passes[]=$r;if(count($this->passes)==1){if($this->throwFirst)throw new RuntimeException("synthetic_failure");if($this->abortFirst)$this->trial=false;}}'. $method.'}');
|
||||
$count=0;function ck($v,$s){global$count;if(!$v)throw new RuntimeException('FAIL '.$s);$count++;echo "PASS $s\n";}
|
||||
$instance=new FallbackHarness();$instance->run();ck($instance->passes===[false,true],'trial abort performs exactly one fresh ordinary recomputation');
|
||||
ck($instance->attributes['LetzteSollwerte']==='{}','pre-trial cached target is discarded before fallback');ck(!$locked,'control semaphore released after fallback');
|
||||
$instance=new FallbackHarness();$instance->trial=false;$instance->run();ck($instance->passes===[false],'ordinary control path does not gain extra passes');
|
||||
$instance=new FallbackHarness();$instance->abortFirst=false;$instance->run();ck($instance->passes===[false],'valid trial not overridden by ordinary pass');
|
||||
$instance=new FallbackHarness();$instance->status=201;$instance->run();ck($instance->passes===[false],'invalid measurement status prevents forced ordinary recomputation');
|
||||
$instance=new FallbackHarness();$instance->throwFirst=true;try{$instance->run();}catch(RuntimeException $e){}
|
||||
ck(!$locked&&!$instance->trial&&count($instance->stops)===1,'exception revokes trial and releases semaphore without endless retries');
|
||||
echo "TOTAL $count actual manager fallback-wrapper checks passed. No real IPS runtime.\n";
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// Exact Battery message builder with no optional visible diagnostic variables.
|
||||
if(function_exists('IPS_GetKernelVersion'))throw new RuntimeException('Offline only');
|
||||
class IPSModule {
|
||||
public int $InstanceID=44234;public array $buffers=['V4FeedbackEstimated'=>'1'];public bool $valid=true;
|
||||
protected function GetBuffer($k){return $this->buffers[$k]??'';}
|
||||
protected function GetValue($k){throw new RuntimeException('No diagnostic variables exist: '.$k);}
|
||||
protected function ReadAttributeBoolean($k){return $k==='ZustandLadezustandGueltig'?$this->valid:false;}
|
||||
protected function ReadAttributeInteger($k){return 0;}
|
||||
protected function ReadAttributeFloat($k){return 0.0;}
|
||||
protected function ReadAttributeString($k){return $k==='Leistungsangebot'?'[0]':($k==='Betriebsart'?'Auto':'');}
|
||||
protected function ReadPropertyInteger($k){return 1;}
|
||||
}
|
||||
require __DIR__.'/../../Batterie/module.php';
|
||||
use Belevo\EnelixEMS\Nachrichtenvertrag as N;
|
||||
$b=new Batterie();$read=new ReflectionMethod(Batterie::class,'baueVerbraucherdaten');$count=0;
|
||||
function ck($v,$s){global$count;if(!$v)throw new RuntimeException('FAIL '.$s);$count++;echo "PASS $s\n";}
|
||||
$d=$read->invoke($b,17004);ck($d['Leistungsquelle']===N::LEISTUNGSQUELLE_BERECHNET,'partition source quality independent of hidden diagnostic variables');
|
||||
$b->buffers['V4FeedbackEstimated']='0';$d=$read->invoke($b,17004);ck($d['Leistungsquelle']===N::LEISTUNGSQUELLE_GEMESSEN,'ordinary physical source marked measured');
|
||||
$b->valid=false;$d=$read->invoke($b,17004);ck($d['Leistungsquelle']===N::LEISTUNGSQUELLE_NICHT_VORHANDEN&&$d['Istleistung_W']===null,'invalid sample never published as measured');
|
||||
$b->valid=true;$b->buffers['V4FeedbackEstimated']='1';$b->buffers['V4BatteryTrialSession']='{}';$d=$read->invoke($b,17004);ck($d['Leistungsquelle']===N::LEISTUNGSQUELLE_BERECHNET,'last estimate not relabelled measured immediately after trial ends');
|
||||
$source=file_get_contents(__DIR__.'/../../Batterie/module.php');ck(str_contains($source,"SetBuffer('V4FeedbackEstimated', (\$messwerte['V4FeedbackEstimated'] ?? false) ? '1' : '0')"),'fresh measurement updates internal source quality');
|
||||
echo "TOTAL $count actual message-builder checks passed. No real variables or writes.\n";
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Belevo\EnelixEMS {
|
||||
if (\function_exists('IPS_GetKernelVersion')) throw new \RuntimeException('CLI test only');
|
||||
require_once __DIR__.'/../../libs/ManagerNetzfahrplanV4EmpfangTrait.php';
|
||||
class Lizenzpruefung {const NETZFAHRPLAN='synthetic';}
|
||||
function time(){return $GLOBALS['now'];}
|
||||
function IPS_InstanceExists($id){return $id===44234;}
|
||||
function IPS_GetInstance($id){return ['ModuleInfo'=>['ModuleID'=>'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}']];}
|
||||
function IPS_GetProperty($id,$k){return ['IstleistungVariableID'=>1002,'LadezustandVariableID'=>1001,'NetzfahrplanV4RueckmeldungKonfiguration'=>$GLOBALS['configured']?'configured':'{}'][$k];}
|
||||
function IPS_VariableExists($id){return $id===1002;}
|
||||
function IPS_GetVariable($id){return ['VariableUpdated'=>$GLOBALS['now']];}
|
||||
function GetValue($id){return -9000.;} // deliberately held/incorrect legacy virtual display
|
||||
class FeedbackReceiver {
|
||||
use ManagerNetzfahrplanV4EmpfangTrait;
|
||||
public int $InstanceID=17004;public array $props,$attrs,$op;
|
||||
public function __construct($r,$op){$this->op=$op;$this->props=['NetzfahrplanV4EmpfangAktiv'=>true,'NetzfahrplanV4SchattenAktiv'=>true,'NetzfahrplanAktiv'=>false,'MesswertMaxAlter'=>60,'AnlagenBatterien'=>json_encode([['ID'=>'b','LeistungVariableID'=>1002,'SOCVariableID'=>1001,'Leistungsfaktor'=>1]])];$this->attrs=['LizenzInstallationID'=>$r['installationId'],'NetzfahrplanV4Empfang'=>json_encode(['response'=>$r,'receivedAt'=>time()]),'NetzfahrplanV4EmpfangStatus'=>'{}'];}
|
||||
private function ReadPropertyBoolean($k){return $this->props[$k];}
|
||||
private function ReadPropertyString($k){return $this->props[$k];}
|
||||
private function ReadPropertyInteger($k){return $this->props[$k];}
|
||||
private function ReadAttributeString($k){return $this->attrs[$k];}
|
||||
private function aktiveVerbraucherIDs(){return [44234];}
|
||||
private function berechtigungLizenziert($k){return true;}
|
||||
public function GetNetzfahrplanV4Diagnose(){return json_encode(['operation'=>$this->op]);}
|
||||
}
|
||||
}
|
||||
namespace {
|
||||
function ENELIX_GetV4BatterieRueckmeldung($id){return json_encode($GLOBALS['feedback']);}
|
||||
require_once __DIR__.'/../V4Receiver/fixture.php';
|
||||
use Belevo\EnelixEMS\FeedbackReceiver;
|
||||
[$response,$id,$op,$now]=receiverFixture();$configured=true;$count=0;
|
||||
$feedback=['kind'=>'local_physical_feedback','version'=>1,'installationId'=>$id,'assetId'=>'b','managerId'=>17004,'batteryInstanceId'=>44234,'batteryW'=>0.,'gridW'=>$op['gridW'],'checkedAt'=>$now,'estimated'=>true,'usableForTrial'=>false,'canDispatch'=>false,'configHash'=>str_repeat('a',64)];
|
||||
function ck($v,$t){global$count;if(!$v)throw new RuntimeException('FAIL '.$t);$count++;echo "PASS $t\n";}
|
||||
$m=new FeedbackReceiver($response,$op);$p=json_decode($m->GetNetzfahrplanV4Vorschau(),true);
|
||||
ck($p['status']==='preview_only'&&$p['actualBatteryW']==0,'real receiver uses corrected physical feedback rather than held -9kW');
|
||||
ck($p['previewBatteryW']==3000,'net target translated once to total battery demand');
|
||||
ck($p['canDispatch']===false&&$p['localFeedback']['b']['estimated']===true,'model feedback remains marked and preview grants no dispatch');
|
||||
$feedback=['status'=>'unavailable','batteryW'=>null,'gridW'=>null];$p=json_decode($m->GetNetzfahrplanV4Vorschau(),true);
|
||||
ck($p['status']==='unavailable'&&$p['previewBatteryW']===null,'configured feedback failure never falls back to held virtual display');
|
||||
$configured=false;$p=json_decode($m->GetNetzfahrplanV4Vorschau(),true);
|
||||
ck($p['status']==='preview_only'&&$p['feedbackMeaning']==='legacy_display_only_not_for_trial','unconfigured old preview stays compatible but explicitly not for control');
|
||||
ck($p['canDispatch']===false,'legacy display cannot gain actuation permission');
|
||||
echo "TOTAL $count actual receiver checks passed; synthetic read-only controller API.\n";
|
||||
}
|
||||
Reference in New Issue
Block a user