Files
Enelix-EMS/libs/NetzfahrplanV4RueckmeldungEinrichtung.php
T
dh 74a2d6c685
Tests / test (push) Successful in 1m1s
feat(workflow): publish verified forecast controls and consolidated docs
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.
2026-10-08 10:02:58 +00:00

112 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
use InvalidArgumentException;
require_once __DIR__ . '/NetzfahrplanV4Messaufnahme.php';
require_once __DIR__ . '/NetzfahrplanV4Rueckmeldung.php';
/** Pure feedback binding for an already verified automatic physical capture. */
final class NetzfahrplanV4RueckmeldungEinrichtung
{
/**
* The caller verifies the current capture against the automatic plant setup,
* including supported physical source parents. Custom mappings stay separate.
* $assets is AnlagenBatterien; $controllers contains every assigned active
* battery controller with its existing public measurement properties only.
* No device read, configuration write, watchdog proof or permission is created.
*/
public static function erstellen(
array $capture,
string $installationId,
int $managerId,
int $batteryId,
array $assets,
array $controllers
): array {
$capture = NetzfahrplanV4Messaufnahme::configuration($capture);
if (($capture['installationId'] ?? null) !== $installationId
|| ($capture['managerId'] ?? null) !== $managerId
|| ($capture['accounting']['storageMode'] ?? null) !== 'physical_only'
|| $capture['extraSources'] !== []) {
throw new InvalidArgumentException('feedback_auto_capture_binding_invalid');
}
if (array_keys($assets) !== array_keys(array_values($assets)) || count($assets) !== 1 || !is_array($assets[0])
|| array_keys($controllers) !== array_keys(array_values($controllers)) || count($controllers) !== 1 || !is_array($controllers[0])) {
throw new InvalidArgumentException('feedback_auto_battery_assignment_ambiguous');
}
$asset = $assets[0];
$controller = $controllers[0];
$assetId = $asset['ID'] ?? null;
if (!is_string($assetId) || !preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/D', trim($assetId))
|| ($asset['Kopplung'] ?? null) !== 'ac' || ($asset['Virtuell'] ?? false) !== false
|| ($asset['SDLAktiv'] ?? false) !== false || ($controller['InstanzID'] ?? null) !== $batteryId
|| ($controller['Aktiv'] ?? true) !== true) {
throw new InvalidArgumentException('feedback_auto_battery_binding_invalid');
}
foreach (['LeistungVariableID', 'SOCVariableID'] as $key) {
if (!is_int($asset[$key] ?? null) || $asset[$key] < 1 || $asset[$key] > 99999) {
throw new InvalidArgumentException('feedback_auto_battery_measurements_missing');
}
}
if (($controller['IstleistungVariableID'] ?? null) !== $asset['LeistungVariableID']
|| ($controller['LadezustandVariableID'] ?? null) !== $asset['SOCVariableID']
|| $asset['SOCVariableID'] === $asset['LeistungVariableID']) {
throw new InvalidArgumentException('feedback_auto_controller_measurements_differ');
}
if ((!is_int($asset['Leistungsfaktor'] ?? null) && !is_float($asset['Leistungsfaktor'] ?? null))
|| (float) $asset['Leistungsfaktor'] !== 1.0) {
throw new InvalidArgumentException('feedback_auto_controller_units_differ');
}
$maxAge = $controller['MesswertMaxAlter'] ?? null;
if (!is_int($maxAge) || $maxAge < 1) {
throw new InvalidArgumentException('feedback_auto_controller_freshness_invalid');
}
$maxAge = min(60, $maxAge);
$sources = [];
$roles = [];
foreach ($capture['accounting']['sources'] as $source) {
if ($source['dependsOn'] !== [] || !in_array($source['role'], ['grid', 'pv', 'physical_storage'], true)) {
throw new InvalidArgumentException('feedback_auto_derived_source_refused');
}
if (!in_array($source['role'], ['grid', 'physical_storage'], true)) {
continue;
}
$roles[$source['role']] = ($roles[$source['role']] ?? 0) + 1;
// The ordinary battery module consumes these same variables as W.
if ($source['factorToW'] !== 1.0) {
throw new InvalidArgumentException('feedback_auto_controller_units_differ');
}
if (($source['role'] === 'grid' && $source['variableId'] !== ($controller['NetzleistungVariableID'] ?? null))
|| ($source['role'] === 'physical_storage' && $source['variableId'] !== $asset['LeistungVariableID'])
|| $source['variableId'] === $asset['SOCVariableID']) {
throw new InvalidArgumentException('feedback_auto_source_binding_invalid');
}
$expectedKey = $source['role'] === 'grid' ? 'grid' : 'battery_' . substr(hash('sha256', trim($assetId)), 0, 24);
if ($source['key'] !== $expectedKey) {
throw new InvalidArgumentException('feedback_auto_source_identity_changed');
}
$maxAge = min($maxAge, $source['maxAgeSeconds']);
$sources[] = [
'key' => $source['key'], 'role' => $source['role'] === 'grid' ? 'grid' : 'physical',
'variableId' => $source['variableId'], 'parentId' => $source['parentId'],
'ident' => $source['ident'], 'factorToW' => $source['factorToW'],
];
}
if (($roles['grid'] ?? 0) !== 1 || ($roles['physical_storage'] ?? 0) !== 1) {
throw new InvalidArgumentException('feedback_auto_physical_sources_ambiguous');
}
return NetzfahrplanV4Rueckmeldung::configuration([
'version' => 1, 'installationId' => $installationId, 'assetId' => trim($assetId),
'managerId' => $managerId, 'batteryInstanceId' => $batteryId,
'mode' => 'physical_sum', 'allowEstimatedForTrial' => false,
'maxAgeSeconds' => $maxAge,
'maxSkewSeconds' => min(30, $maxAge, $capture['accounting']['maxSkewSeconds']),
'idleToleranceW' => 0.0, 'trackingToleranceW' => 0.0, 'sources' => $sources,
]);
}
}