feat(workflow): publish verified forecast controls and consolidated docs
Tests / test (push) Successful in 1m1s
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:
@@ -0,0 +1,111 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user