feat(v4): confirm feedback with device reads
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"scope": "confirmed_feedback_trial_disabled",
|
||||
"managerId": 17004,
|
||||
"batteryInstanceId": 44234,
|
||||
"files": {
|
||||
"libs/NetzfahrplanV4Rueckmeldung.php": {
|
||||
"before": "a7459a208c4dd84ffe3a166d266dcd00216f3660fe60e5ee61d5042d0b840631",
|
||||
"after": "0b261f6534c5ca9864018ea4c2caf97e6ce6913d92f13dcc926383b4f401a673"
|
||||
},
|
||||
"libs/NetzfahrplanV4Geraeteabruf.php": {
|
||||
"before": null,
|
||||
"after": "2b69a2ed27554139db6c996c906a4afe430918381e6359d23e8bb3369cd9be47"
|
||||
},
|
||||
"libs/BatterieNetzfahrplanV4RueckmeldungTrait.php": {
|
||||
"before": "24cf0654f1a6bd545d64ce5f115f309b16d8ffca5172ec64fb37c390c6f001a7",
|
||||
"after": "1de81499da07ad814c98ba778ca728e34817bdfcf29cc7e518f7b5f8fd7733b7"
|
||||
}
|
||||
},
|
||||
"testSupport": {
|
||||
"libs/BatterieNetzfahrplanV4TestTrait.php": "80474ff24cf9b2d32dfe481f0ce26bb94584c458b0fa3de232dec3897b64a4b2",
|
||||
"libs/NetzfahrplanV4Regeltest.php": "bdac3e2e54c50a4c1541da8918ddf3ab2f665bb5cc6696848f546e7b05f5f249",
|
||||
"libs/ManagerNetzfahrplanV4TestTrait.php": "c0d9b6336574dd8acf5f84adb12a32cad3b42ae91c5a31d513d0bc638a560f18",
|
||||
"libs/NetzfahrplanV4Planpruefung.php": "24b935d80bad4ce2d9047b6777aaf74a18773236ca91a2844a0907012f4fadf4"
|
||||
},
|
||||
"tests": {
|
||||
"tests/V4Feedback/checks.php": "b1aacc540ba11adabdd140aaded9e2968d1328f261ca15756e6002db3427330a",
|
||||
"tests/V4Feedback/device_read_checks.php": "5c5d9e50dd1d0f80cb01a2d95e63018eeec558ba78c07afde208e3c0ef304cc3",
|
||||
"tests/V4ControlTrial/checks.php": "2f12e94aeb207e6939a645f2f45b5f54e57c18530360ed3b16aa52ff89a1ef39",
|
||||
"tests/V4ControlTrial/additional_checks.php": "df01119f2b9c0befe1d14ae6b6ddf39c75d3687b669142e9b24b5f9f4821f2c3",
|
||||
"tests/V4ControlTrial/feedback_checks.php": "e87d41dff8e3ecd414a59b3dd7a234b34183d37acca8cddaf847b82eae92e60c",
|
||||
"tests/V4Receiver/fixture.php": "0eeb65e63250aff5ca364a82c2b2a71f9f229be2d9d412e3037014979deb793a"
|
||||
},
|
||||
"configurationSha256": "e393c1f01b30febc1d0785233474be43e98d849b152b4b2450b72efa6b73d540",
|
||||
"dependencies": {
|
||||
"libs/NetzfahrplanV4Regeltest.php": "bdac3e2e54c50a4c1541da8918ddf3ab2f665bb5cc6696848f546e7b05f5f249",
|
||||
"libs/ManagerNetzfahrplanV4TestTrait.php": "c0d9b6336574dd8acf5f84adb12a32cad3b42ae91c5a31d513d0bc638a560f18",
|
||||
"libs/ManagerNetzfahrplanV4EmpfangTrait.php": "936939a3e4a5a5d54fdff68f6fc689a9400e8406aecd5ac09ebc8a6b569c0b66",
|
||||
"Batterie/module.php": "3d5fcd83e4e7d999d5ac949fe357b492d5661ffa76639d2ca57a3f3934eabdbe",
|
||||
"Manager/module.php": "cbbb33a8f71dda8cc80a4fc01cd3d6154da8153b5523306b2da723bb44f2dd79"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# V4 confirmed feedback for Lihrenmoos
|
||||
|
||||
This incremental package adds synchronous device reads before V4 feedback is evaluated.
|
||||
|
||||
- Grid input 40348 is refreshed through its M-Bus device.
|
||||
- GoodWe and SolarEdge inputs are refreshed through their ModBus parent instances.
|
||||
- A successful driver call is required; cached variable timestamps alone are not accepted.
|
||||
- Daniel Haefliger accepted the existing `virtual_split` model for the bounded field test on 2026-10-04.
|
||||
- The installer keeps the legacy schedule and both V4 trial permissions disabled.
|
||||
- A real control trial still requires separate server authorization and evidence of an independent device-side watchdog.
|
||||
|
||||
Build a private staging directory:
|
||||
|
||||
```sh
|
||||
python3 examples/V4ConfirmedFeedback/build_stage.py /srv/agent/netplan-v4-confirmed-feedback
|
||||
```
|
||||
|
||||
Run the staged PHP checks before copying the package to the test plant. Execute `install.php` only from the IP-Symcon script editor on Lihrenmoos.
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Create the reviewed Lihrenmoos confirmed-feedback package; never installs into Symcon."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
|
||||
here = Path(__file__).resolve().parent
|
||||
repo = here.parents[1]
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("Usage: python3 build_stage.py NEW_PRIVATE_STAGING_DIRECTORY")
|
||||
target = Path(sys.argv[1]).resolve()
|
||||
allowed_roots = [Path("/srv/agent"), Path("/home/agent/services/qa")]
|
||||
if not any(target != root and target.is_relative_to(root) for root in allowed_roots):
|
||||
raise SystemExit("Use a private agent staging directory, not live module paths")
|
||||
if target.exists():
|
||||
raise SystemExit("Destination must not exist")
|
||||
|
||||
manifest = json.loads((here / "MANIFEST.json").read_text())
|
||||
contents = {}
|
||||
for group in ("files", "testSupport", "tests"):
|
||||
for name, record in manifest[group].items():
|
||||
expected = record["after"] if group == "files" else record
|
||||
source = repo / name
|
||||
if source.is_symlink() or not source.resolve().is_relative_to(repo):
|
||||
raise SystemExit("Unsafe source: " + name)
|
||||
data = source.read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != expected:
|
||||
raise SystemExit("Reviewed source changed: " + name)
|
||||
contents["source/" + name] = data
|
||||
|
||||
for name, expected in manifest["dependencies"].items():
|
||||
if hashlib.sha256((repo / name).read_bytes()).hexdigest() != expected:
|
||||
raise SystemExit("Dependency changed: " + name)
|
||||
|
||||
for name in ("install.php", "file_installer.php", "MANIFEST.json", "feedback-config.json", "README.md"):
|
||||
contents[name] = (here / name).read_bytes()
|
||||
if hashlib.sha256(contents["feedback-config.json"]).hexdigest() != manifest["configurationSha256"]:
|
||||
raise SystemExit("Configuration changed")
|
||||
|
||||
target.mkdir(parents=True, mode=0o700)
|
||||
for name, data in contents.items():
|
||||
path = target / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
path.chmod(0o640)
|
||||
(target / "PACKAGE_HASHES.json").write_text(
|
||||
json.dumps({name: hashlib.sha256(data).hexdigest() for name, data in contents.items()}, indent=2) + "\n"
|
||||
)
|
||||
print("Private confirmed-feedback package created:", target, "; both trial permissions remain disabled.")
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"version": 1,
|
||||
"installationId": "e3a08f9e-af12-4695-99bd-8b51c0520021",
|
||||
"assetId": "anlage01-virtual-ev",
|
||||
"managerId": 17004,
|
||||
"batteryInstanceId": 44234,
|
||||
"mode": "virtual_split",
|
||||
"allowEstimatedForTrial": true,
|
||||
"maxSkewSeconds": 30,
|
||||
"maxAgeSeconds": 60,
|
||||
"idleToleranceW": 50.0,
|
||||
"trackingToleranceW": 200.0,
|
||||
"sources": [
|
||||
{
|
||||
"key": "grid",
|
||||
"variableId": 40348,
|
||||
"parentId": 11490,
|
||||
"ident": "Power_8",
|
||||
"factorToW": 1000,
|
||||
"role": "grid"
|
||||
},
|
||||
{
|
||||
"key": "physical_goodwe1",
|
||||
"variableId": 47725,
|
||||
"parentId": 19742,
|
||||
"ident": "A_6_3_35182",
|
||||
"factorToW": -1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "physical_goodwe2",
|
||||
"variableId": 35724,
|
||||
"parentId": 57658,
|
||||
"ident": "A_6_3_35182",
|
||||
"factorToW": -1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "physical_solaredge",
|
||||
"variableId": 21447,
|
||||
"parentId": 30789,
|
||||
"ident": "Value",
|
||||
"factorToW": 1,
|
||||
"role": "physical"
|
||||
},
|
||||
{
|
||||
"key": "ev_requested",
|
||||
"variableId": 19651,
|
||||
"parentId": 58448,
|
||||
"ident": "Nennleistung_Soll_EV",
|
||||
"factorToW": 1.0,
|
||||
"role": "ev_request"
|
||||
},
|
||||
{
|
||||
"key": "sdl_requested",
|
||||
"variableId": 38943,
|
||||
"parentId": 58448,
|
||||
"ident": "Nennleistung_Soll_SDL",
|
||||
"factorToW": 1.0,
|
||||
"role": "sdl_request"
|
||||
},
|
||||
{
|
||||
"key": "gateway_active",
|
||||
"role": "gateway_active",
|
||||
"variableId": 23483,
|
||||
"parentId": 58448,
|
||||
"ident": "State",
|
||||
"factorToW": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Atomic incremental installer for the confirmed-feedback update. */
|
||||
function v4ConfirmedWrite(string $path, string $content, int $mode = 0644): void
|
||||
{
|
||||
if (is_link($path)) {
|
||||
throw new RuntimeException('Symlink wird nicht ersetzt.');
|
||||
}
|
||||
$temp = tempnam(dirname($path), '.v4-confirmed-');
|
||||
if ($temp === false) {
|
||||
throw new RuntimeException('Temporaere Datei nicht verfuegbar.');
|
||||
}
|
||||
try {
|
||||
if (file_put_contents($temp, $content, LOCK_EX) !== strlen($content)
|
||||
|| !chmod($temp, $mode)
|
||||
|| !rename($temp, $path)) {
|
||||
throw new RuntimeException('Atomarer Dateitausch fehlgeschlagen.');
|
||||
}
|
||||
} finally {
|
||||
if (is_file($temp)) {
|
||||
unlink($temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function v4ConfirmedFiles(string $stage, string $target): array
|
||||
{
|
||||
$allowed = [
|
||||
'libs/NetzfahrplanV4Rueckmeldung.php',
|
||||
'libs/NetzfahrplanV4Geraeteabruf.php',
|
||||
'libs/BatterieNetzfahrplanV4RueckmeldungTrait.php',
|
||||
];
|
||||
$manifest = json_decode((string) file_get_contents($stage . '/MANIFEST.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
if (($manifest['scope'] ?? null) !== 'confirmed_feedback_trial_disabled'
|
||||
|| count($manifest['files'] ?? []) !== count($allowed)
|
||||
|| array_diff(array_keys($manifest['files']), $allowed)
|
||||
|| array_diff($allowed, array_keys($manifest['files']))) {
|
||||
throw new RuntimeException('Unerwarteter Dateiumfang.');
|
||||
}
|
||||
if (realpath($target) !== $target || is_link($target)) {
|
||||
throw new RuntimeException('Modulpfad unerwartet.');
|
||||
}
|
||||
foreach ($manifest['dependencies'] as $name => $hash) {
|
||||
if (!preg_match('~^(libs/[A-Za-z0-9]+\\.php|Batterie/module\\.php|Manager/module\\.php)$~D', $name)
|
||||
|| is_link($target . '/' . $name)
|
||||
|| hash_file('sha256', $target . '/' . $name) !== $hash) {
|
||||
throw new RuntimeException('Abhaengigkeit wurde parallel geaendert: ' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
$before = [];
|
||||
$source = [];
|
||||
$already = true;
|
||||
foreach ($allowed as $name) {
|
||||
$path = $target . '/' . $name;
|
||||
$candidate = $stage . '/source/' . $name;
|
||||
if (is_link($path) || is_link($candidate) || realpath(dirname($path)) !== $target . '/' . dirname($name)) {
|
||||
throw new RuntimeException('Unerwarteter Dateipfad.');
|
||||
}
|
||||
$source[$name] = (string) file_get_contents($candidate);
|
||||
if (hash('sha256', $source[$name]) !== $manifest['files'][$name]['after']) {
|
||||
throw new RuntimeException('Paketpruefsumme geaendert: ' . $name);
|
||||
}
|
||||
token_get_all($source[$name], TOKEN_PARSE);
|
||||
$before[$name] = is_file($path) ? file_get_contents($path) : null;
|
||||
$hash = $before[$name] === null ? null : hash('sha256', $before[$name]);
|
||||
if (!in_array($hash, [$manifest['files'][$name]['before'], $manifest['files'][$name]['after']], true)) {
|
||||
throw new RuntimeException('Paralleler Modulstand; nichts ueberschrieben: ' . $name);
|
||||
}
|
||||
if ($hash !== $manifest['files'][$name]['after']) {
|
||||
$already = false;
|
||||
}
|
||||
}
|
||||
if ($already) {
|
||||
return ['status' => 'already_installed', 'backup' => null, 'filesChanged' => []];
|
||||
}
|
||||
|
||||
$backup = $stage . '/backups/' . gmdate('Ymd\\THis\\Z') . '-' . bin2hex(random_bytes(4));
|
||||
if (!mkdir($backup, 0700, true)) {
|
||||
throw new RuntimeException('Sicherung fehlgeschlagen.');
|
||||
}
|
||||
foreach ($allowed as $name) {
|
||||
if ($before[$name] !== null) {
|
||||
$destination = $backup . '/' . $name;
|
||||
if (!is_dir(dirname($destination))) {
|
||||
mkdir(dirname($destination), 0700, true);
|
||||
}
|
||||
v4ConfirmedWrite($destination, $before[$name], 0600);
|
||||
}
|
||||
}
|
||||
v4ConfirmedWrite(
|
||||
$backup . '/MANIFEST.json',
|
||||
json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . "\n",
|
||||
0600
|
||||
);
|
||||
|
||||
$written = [];
|
||||
try {
|
||||
foreach ($allowed as $name) {
|
||||
if ((is_file($target . '/' . $name) ? file_get_contents($target . '/' . $name) : null) !== $before[$name]) {
|
||||
throw new RuntimeException('Parallele Aenderung waehrend Installation.');
|
||||
}
|
||||
if ($before[$name] === $source[$name]) {
|
||||
continue;
|
||||
}
|
||||
v4ConfirmedWrite($target . '/' . $name, $source[$name]);
|
||||
$written[] = $name;
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
foreach (array_reverse($written) as $name) {
|
||||
$path = $target . '/' . $name;
|
||||
if (!is_link($path) && is_file($path)
|
||||
&& hash_file('sha256', $path) === $manifest['files'][$name]['after']) {
|
||||
if ($before[$name] === null) {
|
||||
unlink($path);
|
||||
} else {
|
||||
v4ConfirmedWrite($path, $before[$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw $error;
|
||||
}
|
||||
|
||||
return ['status' => 'installed', 'backup' => $backup, 'filesChanged' => $written];
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Execute only in Lihrenmoos Symcon. Installs confirmed reads and accepted virtual_split mapping; never starts a trial.
|
||||
if (!function_exists('IPS_GetKernelVersion')) {
|
||||
throw new RuntimeException('Nur im IP-Symcon-Skripteditor ausfuehren.');
|
||||
}
|
||||
require_once __DIR__ . '/file_installer.php';
|
||||
|
||||
$manager = 17004;
|
||||
$battery = 44234;
|
||||
$locked = false;
|
||||
$changedMapping = false;
|
||||
$oldMapping = null;
|
||||
$report = [
|
||||
'scope' => 'confirmed_feedback_trial_disabled',
|
||||
'startedAt' => gmdate('c'),
|
||||
'actuatorPermissionGranted' => false,
|
||||
'serverUpdateRequired' => true,
|
||||
'acceptedModel' => 'virtual_split',
|
||||
];
|
||||
|
||||
try {
|
||||
$hashes = json_decode((string) file_get_contents(__DIR__ . '/PACKAGE_HASHES.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
foreach ($hashes as $name => $hash) {
|
||||
if (str_contains($name, '..') || str_starts_with($name, '/') || is_link(__DIR__ . '/' . $name)
|
||||
|| hash_file('sha256', __DIR__ . '/' . $name) !== $hash) {
|
||||
throw new RuntimeException('Vorbereitetes Paket veraendert.');
|
||||
}
|
||||
}
|
||||
foreach ([
|
||||
$manager => '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||
$battery => '{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
|
||||
] as $id => $module) {
|
||||
if (!IPS_InstanceExists($id) || IPS_GetInstance($id)['ModuleInfo']['ModuleID'] !== $module) {
|
||||
throw new RuntimeException('Modulzuordnung ungueltig.');
|
||||
}
|
||||
if (IPS_HasChanges($id)) {
|
||||
throw new RuntimeException('Offene Aenderungen zuerst speichern oder verwerfen.');
|
||||
}
|
||||
}
|
||||
|
||||
$beforeManager = json_decode(IPS_GetConfiguration($manager), true, 128, JSON_THROW_ON_ERROR);
|
||||
$beforeBattery = json_decode(IPS_GetConfiguration($battery), true, 128, JSON_THROW_ON_ERROR);
|
||||
if (($beforeManager['NetzfahrplanAktiv'] ?? null) !== false
|
||||
|| ($beforeManager['NetzfahrplanV4RegeltestErlaubt'] ?? false) !== false
|
||||
|| ($beforeBattery['NetzfahrplanV4RegeltestErlaubt'] ?? false) !== false) {
|
||||
throw new RuntimeException('Alte Fahrplanausgabe und beide Regeltestfreigaben muessen AUS bleiben.');
|
||||
}
|
||||
|
||||
$configuration = json_decode((string) file_get_contents(__DIR__ . '/feedback-config.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
$manifest = json_decode((string) file_get_contents(__DIR__ . '/MANIFEST.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
if (($configuration['managerId'] ?? null) !== $manager
|
||||
|| ($configuration['batteryInstanceId'] ?? null) !== $battery
|
||||
|| ($configuration['mode'] ?? null) !== 'virtual_split'
|
||||
|| ($configuration['allowEstimatedForTrial'] ?? null) !== true
|
||||
|| hash_file('sha256', __DIR__ . '/feedback-config.json') !== $manifest['configurationSha256']) {
|
||||
throw new RuntimeException('Falsche Rueckmeldekonfiguration.');
|
||||
}
|
||||
$desired = json_encode($configuration, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
|
||||
$previousConfiguration = $configuration;
|
||||
$previousConfiguration['allowEstimatedForTrial'] = false;
|
||||
$previousDesired = json_encode($previousConfiguration, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
|
||||
$oldMapping = $beforeBattery['NetzfahrplanV4RueckmeldungKonfiguration'] ?? '{}';
|
||||
if (!in_array($oldMapping, [$previousDesired, $desired], true)) {
|
||||
throw new RuntimeException('Abweichende Rueckmeldekonfiguration nicht ueberschrieben.');
|
||||
}
|
||||
|
||||
$controls = [];
|
||||
foreach (IPS_GetModuleList() as $module) {
|
||||
if ((IPS_GetModule($module)['Prefix'] ?? '') === 'MC') {
|
||||
$controls = array_merge($controls, IPS_GetInstanceListByModuleID($module));
|
||||
}
|
||||
}
|
||||
if (count($controls) !== 1 || !function_exists('MC_ReloadModule')) {
|
||||
throw new RuntimeException('Module Control nicht eindeutig.');
|
||||
}
|
||||
$locked = IPS_SemaphoreEnter('ENELIX.V4.ConfirmedFeedbackInstall', 3000);
|
||||
if (!$locked) {
|
||||
throw new RuntimeException('Installation bereits aktiv.');
|
||||
}
|
||||
|
||||
$report['files'] = v4ConfirmedFiles(__DIR__, '/var/lib/symcon/modules/Enelix-EMS');
|
||||
if (MC_ReloadModule($controls[0], 'Enelix-EMS') === false) {
|
||||
throw new RuntimeException('Bibliotheks-Reload fehlgeschlagen.');
|
||||
}
|
||||
|
||||
$afterManager = json_decode(IPS_GetConfiguration($manager), true, 128, JSON_THROW_ON_ERROR);
|
||||
$afterBattery = json_decode(IPS_GetConfiguration($battery), true, 128, JSON_THROW_ON_ERROR);
|
||||
foreach ([[$beforeManager, $afterManager], [$beforeBattery, $afterBattery]] as $pair) {
|
||||
foreach ($pair[0] as $key => $value) {
|
||||
if (!array_key_exists($key, $pair[1]) || $pair[1][$key] !== $value) {
|
||||
throw new RuntimeException('Bestehender Parameter beim Reload veraendert: ' . $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('ENELIX_GetV4BatterieRueckmeldung')) {
|
||||
$report['status'] = 'waiting_for_registration';
|
||||
echo "Dateien installiert. Modulregistrierung wird abgeschlossen; diesen Aufruf einmal wiederholen. Keine Stellfreigabe.\n";
|
||||
} else {
|
||||
if ($afterBattery['NetzfahrplanV4RueckmeldungKonfiguration'] !== $desired) {
|
||||
IPS_SetProperty($battery, 'NetzfahrplanV4RueckmeldungKonfiguration', $desired);
|
||||
$changedMapping = true;
|
||||
IPS_ApplyChanges($battery);
|
||||
}
|
||||
$finalManager = json_decode(IPS_GetConfiguration($manager), true, 128, JSON_THROW_ON_ERROR);
|
||||
$finalBattery = json_decode(IPS_GetConfiguration($battery), true, 128, JSON_THROW_ON_ERROR);
|
||||
foreach ([[$beforeManager, $finalManager], [$beforeBattery, $finalBattery]] as $pair) {
|
||||
foreach ($pair[0] as $key => $value) {
|
||||
if ($key !== 'NetzfahrplanV4RueckmeldungKonfiguration'
|
||||
&& (!array_key_exists($key, $pair[1]) || $pair[1][$key] !== $value)) {
|
||||
throw new RuntimeException('Bestehender Parameter unerwartet veraendert: ' . $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (($finalManager['NetzfahrplanAktiv'] ?? null) !== false
|
||||
|| ($finalManager['NetzfahrplanV4RegeltestErlaubt'] ?? null) !== false
|
||||
|| ($finalBattery['NetzfahrplanV4RegeltestErlaubt'] ?? null) !== false) {
|
||||
throw new RuntimeException('Unerwartete Stellfreigabe nach Installation.');
|
||||
}
|
||||
|
||||
$feedback = json_decode(ENELIX_GetV4BatterieRueckmeldung($battery), true, 32, JSON_THROW_ON_ERROR);
|
||||
$report['feedback'] = array_intersect_key($feedback, array_flip([
|
||||
'status', 'kind', 'method', 'reason', 'reasons', 'estimated', 'batteryW', 'gridW',
|
||||
'sourceOldestAt', 'sourceVariableOldestAt', 'confirmedAt', 'checkedAt', 'configHash',
|
||||
'deviceReadConfirmed', 'usableForTrial', 'canDispatch',
|
||||
]));
|
||||
$report['status'] = 'confirmed_feedback_installed_trial_disabled';
|
||||
$report['originalParametersPreserved'] = true;
|
||||
echo "OK: Geraeteabfrage bestaetigt die physischen Messwerte; virtual_split ist fuer den begrenzten Regeltest akzeptiert.\n";
|
||||
echo 'Rueckmeldung: ' . ($feedback['status'] ?? 'available')
|
||||
. '; Geraeteabfrage: ' . (($feedback['deviceReadConfirmed'] ?? false) ? 'bestaetigt' : 'nicht bestaetigt')
|
||||
. '; Modellanteil: ' . (($feedback['estimated'] ?? false) ? 'ja' : 'nein') . ".\n";
|
||||
if (isset($feedback['reason'])) {
|
||||
echo 'Aktueller Hinweis: ' . $feedback['reason'] . "\n";
|
||||
}
|
||||
echo "Beide Regeltestfreigaben AUS. Kein Stellbefehl ohne separat nachgewiesenen Geraete-Watchdog und Serverfreigabe.\n";
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
$report['status'] = 'needs_review';
|
||||
$report['reason'] = substr($error->getMessage(), 0, 260);
|
||||
if ($changedMapping && $oldMapping !== null) {
|
||||
try {
|
||||
IPS_SetProperty($battery, 'NetzfahrplanV4RueckmeldungKonfiguration', $oldMapping);
|
||||
IPS_ApplyChanges($battery);
|
||||
$report['mappingRestored'] = true;
|
||||
} catch (Throwable $ignored) {
|
||||
$report['mappingRestored'] = false;
|
||||
}
|
||||
}
|
||||
echo 'FEHLER: ' . $report['reason'] . "\nKeine V4-Stellfreigabe erteilt.\n";
|
||||
} finally {
|
||||
$report['finishedAt'] = gmdate('c');
|
||||
v4ConfirmedWrite(__DIR__ . '/INSTALL_RESULT.json', json_encode($report, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . "\n", 0644);
|
||||
if ($locked) {
|
||||
IPS_SemaphoreLeave('ENELIX.V4.ConfirmedFeedbackInstall');
|
||||
}
|
||||
echo 'CONFIRMED FEEDBACK INSTALL REPORT: ' . __DIR__ . "/INSTALL_RESULT.json\n";
|
||||
}
|
||||
Reference in New Issue
Block a user