0.0],$now); check($v['previewBatteryW']===3000.0 && $v['canDispatch']===false && $v['liveEnabled']===false,'zero grid uses actual surplus, never permission'); invalid('wrong installation',fn(&$r)=>$r['installationId']='00000000-0000-4000-8000-000000000001'); invalid('foreign plan',fn(&$r)=>$r['plan']['installationId']='00000000-0000-4000-8000-000000000001'); invalid('old protocol',function(&$r){unset($r['receiverProtocolVersion']);}); invalid('revision mismatch',fn(&$r)=>$r['plan']['configRevision']=2); invalid('boolean revision',fn(&$r)=>$r['plan']['configRevision']=true); invalid('live response',fn(&$r)=>$r['liveEnabled']=true); invalid('live plan',fn(&$r)=>$r['plan']['runMode']='live'); invalid('fake executable',fn(&$r)=>$r['plan']['executable']='true'); invalid('stale server flag',fn(&$r)=>$r['fresh']=false); invalid('pending computation',fn(&$r)=>$r['pending']=['reason'=>'new price']); invalid('last run different plan',fn(&$r)=>$r['lastRun']['detail']['planId']='wrong'); invalid('last run invalid',fn(&$r)=>$r['lastRun']['status']='invalid_inputs'); invalid('family mismatch',fn(&$r)=>$r['settings']['family']='23'); invalid('unknown family',fn(&$r)=>$r['plan']['sourceFamily']='unregistered'); invalid('receipt stale',function(&$r,&$op,&$now,&$receipt){$receipt-=91;}); invalid('future receipt',function(&$r,&$op,&$now,&$receipt){$receipt++;}); invalid('stale server response',function(&$r)use($now){$r['checkedAt']=gmdate('c',$now-91);}); invalid('future plan',function(&$r)use($now){$r['plan']['generatedAt']=gmdate('c',$now+10);}); invalid('stale plan',function(&$r)use($now){$r['plan']['generatedAt']=gmdate('c',$now-901);}); invalid('price horizon too short',function(&$r)use($now){$r['plan']['pricesKnownUntil']=gmdate('c',$now+300);}); invalid('stale local operation',function(&$r,&$op)use($now){$op['observedAt']=gmdate('c',$now-61);}); invalid('changed local reserve',fn(&$r,&$op)=>$op['batteries'][0]['minSocPercent']=30.0); invalid('changed local grid limit',fn(&$r,&$op)=>$op['limits']['importW']=2000.0); invalid('typed context required',fn(&$r)=>$r['plan']['controlContext']['batteries']['b']['capacityKwh']='20'); invalid('input provenance missing',function(&$r){unset($r['plan']['inputRefs']);}); invalid('unknown assets',function(&$r){$r['plan']['points'][0]['assetTargetsW']=['c'=>3000];}); invalid('overlap',function(&$r){$r['plan']['points'][1]['time']=$r['plan']['points'][0]['time'];}); invalid('gap',function(&$r)use($now){$r['plan']['points'][1]['time']=gmdate('c',$now+310);}); invalid('bool target',fn(&$r)=>$r['plan']['points'][0]['gridTargetW']=false); invalid('string target',fn(&$r)=>$r['plan']['points'][0]['gridTargetW']='0'); invalid('NaN target',fn(&$r)=>$r['plan']['points'][0]['batteryTargetW']=NAN); invalid('broken balance',fn(&$r)=>$r['plan']['points'][0]['baselineGridW']=10000.0); invalid('unknown intent',fn(&$r)=>$r['plan']['points'][0]['intent']='discharge_all'); invalid('no timezone',fn(&$r)=>$r['plan']['points'][0]['time']='2026-10-02T12:00:00'); invalid('bad calendar date',fn(&$r)=>$r['plan']['points'][0]['time']='2026-02-30T12:00:00Z'); invalid('missing step',fn(&$r)=>$r['plan']['points']=[]); invalid('negative curtailment',fn(&$r)=>$r['plan']['points'][0]['pvCurtailmentW']=-100.0); [$r,$id,$op,$now]=receiverFixture();$r['checkedAt']=$op['observedAt']=gmdate('c',$now+300);$op['batteries'][0]['measuredAt']=$op['observedAt']; $x=P::pruefen($r,$id,$op,$now+300,$now+300); check($x['point']['time']===gmdate('c',$now+300),'half-open boundary picks current, not interpolated'); [$r,$id,$op,$now]=receiverFixture();$r['plan']['sourceFamily']=$r['settings']['family']='future-family';$r['families'][]=['key'=>'future-family']; check(P::pruefen($r,$id,$op,$now,$now)['plan']['sourceFamily']==='future-family','registry is extensible'); [$r,$id,$op,$now]=receiverFixture();$op['gridW']=3000.0;$op['batteries'][0]['dischargeBlocked']=true; $x=P::pruefen($r,$id,$op,$now,$now);$v=P::vorschau($x,$op,['b'=>0],$now); check($v['previewBatteryW']===0.0,'local hysteresis prevents preview discharge'); [$r,$id,$op,$now]=receiverFixture();$op['gridW']=3000.0;$op['batteries'][0]['socPercent']=15.001; $x=P::pruefen($r,$id,$op,$now,$now);$v=P::vorschau($x,$op,['b'=>0],$now); check($v['previewBatteryW']>-3 && $v['previewBatteryW']<0,'remaining-step energy cannot cross reserve'); [$r,$id,$op,$now]=receiverFixture();$op['batteries'][0]['socPercent']=90; $x=P::pruefen($r,$id,$op,$now,$now);$v=P::vorschau($x,$op,['b'=>0],$now);check($v['previewBatteryW']===0.0,'full battery cannot charge'); [$r,$id,$op,$now]=receiverFixture();$op['batteries'][0]['maxChargeW']=0;$op['limits']['exportW']=0.;$r['plan']['controlContext']=P::kontext($op); $x=P::pruefen($r,$id,$op,$now,$now);$v=P::vorschau($x,$op,['b'=>0],$now);check($v['unmetGridLimitW']===3000.0 && !$v['canDispatch'],'zero export limit reports unmet constraint'); [$r,$id,$op,$now]=receiverFixture();$x=P::pruefen($r,$id,$op,$now,$now);$op['gridW']=2000;$op['batteries'][0]['gridCharging']=false; $x['point']['gridTargetW']=8000.;$x['point']['intent']='gridCharge';$v=P::vorschau($x,$op,['b'=>0],$now);check($v['previewBatteryW']===0.,'grid charging opt-in enforced'); $op['batteries'][0]['gridCharging']=true;$x['point']['intent']='hold';$v=P::vorschau($x,$op,['b'=>0],$now);check($v['previewBatteryW']===0.,'nonzero hold does not accidentally charge'); $x['point']['gridTargetW']=0.;$x['point']['intent']='discharge';$op['gridW']=0.;$v=P::vorschau($x,$op,['b'=>-2000],$now);check($v['previewBatteryW']===-2000.,'uses actual battery feedback, not incremental zero'); try { P::dekodieren(str_repeat('x',P::MAX_RESPONSE_BYTES+1));check(false,'body limit'); } catch (InvalidArgumentException $e) {check(true,'body limit');} echo "Total: $count checks. Synthetic data only.\n";