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.
112 lines
5.9 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|