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
+124
View File
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
// Offline contract tests only. Native scheduling is verified by the separate kernel fixture.
if (PHP_SAPI !== 'cli' || function_exists('IPS_GetKernelVersion')) {
throw new RuntimeException('Standalone PHP CLI only; never load mocks into Symcon.');
}
$GLOBALS['deviceCalls'] = 0;
$GLOBALS['testParent'] = 20;
function MBUS_UpdateValues(int $id): bool { ++$GLOBALS['deviceCalls']; throw new RuntimeException('Unexpected device I/O'); }
function ModBus_RequestRead(int $id): bool { ++$GLOBALS['deviceCalls']; throw new RuntimeException('Unexpected device I/O'); }
function IPS_VariableExists(int $id): bool { return $id === 10; }
function IPS_GetObject(int $id): array { return ['ParentID' => $GLOBALS['testParent'], 'ObjectIdent' => 'grid']; }
function IPS_GetInstanceListByModuleID(string $id): array { return [17004]; }
function IPS_GetProperty(int $id, string $key): mixed
{
return $key === 'AutomatischeSuche' ? false : '[{"Aktiv":true,"InstanzID":44234}]';
}
class IPSModule
{
public int $InstanceID = 44234;
public array $properties = [], $attributes = [], $buffers = [], $timers = [], $calls = [];
public function Create(): void {}
public function __call(string $method, array $args): mixed
{
if (str_starts_with($method, 'RegisterProperty')) { $this->properties[$args[0]] ??= $args[1]; return null; }
if (str_starts_with($method, 'RegisterAttribute')) { $this->attributes[$args[0]] ??= $args[1]; return null; }
if (str_starts_with($method, 'RegisterVariable') || $method === 'EnableAction') return null;
if (str_starts_with($method, 'ReadProperty')) return $this->properties[$args[0]];
if (str_starts_with($method, 'ReadAttribute')) return $this->attributes[$args[0]];
if ($method === 'RegisterTimer') { $this->timers[$args[0]] = ['interval' => $args[1], 'script' => $args[2]]; return null; }
if ($method === 'GetBuffer') return $this->buffers[$args[0]] ?? '';
throw new RuntimeException('Unexpected mock call: ' . $method);
}
}
require __DIR__ . '/../../Batterie/module.php';
class TimerContractBattery extends Batterie
{
public function RequestAction($ident, $value): void { $this->calls[] = [$ident, $value]; }
}
$battery = new TimerContractBattery();
$battery->Create();
$GLOBALS['testBattery'] = $battery;
function ENELIX_BatterieTimerAktion(int $id, string $ident, bool $value): void
{
if ($id !== 44234) throw new RuntimeException('Wrong timer target');
$GLOBALS['testBattery']->BatterieTimerAktion($ident, $value);
}
$count = 0;
function check(bool $valid, string $name): void
{
global $count;
if (!$valid) throw new RuntimeException('FAIL ' . $name);
++$count;
echo 'PASS ' . $name . PHP_EOL;
}
function rejects(callable $call, string $reason): void
{
try { $call(); }
catch (RuntimeException $error) {
check($error->getMessage() === $reason, $reason);
return;
}
throw new RuntimeException('Expected rejection: ' . $reason);
}
$expected = [
'Meldezyklus' => ['Melden', false],
'RueckmeldungVerzoegert' => ['Melden', true],
'VorgabeTimeout' => ['VorgabeTimeout', 0],
'AenderungFreigabe' => ['AenderungFreigabe', 0],
'NetzfahrplanV4RueckmeldungAktualisieren' => ['NetzfahrplanV4RueckmeldungAktualisieren', 0],
'NetzfahrplanV4TestWatchdog' => ['NetzfahrplanV4TestWatchdog', 0],
];
check(count($battery->timers) === count($expected), 'same six native timers');
$battery->attributes['ZustandBezogeneEnergie'] = 123.456;
$battery->attributes['EnergieStandZeit'] = 123456789;
$before = [$battery->properties, $battery->attributes, $battery->buffers];
foreach ($expected as $name => $call) {
$timer = $battery->timers[$name];
check($timer['interval'] === 0, $name . ' keeps Create interval zero');
check(str_starts_with($timer['script'], 'ENELIX_BatterieTimerAktion(')
&& !str_contains($timer['script'], 'IPS_RequestAction('), $name . ' uses generated wrapper');
$_IPS = ['TARGET' => 44234];
eval($timer['script']);
check(end($battery->calls) === $call, $name . ' preserves action and value types');
}
check([$battery->properties, $battery->attributes, $battery->buffers] === $before,
'timer entrypoint does not change configuration, energy or buffers');
$config = ['batteryInstanceId' => 44234, 'managerId' => 17004,
'sources' => [['variableId' => 10, 'parentId' => 20, 'ident' => 'grid']]];
$json = json_encode($config, JSON_THROW_ON_ERROR);
$battery->properties['NetzfahrplanV4RueckmeldungKonfiguration'] = $json;
$sample = ['kind' => 'local_physical_feedback', 'checkedAt' => time(),
'sourceOldestAt' => time() - 1, 'deviceReadConfirmed' => true, 'batteryW' => -123.0];
$validBuffers = ['V4ConfirmedFeedback' => json_encode($sample, JSON_THROW_ON_ERROR),
'V4ConfirmedFeedbackConfig' => hash('sha256', $json),
'V4ConfirmedFeedbackMono' => (string) (hrtime(true) / 1e9)];
$battery->buffers = $validBuffers;
check($battery->GetV4BatterieRueckmeldungCache() === $validBuffers['V4ConfirmedFeedback'],
'cache-only API returns original confirmed sample and timestamps');
check($battery->buffers === $validBuffers, 'cache inspection never renews buffers');
foreach ([time() - 3, time() + 10] as $invalidTime) {
$battery->buffers = $validBuffers;
$battery->buffers['V4ConfirmedFeedback'] = json_encode(array_replace($sample, ['checkedAt' => $invalidTime]));
rejects(fn() => $battery->GetV4BatterieRueckmeldungCache(), 'feedback_confirmed_cache_stale_or_invalid');
}
$battery->buffers = $validBuffers;
$battery->buffers['V4ConfirmedFeedbackMono'] = (string) (hrtime(true) / 1e9 - 3);
rejects(fn() => $battery->GetV4BatterieRueckmeldungCache(), 'feedback_confirmed_cache_stale_or_invalid');
$battery->buffers = $validBuffers;
$battery->buffers['V4ConfirmedFeedbackConfig'] = 'changed';
rejects(fn() => $battery->GetV4BatterieRueckmeldungCache(), 'feedback_confirmed_cache_stale_or_invalid');
$battery->buffers = $validBuffers;
$GLOBALS['testParent'] = 99;
rejects(fn() => $battery->GetV4BatterieRueckmeldungCache(), 'feedback_cache_source_changed');
check($GLOBALS['deviceCalls'] === 0, 'all cache checks perform zero device reads');
echo "TOTAL $count offline battery timer/cache contract checks passed. Native scheduler not mocked as proof.\n";