diff --git a/libs/BatterieNetzfahrplanV4RueckmeldungTrait.php b/libs/BatterieNetzfahrplanV4RueckmeldungTrait.php index 0ddfcdd..d2fd5dc 100644 --- a/libs/BatterieNetzfahrplanV4RueckmeldungTrait.php +++ b/libs/BatterieNetzfahrplanV4RueckmeldungTrait.php @@ -49,7 +49,8 @@ trait BatterieNetzfahrplanV4RueckmeldungTrait return false; }, $reader, - static fn():int=>time() + static fn():int=>time(), + static function(int $microseconds):void { usleep($microseconds); } ); $this->SetBuffer('V4ConfirmedFeedback',json_encode($result,JSON_THROW_ON_ERROR)); return $result; diff --git a/libs/ManagerNetzfahrplanV4EmpfangTrait.php b/libs/ManagerNetzfahrplanV4EmpfangTrait.php index a6fde3f..0785d88 100644 --- a/libs/ManagerNetzfahrplanV4EmpfangTrait.php +++ b/libs/ManagerNetzfahrplanV4EmpfangTrait.php @@ -47,10 +47,10 @@ trait ManagerNetzfahrplanV4EmpfangTrait /** Appended dynamically; existing form.json and parallel portal GUI stay untouched. */ private function netzfahrplanV4EmpfangFormular(): array { - return ['type' => 'ExpansionPanel', 'caption' => 'V4 Planempfang / Sollwert-Vorschau', 'items' => [ - ['type' => 'Label', 'caption' => 'NUR BEOBACHTEN. Kein Lade-/Entladebefehl. Der alte intelligente Netzfahrplan muss AUS bleiben.'], + return ['type' => 'ExpansionPanel', 'caption' => 'V4 Planempfang / Prognosemanager', 'items' => [ + ['type' => 'Label', 'caption' => 'Der alte intelligente Netzfahrplan muss AUS bleiben. Die separate Laufzeitvariable unter dem Manager schaltet den zeitlich begrenzten Testbetrieb ein oder aus.'], ['type' => 'CheckBox', 'name' => 'NetzfahrplanV4EmpfangAktiv', 'caption' => 'V4 Schattenplan empfangen und Sollwert anzeigen (keine Steuerung)'], - ['type' => 'Label', 'caption' => 'Diagnose und Sollwerte stehen in den beiden V4-Vorschauvariablen unterhalb des Managers.'], + ['type' => 'Label', 'caption' => 'Schalter: V4 Prognosemanager aktiv (Testbetrieb). Ausschalten beendet V4 und fordert sofort 0 W an. Maximale Laufzeit ab dem ersten Einschalten: 48 Stunden.'], ['type' => 'Button', 'caption' => 'Plan jetzt pruefen (Abruf begrenzt auf 1/min)', 'onClick' => "IPS_RequestAction(\$id, 'NetzfahrplanV4Empfangen', true);"], ]]; @@ -172,6 +172,10 @@ trait ManagerNetzfahrplanV4EmpfangTrait if (is_string($configured) && !in_array(trim($configured),['','{}'],true)) { if (count($topology)!==1) throw new RuntimeException('feedback_multi_asset_adapter_required'); $f=json_decode(ENELIX_GetV4BatterieRueckmeldung($controller),true,32,JSON_THROW_ON_ERROR); + if (($f['status']??null)==='unavailable') { + $reason=is_string($f['reason']??null)?$f['reason']:'unknown'; + throw new RuntimeException('feedback_unavailable:'.substr($reason,0,180)); + } if (($f['installationId']??null)!==$this->ReadAttributeString('LizenzInstallationID') || ($f['assetId']??null)!==$asset['ID'] || ($f['managerId']??null)!==$this->InstanceID || ($f['batteryInstanceId']??null)!==$controller || !is_numeric($f['batteryW']??null) diff --git a/libs/NetzfahrplanV4Geraeteabruf.php b/libs/NetzfahrplanV4Geraeteabruf.php index 097943e..af58290 100644 --- a/libs/NetzfahrplanV4Geraeteabruf.php +++ b/libs/NetzfahrplanV4Geraeteabruf.php @@ -25,7 +25,8 @@ final class NetzfahrplanV4Geraeteabruf callable $moduleId, callable $refresh, callable $reader, - callable $clock + callable $clock, + ?callable $pause = null ): array { $config = NetzfahrplanV4Rueckmeldung::configuration($config); $groups = []; @@ -43,7 +44,17 @@ final class NetzfahrplanV4Geraeteabruf if (!in_array($module, [self::MBUS_DEVICE, self::MODBUS_DEVICE, self::MODBUS_ADDRESS], true)) { throw new InvalidArgumentException('feedback_refresh_module_unsupported:' . $sources[0]['key']); } - if ($refresh($parentId, $module) !== true) { + $confirmed = false; + for ($attempt = 1; $attempt <= 4; ++$attempt) { + if ($refresh($parentId, $module) === true) { + $confirmed = true; + break; + } + if ($attempt < 4 && $pause !== null) { + $pause(200000); + } + } + if (!$confirmed) { throw new InvalidArgumentException('feedback_device_read_failed:' . $sources[0]['key']); } $confirmedAt = $clock(); diff --git a/tests/V4Feedback/device_read_checks.php b/tests/V4Feedback/device_read_checks.php index 6b00aca..bb6f455 100644 --- a/tests/V4Feedback/device_read_checks.php +++ b/tests/V4Feedback/device_read_checks.php @@ -35,6 +35,12 @@ dcheck($f['deviceReadConfirmed']===true&&$f['sourceOldestAt']===$n-1,'successful 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];$attempts=0;$pauses=[]; +$f=D::snapshot($c,fn($id)=>D::MODBUS_DEVICE, + function()use(&$attempts){$attempts++;return $attempts>=2;},fn($id)=>$r[$id], + function()use(&$times){return array_shift($times);},function($us)use(&$pauses){$pauses[]=$us;}); +dcheck($attempts===3&&$pauses===[200000],'transient busy read is retried once without relaxing confirmation'); + [$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'); diff --git a/tests/V4Feedback/receiver_checks.php b/tests/V4Feedback/receiver_checks.php index f617f4b..62887d5 100644 --- a/tests/V4Feedback/receiver_checks.php +++ b/tests/V4Feedback/receiver_checks.php @@ -34,8 +34,9 @@ namespace { 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); + $feedback=['status'=>'unavailable','reason'=>'feedback_device_read_failed:grid','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'); + ck(str_contains($p['reason'],'feedback_unavailable:feedback_device_read_failed:grid'),'configured feedback exposes bounded root cause without credentials'); $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');