fix(battery): keep native timer callbacks reload-safe
Tests / test (push) Failing after 53s

This commit is contained in:
dh
2026-10-08 05:15:27 +00:00
parent 5885681669
commit d42cfb3342
10 changed files with 535 additions and 6 deletions
@@ -0,0 +1,11 @@
{
"id": "{044221CE-A8E5-4413-A081-C3235FF73350}",
"name": "TimerLifecycleProbe",
"type": 3,
"vendor": "ENELIX Test",
"aliases": [],
"parentRequirements": [],
"childRequirements": [],
"implemented": [],
"prefix": "ENLTMR"
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
// Development-only native timer fixture. No device, manager or variable actions.
class TimerLifecycleProbe extends IPSModule
{
public function Create(): void
{
parent::Create();
$this->RegisterPropertyInteger('ReadWorkMs', 1500);
$this->RegisterPropertyInteger('ReportWorkMs', 1500);
$this->RegisterPropertyString('LifecycleMode', 'baseline');
$this->RegisterAttributeInteger('ReadRuns', 0);
$this->RegisterAttributeInteger('ReportRuns', 0);
$this->RegisterAttributeInteger('CallbackEnds', 0);
$this->RegisterAttributeInteger('ScriptEnds', 0);
$this->RegisterTimer('Meldezyklus', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Report', 0); ENLTMR_MarkScriptEnd(\$_IPS['TARGET']);");
$this->RegisterTimer('Rueckmeldesampler', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Read', 0); ENLTMR_MarkScriptEnd(\$_IPS['TARGET']);");
}
public function ApplyChanges(): void
{
parent::ApplyChanges();
$lock = 'ENELIX.TimerProbe.Battery.' . $this->InstanceID;
if (!IPS_SemaphoreEnter($lock, 1000)) throw new RuntimeException('Probe battery busy');
IPS_SemaphoreLeave($lock);
$this->SetTimerInterval('Meldezyklus', 5000);
$this->SetTimerInterval('Rueckmeldesampler', 1000);
$this->SetStatus(102);
}
public function Destroy(): void
{
IPS_LogMessage('ENELIXTimerLifecycleProbe', 'Destroy entry ' . $this->InstanceID);
// Isolated experiment only: determine whether unload can drain native timers.
if (IPS_InstanceExists($this->InstanceID) && $this->ReadPropertyString('LifecycleMode') === 'drain') {
$this->SetTimerInterval('Meldezyklus', 0);
$this->SetTimerInterval('Rueckmeldesampler', 0);
$deadline = microtime(true) + 2.5;
do {
$running = false;
foreach (IPS_GetTimerList() as $id) {
$timer = IPS_GetTimer($id);
if ($timer['InstanceID'] === $this->InstanceID && $timer['Running']) $running = true;
}
if ($running) usleep(20000);
} while ($running && microtime(true) < $deadline);
IPS_LogMessage('ENELIXTimerLifecycleProbe', 'Destroy drain running=' . (int) $running);
}
parent::Destroy();
}
public function Tick(string $ident): void
{
$this->RequestAction($ident, 0);
}
public function StopProbe(): void
{
$this->SetTimerInterval('Meldezyklus', 0);
$this->SetTimerInterval('Rueckmeldesampler', 0);
}
public function RequestAction($Ident, $Value): void
{
if (!in_array($Ident, ['Read', 'Report'], true)) throw new InvalidArgumentException('Unknown probe action');
$lock = 'ENELIX.TimerProbe.' . ($Ident === 'Read' ? 'Feedback.' : 'Battery.') . $this->InstanceID;
if (!IPS_SemaphoreEnter($lock, 0)) return;
try {
usleep(max(0, min(2000, $this->ReadPropertyInteger($Ident . 'WorkMs'))) * 1000);
$this->WriteAttributeInteger($Ident . 'Runs', $this->ReadAttributeInteger($Ident . 'Runs') + 1);
} finally {
IPS_SemaphoreLeave($lock);
}
$this->WriteAttributeInteger('CallbackEnds', $this->ReadAttributeInteger('CallbackEnds') + 1);
}
public function MarkScriptEnd(): void
{
$this->WriteAttributeInteger('ScriptEnds', $this->ReadAttributeInteger('ScriptEnds') + 1);
}
public function ReadProbeState(): string
{
return json_encode(['readRuns' => $this->ReadAttributeInteger('ReadRuns'),
'callbackEnds' => $this->ReadAttributeInteger('CallbackEnds'),
'scriptEnds' => $this->ReadAttributeInteger('ScriptEnds'),
'reportRuns' => $this->ReadAttributeInteger('ReportRuns')], JSON_THROW_ON_ERROR);
}
}