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.
283 lines
20 KiB
PHP
283 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
require_once __DIR__ . '/../../libs/ManagerPrognoseWorkflowTrait.php';
|
|
require_once __DIR__ . '/../../libs/ManagerNetzfahrplanV4DatenTrait.php';
|
|
require_once __DIR__ . '/../../libs/Lizenzpruefung.php';
|
|
require_once __DIR__ . '/../V4Receiver/fixture.php';
|
|
|
|
function time(): int { return $GLOBALS['workflowTime']; }
|
|
function gmdate(string $format, ?int $at = null): string { return \gmdate($format, $at ?? time()); }
|
|
function IPS_SemaphoreEnter(string $name, int $wait): bool { return true; }
|
|
function IPS_SemaphoreLeave(string $name): void { $GLOBALS['workflowUnlocks'][] = $name; }
|
|
function IPS_VariableExists(int $id): bool { return isset($GLOBALS['workflowObjects'][$id]); }
|
|
function IPS_GetObject(int $id): array { return $GLOBALS['workflowObjects'][$id]; }
|
|
function IPS_GetVariable(int $id): array { return ['VariableType' => 2]; }
|
|
function IPS_InstanceExists(int $id): bool { return isset($GLOBALS['workflowInstances'][$id]); }
|
|
function IPS_GetInstance(int $id): array { return $GLOBALS['workflowInstances'][$id]; }
|
|
function IPS_GetProperty(int $id, string $key) { return $GLOBALS['workflowProperties'][$id][$key] ?? null; }
|
|
function IPS_RequestAction(int $id, string $action, $value): bool
|
|
{
|
|
$GLOBALS['workflowActions'][] = ['id' => $id, 'action' => $action, 'value' => json_decode($value, true, 64, JSON_THROW_ON_ERROR)];
|
|
return !in_array($id, $GLOBALS['workflowReject'] ?? [], true);
|
|
}
|
|
|
|
final class WorkflowHarness
|
|
{
|
|
use ManagerPrognoseWorkflowTrait;
|
|
use ManagerNetzfahrplanV4DatenTrait;
|
|
|
|
public int $InstanceID = 900;
|
|
public array $properties = [];
|
|
public array $attributes = [];
|
|
public array $values = ['Aktiv' => true, 'NetzfahrplanV4Aktivtest' => false];
|
|
public array $timers = [];
|
|
public array $requests = [];
|
|
public array $session = [];
|
|
public array $operation = [];
|
|
public array $actual = [];
|
|
public bool $licensed = true;
|
|
public ?string $httpError = null;
|
|
public ?string $selectedDataset = null;
|
|
public int $uiUpdates = 0;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->registrierePrognoseWorkflow();
|
|
$this->registriereNetzfahrplanV4Daten();
|
|
[$response, $id, $operation, $now] = \receiverFixture();
|
|
$GLOBALS['workflowTime'] = $now;
|
|
$id = '11111111-2222-4333-8444-555555555555';
|
|
$response['installationId'] = $response['plan']['installationId'] = $id;
|
|
$response['plan']['inputQuality'] = ['loadBasis' => 'base_load', 'accountingEvidenceId' => 'physical-map-v1'];
|
|
$this->properties += ['PrognoseAktiv' => true, 'NetzleistungVariableID' => 101, 'Netzleistungsfaktor' => 1.0,
|
|
'MesswertMaxAlter' => 60, 'SDLAktiv' => false, 'SDLLeistungVariableID' => 0,
|
|
'AnlagenWechselrichter' => json_encode([
|
|
['ID' => 'pv-wr', 'Typ' => 'pv', 'IstleistungVariableID' => 102, 'Istleistungsfaktor' => 1.0],
|
|
['ID' => 'battery-wr', 'Typ' => 'battery'],
|
|
], JSON_THROW_ON_ERROR),
|
|
'AnlagenBatterien' => json_encode([['ID' => 'battery', 'WechselrichterID' => 'battery-wr',
|
|
'Kopplung' => 'ac', 'LeistungVariableID' => 103, 'Leistungsfaktor' => 1.0]], JSON_THROW_ON_ERROR)];
|
|
$this->attributes += ['LizenzInstallationID' => $id, 'PrognoseInstallationsToken' => ''];
|
|
$this->attributes['NetzfahrplanV4Empfang'] = json_encode(['response' => $response, 'receivedAt' => $now], JSON_THROW_ON_ERROR);
|
|
$operation['localFeedback'] = ['b' => [
|
|
'version' => 1, 'kind' => 'local_physical_feedback', 'installationId' => $id, 'managerId' => 900,
|
|
'assetId' => 'b', 'batteryInstanceId' => 901, 'checkedAt' => $now, 'sourceOldestAt' => $now,
|
|
'maxAgeSeconds' => 60, 'configHash' => str_repeat('a', 64), 'gridW' => $operation['gridW'],
|
|
'batteryW' => 0.0, 'estimated' => false, 'separatelyMeasured' => true,
|
|
'deviceReadConfirmed' => true, 'usableForTrial' => true,
|
|
]];
|
|
$this->operation = $operation;
|
|
$this->actual = ['b' => 0.0];
|
|
$GLOBALS['workflowObjects'] = [];
|
|
$GLOBALS['workflowInstances'] = [
|
|
900 => ['InstanceStatus' => 102, 'ModuleInfo' => ['ModuleID' => 'manager-module']],
|
|
901 => ['InstanceStatus' => 102, 'ModuleInfo' => ['ModuleID' => 'battery-module']],
|
|
902 => ['InstanceStatus' => 102, 'ModuleInfo' => ['ModuleID' => 'charger-module']],
|
|
];
|
|
foreach ([101 => 'Grid', 102 => 'PV', 103 => 'Battery'] as $source => $ident) {
|
|
$GLOBALS['workflowObjects'][$source] = ['ParentID' => $source + 100, 'ObjectIdent' => $ident, 'ObjectType' => 2];
|
|
$GLOBALS['workflowInstances'][$source + 100] = ['InstanceStatus' => 102,
|
|
'ModuleInfo' => ['ModuleID' => NetzfahrplanV4Geraeteabruf::MODBUS_DEVICE]];
|
|
}
|
|
$GLOBALS['workflowProperties'] = [901 => ['NetzfahrplanV4GeraeteWatchdogNachweis' => 'device-watchdog-reviewed-v1']];
|
|
$GLOBALS['workflowActions'] = [];
|
|
$GLOBALS['workflowReject'] = [];
|
|
}
|
|
|
|
public function RegisterPropertyBoolean(string $name, bool $value): void { $this->properties[$name] ??= $value; }
|
|
public function RegisterPropertyString(string $name, string $value): void { $this->properties[$name] ??= $value; }
|
|
public function RegisterAttributeString(string $name, string $value): void { $this->attributes[$name] ??= $value; }
|
|
public function RegisterAttributeInteger(string $name, int $value): void { $this->attributes[$name] ??= $value; }
|
|
public function RegisterTimer(string $name, int $value, string $script): void { $this->timers[$name] = $value; }
|
|
public function RegisterVariableString(string $name, string $caption, string $profile, int $position): void {}
|
|
public function ReadPropertyBoolean(string $name): bool { return $this->properties[$name] ?? false; }
|
|
public function ReadPropertyString(string $name): string { return $this->properties[$name] ?? ''; }
|
|
public function ReadPropertyInteger(string $name): int { return $this->properties[$name] ?? 0; }
|
|
public function ReadPropertyFloat(string $name): float { return $this->properties[$name] ?? 0.0; }
|
|
public function ReadAttributeString(string $name): string { return $this->attributes[$name] ?? ''; }
|
|
public function ReadAttributeInteger(string $name): int { return $this->attributes[$name] ?? 0; }
|
|
public function WriteAttributeString(string $name, string $value): void { $this->attributes[$name] = $value; }
|
|
public function WriteAttributeInteger(string $name, int $value): void { $this->attributes[$name] = $value; }
|
|
public function SetTimerInterval(string $name, int $value): void { $this->timers[$name] = $value; }
|
|
public function GetValue(string $name) { return $this->values[$name] ?? false; }
|
|
public function SetValue(string $name, $value): void { $this->values[$name] = $value; }
|
|
private function leseJsonAttribut(string $name): array { return json_decode($this->ReadAttributeString($name) ?: '{}', true, 64, JSON_THROW_ON_ERROR); }
|
|
private function berechtigungLizenziert(string $capability): bool { return $this->licensed; }
|
|
private function aktiveVerbraucherIDs(): array { return [901, 902]; }
|
|
private function v4ManagerTestSession(): array { return $this->session; }
|
|
private function netzfahrplanV4LokaleVorschauwerte(): array { return [$this->operation, $this->actual]; }
|
|
private function aktualisierePrognoseBedienung(): void { ++$this->uiUpdates; }
|
|
private function prognoseWorkflowHttp(string $suffix, array $body): array
|
|
{
|
|
$this->requests[] = ['suffix' => $suffix, 'body' => $body];
|
|
if ($this->httpError !== null) throw new RuntimeException($this->httpError);
|
|
if ($suffix === '/setup') {
|
|
return ['datasetId' => $body['dataset']['datasetId'], 'selectedDatasetId' => $this->selectedDataset ?? $body['dataset']['datasetId']];
|
|
}
|
|
return ['status' => 'recorded'];
|
|
}
|
|
public function server(bool $ready, string $state = 'ready', ?int $at = null): void
|
|
{
|
|
$this->attributes['PrognoseV4Serverstatus'] = json_encode(['receivedAt' => $at ?? time(),
|
|
'workflow' => ['planningReady' => $ready, 'state' => $state, 'reason' => 'server-' . $state]], JSON_THROW_ON_ERROR);
|
|
}
|
|
public function state(): array { return $this->prognoseWorkflowZustand(); }
|
|
public function prepare(): void { $this->vorbereitePrognoseWorkflow(); }
|
|
public function tick(): void { $this->aktualisierePrognoseWorkflow(); }
|
|
public function dataset(): string { return $this->prognoseV4Datensatz(); }
|
|
public function mapping(): string { return $this->prognoseV4Messkonfiguration(); }
|
|
public function correct(array &$allocation): ?array { return $this->korrigierePrognoseBatterie($allocation, [], true); }
|
|
}
|
|
|
|
$count = 0;
|
|
function workflowCheck(bool $ok, string $label): void
|
|
{
|
|
global $count;
|
|
if (!$ok) throw new RuntimeException('FAIL ' . $label);
|
|
++$count;
|
|
echo 'PASS ' . $label . "\n";
|
|
}
|
|
|
|
$h = new WorkflowHarness();
|
|
$h->server(false, 'collecting');
|
|
workflowCheck($h->state()['state'] === 'collecting' && !$h->state()['ready'], 'model not ready does not enable control');
|
|
$h->properties['NetzfahrplanRegelungAktiv'] = true;
|
|
$a = ['Sollwerte' => [901 => 1200, 902 => 5000]]; $old = $a;
|
|
workflowCheck($h->correct($a) === null && $a === $old, 'persisted control request cannot bypass missing model');
|
|
$h->properties['NetzfahrplanAktiv'] = true;
|
|
$h->properties['NetzfahrplanV4RegeltestErlaubt'] = true;
|
|
$h->properties['NetzfahrplanV4AktivtestErlaubt'] = true;
|
|
workflowCheck(!$h->state()['ready'] && !$h->state()['controlActive'], 'old flags do not promote an unready new workflow');
|
|
$h = new WorkflowHarness(); $h->server(false, 'learning');
|
|
workflowCheck($h->state()['state'] === 'learning', 'learning state remains visible before readiness');
|
|
$h = new WorkflowHarness(); $h->server(true);
|
|
workflowCheck($h->state()['state'] === 'ready' && $h->state()['ready'] && !$h->state()['controlActive'], 'ready model and physical guards still do not activate control');
|
|
$h->properties['NetzfahrplanRegelungAktiv'] = true;
|
|
$h->attributes['PrognoseV4Regelstatus'] = json_encode(['controlActive' => true, 'at' => time()], JSON_THROW_ON_ERROR);
|
|
workflowCheck($h->state()['state'] === 'active' && $h->state()['controlActive'], 'fresh local delivery status required for active state');
|
|
$h->attributes['PrognoseV4Regelstatus'] = json_encode(['controlActive' => true, 'at' => time() - 11], JSON_THROW_ON_ERROR);
|
|
workflowCheck($h->state()['state'] === 'interrupted' && !$h->state()['controlActive'], 'stale active status expires');
|
|
$h->server(true, 'ready', time() - 91);
|
|
workflowCheck(!$h->state()['ready'] && !$h->state()['controlActive'], 'stale server status blocks activation');
|
|
$h->server(true, 'ready', time() + 1);
|
|
workflowCheck(!$h->state()['ready'], 'future server status blocked');
|
|
$h = new WorkflowHarness(); $h->server(true); $h->licensed = false;
|
|
workflowCheck($h->state()['state'] === 'interrupted' && !$h->state()['ready'], 'missing licence blocks readiness');
|
|
$h->tick();
|
|
workflowCheck($h->requests === [] && $h->uiUpdates === 1, 'no unlicensed server traffic and visible UI refresh');
|
|
$h = new WorkflowHarness(); $h->server(true); $h->properties['PrognoseAktiv'] = false;
|
|
$h->properties['NetzfahrplanRegelungAktiv'] = true;
|
|
workflowCheck($h->state()['state'] === 'disabled' && !$h->state()['ready'], 'learning switch off prevents control');
|
|
$h->tick();
|
|
workflowCheck(count($h->requests) === 1 && $h->requests[0]['suffix'] === '/workflow'
|
|
&& !$h->requests[0]['body']['learningEnabled'] && !$h->requests[0]['body']['controlActive'], 'learning off is reported without setup or activation');
|
|
$h = new WorkflowHarness(); $h->server(true); $h->values['Aktiv'] = false;
|
|
workflowCheck(!$h->state()['ready'], 'inactive manager blocks readiness');
|
|
$h = new WorkflowHarness(); $h->server(true); $h->session = ['sessionId' => 'existing'];
|
|
workflowCheck(!$h->state()['ready'], 'separate commissioning session cannot run alongside ordinary control');
|
|
$h = new WorkflowHarness(); $h->server(true); $h->values['NetzfahrplanV4Aktivtest'] = true;
|
|
workflowCheck(!$h->state()['ready'], 'old active test is not ordinary control permission');
|
|
$h = new WorkflowHarness(); $h->server(true);
|
|
$GLOBALS['workflowProperties'][901]['NetzfahrplanV4GeraeteWatchdogNachweis'] = 'explicit_test_plant_watchdog_waiver';
|
|
workflowCheck(!$h->state()['ready'], 'historical watchdog waiver cannot activate ordinary control');
|
|
$h = new WorkflowHarness(); $h->server(true);
|
|
$h->operation['localFeedback']['b']['checkedAt'] -= 3;
|
|
workflowCheck(!$h->state()['ready'], 'stale physical feedback blocks ready despite healthy server');
|
|
$h = new WorkflowHarness(); $h->server(true);
|
|
$h->attributes['PrognoseV4Einrichtungsfehler'] = 'mapping failed';
|
|
workflowCheck($h->state()['reason'] === 'mapping failed' && !$h->state()['ready'], 'setup error cannot preserve readiness');
|
|
$h = new WorkflowHarness(); $h->server(true); $h->httpError = 'synthetic HTTP failure';
|
|
$h->tick();
|
|
workflowCheck($h->state()['state'] === 'interrupted' && !$h->state()['ready'], 'connection failure prevents active state');
|
|
$h->httpError = null; $h->tick();
|
|
workflowCheck($h->attributes['PrognoseV4Verbindungsfehler'] === '' && $h->state()['ready'], 'successful report recovers connection status');
|
|
|
|
$h = new WorkflowHarness(); $h->prepare();
|
|
workflowCheck($h->attributes['PrognoseV4Einrichtungsfehler'] === '' && str_starts_with($h->dataset(), 'v4-'), 'ordinary metered plant automatically configured');
|
|
$automatic = $h->attributes['PrognoseV4Einrichtung'];
|
|
$h->tick();
|
|
workflowCheck(array_column($h->requests, 'suffix') === ['/setup', '/workflow'], 'automatic dataset registered before workflow report');
|
|
$h->requests = []; $h->tick();
|
|
workflowCheck(array_column($h->requests, 'suffix') === ['/workflow'], 'unchanged setup throttled');
|
|
$h->properties['Netzleistungsfaktor'] = 1000.0; $h->prepare();
|
|
workflowCheck($h->attributes['PrognoseV4Einrichtung'] === $automatic && $h->attributes['PrognoseV4Einrichtungsfehler'] !== '', 'changed units hold while original setup remains preserved');
|
|
$h = new WorkflowHarness();
|
|
$GLOBALS['workflowInstances'][203]['ModuleInfo']['ModuleID'] = 'foreign-virtual-storage-module';
|
|
$h->prepare();
|
|
workflowCheck($h->attributes['PrognoseV4Einrichtungsfehler'] !== '' && $h->attributes['PrognoseV4Einrichtung'] === '{}', 'foreign library is not physical measurement evidence');
|
|
$h = new WorkflowHarness(); $h->prepare();
|
|
$automatic = $h->attributes['PrognoseV4Einrichtung'];
|
|
$mapping = $h->mapping();
|
|
$h->properties['NetzfahrplanV4Messkonfiguration'] = $mapping;
|
|
$h->properties['NetzfahrplanV4Datensatz'] = 'specialist-existing-v2';
|
|
$h->prepare(); $h->tick();
|
|
workflowCheck($h->mapping() === $mapping && $h->dataset() === 'specialist-existing-v2', 'custom mapping and dataset retained byte for byte');
|
|
workflowCheck($h->attributes['PrognoseV4Einrichtung'] === $automatic, 'historical automatic setup preserved for diagnosis');
|
|
workflowCheck(array_column($h->requests, 'suffix') === ['/workflow'], 'custom mapping never re-registers old automatic dataset');
|
|
$h->properties['NetzfahrplanV4Datensatz'] = ''; $h->prepare();
|
|
workflowCheck($h->attributes['PrognoseV4Einrichtungsfehler'] !== '', 'custom mapping without explicit dataset cannot reuse old automatic identity');
|
|
$h = new WorkflowHarness(); $h->prepare(); $h->selectedDataset = 'existing-server-choice'; $h->tick();
|
|
workflowCheck($h->attributes['PrognoseV4Verbindungsfehler'] !== '' && count($h->requests) === 1, 'server dataset choice is never silently overwritten or reported ready');
|
|
|
|
// Execute the actual Manager send block and sender method in a fake-only harness.
|
|
$module = file_get_contents(__DIR__ . '/../../Manager/module.php');
|
|
$start = strpos($module, ' $this->v4ManagerTestUebergabe($ergebnis, $verbraucher, $synchronisiert);');
|
|
$end = strpos($module, ' $this->aktualisiereStoerungen($verbraucher, $stoerungen);', $start);
|
|
$methodStart = strpos($module, ' private function sendeManagerdaten(');
|
|
$methodEnd = strpos($module, "\n /**", $methodStart);
|
|
if ($start === false || $end === false || $methodStart === false || $methodEnd === false) throw new RuntimeException('Manager source boundary changed');
|
|
$block = substr($module, $start, $end - $start);
|
|
$method = substr($module, $methodStart, $methodEnd - $methodStart);
|
|
class WorkflowSendBase
|
|
{
|
|
public int $InstanceID = 900;
|
|
public array $attributes = ['LetzteSollwerte' => '{}'];
|
|
public array $logs = [];
|
|
protected function v4ManagerTestUebergabe(array &$allocation, array $consumers, bool $sync): void {}
|
|
protected function korrigierePrognoseBatterie(array &$allocation, array $consumers, bool $sync): ?array
|
|
{
|
|
$allocation['Sollwerte'][901] = 2000;
|
|
return ['batteryInstanceId' => 901, 'targetW' => 2000, 'physicalResponseVerified' => false];
|
|
}
|
|
protected function leseJsonAttribut(string $name): array { return json_decode($this->attributes[$name], true, 64, JSON_THROW_ON_ERROR); }
|
|
protected function WriteAttributeString(string $name, string $value): void { $this->attributes[$name] = $value; }
|
|
protected function v4ManagerTestAbbrechen(string $reason): void {}
|
|
protected function protokolliere(string $kind, array $data): void { $this->logs[] = $data; }
|
|
}
|
|
eval('namespace Belevo\\EnelixEMS; use RuntimeException; use Throwable; class WorkflowSendHarness extends WorkflowSendBase {'
|
|
. 'public function run(array $ergebnis): array { $verbraucher=[]; $synchronisiert=true; $erneuern=false;'
|
|
. '$betriebsart="PV"; $stoerungen=[];' . $block . 'return $ergebnis; }' . $method . '}');
|
|
$setup = new WorkflowHarness();
|
|
$sender = new WorkflowSendHarness();
|
|
$sender->run(['Sollwerte' => [901 => 1500, 902 => 5000]]);
|
|
workflowCheck(array_column($GLOBALS['workflowActions'], 'id') === [902, 901], 'other commands sent before corrected battery');
|
|
workflowCheck($GLOBALS['workflowActions'][1]['value']['Sollleistung_W'] === 2000, 'successful handoff uses corrected battery target');
|
|
workflowCheck(json_decode($sender->attributes['PrognoseV4Regelstatus'], true)['controlActive'] === true, 'active only after successful sends');
|
|
$GLOBALS['workflowActions'] = []; $GLOBALS['workflowReject'] = [902];
|
|
$sender = new WorkflowSendHarness();
|
|
$sender->run(['Sollwerte' => [901 => 1500, 902 => 5000]]);
|
|
$report = json_decode($sender->attributes['PrognoseV4Regelstatus'], true);
|
|
workflowCheck(array_column($GLOBALS['workflowActions'], 'id') === [902, 901]
|
|
&& $GLOBALS['workflowActions'][1]['value']['Sollleistung_W'] === 1500, 'failed other command falls back to original ordinary battery target');
|
|
workflowCheck(!$report['controlActive'] && $report['fallbackTargetW'] === 1500, 'fallback is not reported as active V4');
|
|
workflowCheck(!array_key_exists(902, json_decode($sender->attributes['LetzteSollwerte'], true)), 'false device return does not advance accepted target');
|
|
workflowCheck(count($sender->logs) === 1 && $sender->logs[0]['InstanzID'] === 901, 'rejected handoff is not logged as successful');
|
|
$GLOBALS['workflowActions'] = []; $GLOBALS['workflowReject'] = [901];
|
|
$sender = new WorkflowSendHarness();
|
|
$sender->run(['Sollwerte' => [901 => 1500, 902 => 5000]]);
|
|
workflowCheck(!json_decode($sender->attributes['PrognoseV4Regelstatus'], true)['controlActive'], 'false battery return cannot claim active control');
|
|
workflowCheck(!array_key_exists(901, json_decode($sender->attributes['LetzteSollwerte'], true)), 'false battery return retains previous accepted target');
|
|
$GLOBALS['workflowActions'] = []; $GLOBALS['workflowReject'] = [];
|
|
$sender = new WorkflowSendHarness();
|
|
$sender->attributes['LetzteSollwerte'] = json_encode([901 => 2000, 902 => 5000], JSON_THROW_ON_ERROR);
|
|
$sender->run(['Sollwerte' => [901 => 1500, 902 => 5000]]);
|
|
workflowCheck(array_column($GLOBALS['workflowActions'], 'id') === [901], 'active battery target renewed even if value unchanged');
|
|
echo "TOTAL $count workflow checks passed; fake clocks, transport and Symcon calls only.\n";
|