Tests / test (push) Successful in 1m1s
Approved by Daniel Haefliger for develop and beta. Author dh_Agent, authenticated account dh. Preserve published battery, charging and overall Energy Pie changes. No deployment or plant control authorization.
198 lines
11 KiB
PHP
198 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
require_once __DIR__.'/NetzfahrplanV4Rueckmeldung.php';
|
|
require_once __DIR__.'/NetzfahrplanV4Geraeteabruf.php';
|
|
|
|
/** Shared confirmed physical reading used by receiver and battery before a trial command. */
|
|
trait BatterieNetzfahrplanV4RueckmeldungTrait
|
|
{
|
|
/** Existing specialist mappings always win; automatic lookup is passive. */
|
|
private function effektiveV4RueckmeldungKonfiguration(?string $property = null): string
|
|
{
|
|
$json = $property ?? $this->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);
|
|
}
|
|
}
|