feat(workflow): publish verified forecast controls and consolidated docs
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.
This commit is contained in:
dh
2026-10-08 10:02:58 +00:00
parent af69151165
commit 74a2d6c685
82 changed files with 5054 additions and 617 deletions
@@ -9,6 +9,28 @@ 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', '{}');
@@ -21,28 +43,41 @@ trait BatterieNetzfahrplanV4RueckmeldungTrait
$this->SetBuffer('V4ConfirmedFeedback', '');
$this->SetBuffer('V4ConfirmedFeedbackConfig', '');
$this->SetBuffer('V4ConfirmedFeedbackMono', '');
$configured = !in_array(trim($this->ReadPropertyString('NetzfahrplanV4RueckmeldungKonfiguration')), ['', '{}'], true);
$this->SetTimerInterval('NetzfahrplanV4RueckmeldungAktualisieren', $configured ? 1000 : 0);
$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);
}
/** No transport I/O under the battery control lock. Original sample times never move. */
/** Strictly local: never call the Manager from a Manager -> battery cache read. */
private function v4BatterieRueckmeldung(): array
{
$json = $this->ReadPropertyString('NetzfahrplanV4RueckmeldungKonfiguration');
$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 = json_decode($json, true, 32, JSON_THROW_ON_ERROR);
if (($config['batteryInstanceId'] ?? null) !== $this->InstanceID
|| !in_array($config['managerId'] ?? 0, $this->zugeordneteManagerIDs(), true)) {
$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']);
@@ -56,11 +91,25 @@ trait BatterieNetzfahrplanV4RueckmeldungTrait
/** Independent acquisition; never waits behind another read or takes the actuator lock. */
private function aktualisiereV4Rueckmeldung(): void
{
$json=$this->ReadPropertyString('NetzfahrplanV4RueckmeldungKonfiguration');
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');
$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;
@@ -102,9 +151,21 @@ trait BatterieNetzfahrplanV4RueckmeldungTrait
}
}
if (!is_array($result)) throw new \RuntimeException('feedback_snapshot_unavailable');
$this->SetBuffer('V4ConfirmedFeedback',json_encode($result,JSON_THROW_ON_ERROR));
$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;