ReadPropertyString('NetzfahrplanV4RueckmeldungKonfiguration'); if (!in_array(trim($json), ['', '{}'], true)) return $json; $managers = $this->zugeordneteManagerIDs(); if (count($managers) !== 1 || (!function_exists('ENELIX_GetV4RueckmeldungKonfiguration') && !function_exists(__NAMESPACE__ . '\\ENELIX_GetV4RueckmeldungKonfiguration'))) return ''; $json = ENELIX_GetV4RueckmeldungKonfiguration($managers[0], $this->InstanceID); if (!is_string($json) || strlen($json) > 65536) { throw new \RuntimeException('feedback_auto_configuration_invalid'); } if (in_array(trim($json), ['', '{}'], true)) return ''; $config = NetzfahrplanV4Rueckmeldung::configuration(json_decode($json, true, 32, JSON_THROW_ON_ERROR)); if ($config['managerId'] !== $managers[0] || $config['batteryInstanceId'] !== $this->InstanceID || $config['mode'] !== 'physical_sum' || $config['allowEstimatedForTrial']) { throw new \RuntimeException('feedback_auto_configuration_binding_invalid'); } return json_encode($config, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION); } private function registriereV4Rueckmeldung(): void { $this->RegisterPropertyString('NetzfahrplanV4RueckmeldungKonfiguration', '{}'); $this->RegisterTimer('NetzfahrplanV4RueckmeldungAktualisieren', 0, "ENELIX_BatterieTimerAktion(\$_IPS['TARGET'], 'NetzfahrplanV4RueckmeldungAktualisieren', false);"); } private function konfiguriereV4Rueckmeldung(): void { $this->SetBuffer('V4ConfirmedFeedback', ''); $this->SetBuffer('V4ConfirmedFeedbackConfig', ''); $this->SetBuffer('V4ConfirmedFeedbackMono', ''); $this->SetBuffer('V4ConfirmedFeedbackContext', ''); // Assignment can happen after battery ApplyChanges. Empty automatic // configuration returns before device I/O; the existing timer discovers it. $this->SetTimerInterval('NetzfahrplanV4RueckmeldungAktualisieren', 1000); } /** Strictly local: never call the Manager from a Manager -> battery cache read. */ private function v4BatterieRueckmeldung(): array { $property = $this->ReadPropertyString('NetzfahrplanV4RueckmeldungKonfiguration'); $automatic = in_array(trim($property), ['', '{}'], true); $context = json_decode($this->GetBuffer('V4ConfirmedFeedbackContext'), true); $json = is_array($context) && is_string($context['configuration'] ?? null) ? $context['configuration'] : ''; if ($json === '') throw new \RuntimeException('feedback_configuration_unavailable'); $sample = json_decode($this->GetBuffer('V4ConfirmedFeedback'), true); $mono = (float) $this->GetBuffer('V4ConfirmedFeedbackMono'); $now = time();$age = hrtime(true) / 1e9 - $mono; if (!is_array($sample) || !is_int($sample['checkedAt'] ?? null) || $sample['checkedAt'] > $now || $now - $sample['checkedAt'] > 2 || $age < 0 || $age > 2 || ($context['propertyHash'] ?? null) !== hash('sha256', $property) || ($context['autoMode'] ?? null) !== $automatic || (!$automatic && $json !== $property) || $this->GetBuffer('V4ConfirmedFeedbackConfig') !== hash('sha256', $json)) { throw new \RuntimeException('feedback_confirmed_cache_stale_or_invalid'); } $config = NetzfahrplanV4Rueckmeldung::configuration(json_decode($json, true, 32, JSON_THROW_ON_ERROR)); if ($config['batteryInstanceId'] !== $this->InstanceID || ($automatic && ($config['mode'] !== 'physical_sum' || $config['allowEstimatedForTrial'])) || ($sample['configHash'] ?? null) !== NetzfahrplanV4Rueckmeldung::fingerprint($config)) { throw new \RuntimeException('feedback_cache_binding_invalid'); } foreach (['installationId', 'assetId', 'managerId', 'batteryInstanceId'] as $key) { if (($sample[$key] ?? null) !== $config[$key]) throw new \RuntimeException('feedback_cache_binding_invalid'); } foreach ($config['sources'] as $source) { if (!IPS_VariableExists($source['variableId'])) throw new \RuntimeException('feedback_variable_missing'); $object = IPS_GetObject($source['variableId']); if ($object['ParentID'] !== $source['parentId'] || $object['ObjectIdent'] !== $source['ident']) { throw new \RuntimeException('feedback_cache_source_changed'); } } return $sample; } /** Independent acquisition; never waits behind another read or takes the actuator lock. */ private function aktualisiereV4Rueckmeldung(): void { $property=$this->ReadPropertyString('NetzfahrplanV4RueckmeldungKonfiguration'); $automatic=in_array(trim($property), ['', '{}'], true); try { $json=$this->effektiveV4RueckmeldungKonfiguration($property); if ($json==='') { $this->SetBuffer('V4ConfirmedFeedback', ''); $this->SetBuffer('V4ConfirmedFeedbackConfig', ''); $this->SetBuffer('V4ConfirmedFeedbackMono', ''); $this->SetBuffer('V4ConfirmedFeedbackContext', ''); return; } if (strlen($json)>65536) throw new \RuntimeException('feedback_configuration_too_large'); $c=json_decode($json,true,32,JSON_THROW_ON_ERROR); if (!is_array($c) || ($c['batteryInstanceId']??null)!==$this->InstanceID) throw new \RuntimeException('feedback_battery_binding_invalid'); if (!in_array($c['managerId']??0,$this->zugeordneteManagerIDs(),true)) throw new \RuntimeException('feedback_manager_binding_invalid'); } catch (\Throwable $error) { $this->SetBuffer('V4ConfirmedFeedback', ''); throw $error; } $lock='ENELIX.V4.Feedback.'.$this->InstanceID; if (!IPS_SemaphoreEnter($lock,0)) return; try { $reader=static function(int $id):array { if (!IPS_VariableExists($id)) throw new \RuntimeException('feedback_variable_missing'); $v=IPS_GetVariable($id);$o=IPS_GetObject($id);$value=GetValue($id); return ['value'=>$value,'updated'=>$v['VariableUpdated'],'changed'=>$v['VariableChanged'], 'parentID'=>$o['ParentID'],'ident'=>$o['ObjectIdent']]; }; $result=null; for ($attempt=1;$attempt<=3;++$attempt) { try { $result=NetzfahrplanV4Geraeteabruf::snapshot( $c, static function(int $id):string { if (!IPS_InstanceExists($id)) throw new \RuntimeException('feedback_parent_instance_missing'); return (string)(IPS_GetInstance($id)['ModuleInfo']['ModuleID']??''); }, static function(int $id,string $module):bool { if ($module===NetzfahrplanV4Geraeteabruf::MBUS_DEVICE) { if (!function_exists('MBUS_UpdateValues') && !function_exists(__NAMESPACE__.'\\MBUS_UpdateValues')) throw new \RuntimeException('feedback_mbus_refresh_unavailable'); return MBUS_UpdateValues($id)===true; } if (in_array($module,[NetzfahrplanV4Geraeteabruf::MODBUS_DEVICE,NetzfahrplanV4Geraeteabruf::MODBUS_ADDRESS],true)) { if (!function_exists('ModBus_RequestRead') && !function_exists(__NAMESPACE__.'\\ModBus_RequestRead')) throw new \RuntimeException('feedback_modbus_refresh_unavailable'); return ModBus_RequestRead($id)===true; } return false; }, $reader, static fn():int=>time(), static function(int $microseconds):void { usleep($microseconds); } ); break; } catch (\InvalidArgumentException $e) { if (!str_starts_with($e->getMessage(),'feedback_source_changed_after_confirmed_read:') || $attempt>=3) throw $e; usleep(150000); } } if (!is_array($result)) throw new \RuntimeException('feedback_snapshot_unavailable'); $managers=$this->zugeordneteManagerIDs(); if ($this->ReadPropertyString('NetzfahrplanV4RueckmeldungKonfiguration') !== $property || !in_array($c['managerId'], $managers, true) || ($automatic && count($managers) !== 1)) { throw new \RuntimeException('feedback_configuration_changed_during_read'); } // Publish the sample last so a concurrent cache read cannot mix contexts. $this->SetBuffer('V4ConfirmedFeedback', ''); $this->SetBuffer('V4ConfirmedFeedbackContext', json_encode([ 'configuration' => $json, 'propertyHash' => hash('sha256', $property), 'autoMode' => $automatic, ], JSON_THROW_ON_ERROR)); $this->SetBuffer('V4ConfirmedFeedbackConfig', hash('sha256', $json)); $this->SetBuffer('V4ConfirmedFeedbackMono', (string) (hrtime(true) / 1e9)); $this->SetBuffer('V4ConfirmedFeedback',json_encode($result,JSON_THROW_ON_ERROR)); } catch (\Throwable $error) { $this->SetBuffer('V4ConfirmedFeedback', ''); throw $error; } finally { IPS_SemaphoreLeave($lock); } } /** Strictly cache-only diagnostics; never starts a device read or renews sample times. */ public function GetV4BatterieRueckmeldungCache(): string { return json_encode($this->v4BatterieRueckmeldung(), JSON_THROW_ON_ERROR); } /** Read only. A returned current value is NOT actuator permission. */ public function GetV4BatterieRueckmeldung(): string { try { try { $r=$this->v4BatterieRueckmeldung(); } catch (\Throwable) { $this->aktualisiereV4Rueckmeldung(); $r=$this->v4BatterieRueckmeldung(); } } catch (\Throwable $e) { $r=['kind'=>'local_physical_feedback','status'=>'unavailable','reason'=>substr($e->getMessage(),0,200), 'batteryW'=>null,'gridW'=>null,'usableForTrial'=>false,'canDispatch'=>false,'deviceReadConfirmed'=>false]; } return json_encode($r,JSON_THROW_ON_ERROR); } }