fix(manager): isolate V4 active import sources from legacy counter
This commit is contained in:
@@ -13,7 +13,7 @@ final class NetzfahrplanV4BetriebsdatenTest extends TestCase
|
||||
{
|
||||
public function testOfflineInputScenarios(): void
|
||||
{
|
||||
self::assertCount(21, NativeV4Scenarios::run());
|
||||
self::assertCount(24, NativeV4Scenarios::run());
|
||||
}
|
||||
|
||||
public function testSenderCannotChangeLivePlanOrActuators(): void
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixEMS\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once __DIR__ . '/../libs/NetzfahrplanV4Bezugszaehler.php';
|
||||
require_once __DIR__ . '/fixtures/V4BezugszaehlerScenarios.php';
|
||||
|
||||
final class NetzfahrplanV4BezugszaehlerTest extends TestCase
|
||||
{
|
||||
public function testReadOnlyCounterSources(): void
|
||||
{
|
||||
self::assertCount(20, V4BezugszaehlerScenarios::run());
|
||||
}
|
||||
|
||||
public function testNoLegacyMeterOrActuatorFallback(): void
|
||||
{
|
||||
$source = file_get_contents(__DIR__ . '/../libs/NetzfahrplanV4Bezugszaehler.php');
|
||||
foreach (['SetValue(', 'IPS_SetProperty(', 'RequestAction(', 'AC_Set', '53476'] as $forbidden) {
|
||||
self::assertStringNotContainsString($forbidden, $source);
|
||||
}
|
||||
$trait = file_get_contents(__DIR__ . '/../libs/ManagerNetzfahrplanV4Trait.php');
|
||||
self::assertStringNotContainsString("'NetzbezugEnergieVariableID'", $trait);
|
||||
self::assertStringContainsString("RegisterPropertyString('NetzfahrplanV4BezugszaehlerQuellen', '[]')", $trait);
|
||||
}
|
||||
}
|
||||
Vendored
+17
-2
@@ -17,6 +17,10 @@ final class NativeV4Scenarios
|
||||
$m = ['NetzleistungVariableID' => 1, 'NetzbezugEnergieVariableID' => 9,
|
||||
'Netzleistungsfaktor' => 1.0, 'MesswertMaxAlter' => 60, 'Lastspitzenmodus' => 1,
|
||||
'Lastspitzengrenze' => 25000.0, 'EinspeisebegrenzungAktiv' => true, 'Einspeisegrenze' => 25000.0];
|
||||
$m['V4BezugszaehlerQuellen'] = [
|
||||
['VariableID' => 6, 'ElternID' => 99, 'Ident' => 'Energy_0', 'FaktorZuKWh' => 1.0, 'Messgroesse' => 'WirkenergieBezug'],
|
||||
['VariableID' => 7, 'ElternID' => 99, 'Ident' => 'Energy_1', 'FaktorZuKWh' => 1.0, 'Messgroesse' => 'WirkenergieBezug'],
|
||||
];
|
||||
$a = [['ID' => 'ev', 'Nennkapazitaet_kWh' => 161.44, 'Nutzkapazitaet_kWh' => 161.44,
|
||||
'MaxLadeleistung_kW' => 39.0, 'MaxEntladeleistung_kW' => 30.0, 'SOCVariableID' => 2, 'LeistungVariableID' => 5]];
|
||||
$c = [['InstanzID' => 42, 'LadezustandVariableID' => 2, 'MaxLadeleistungVariableID' => 3,
|
||||
@@ -24,7 +28,8 @@ final class NativeV4Scenarios
|
||||
'ReserveLadezustand' => 15.0, 'MindestLadezustand' => 3.0, 'MesswertMaxAlter' => 30,
|
||||
'Verfuegbar' => true, 'Batteriemanagement' => 2, 'EmpfangenAm' => $now]];
|
||||
$read = static function (int $id) use ($now): array {
|
||||
return ['value' => [1 => -5332.0, 2 => 19.0, 3 => 39000.0, 4 => 35000.0][$id], 'updated' => $now];
|
||||
return ['value' => [1 => -5332.0, 2 => 19.0, 3 => 39000.0, 4 => 35000.0, 6 => 3992.203, 7 => 0.0][$id],
|
||||
'updated' => $now, 'parentID' => 99, 'ident' => [6 => 'Energy_0', 7 => 'Energy_1'][$id] ?? 'other'];
|
||||
};
|
||||
$check = static function (bool $ok): void { if (!$ok) { throw new RuntimeException('Assertion failed'); } };
|
||||
$fail = static function (callable $fn): void {
|
||||
@@ -97,7 +102,7 @@ final class NativeV4Scenarios
|
||||
$check($build()['operation']['batteries'][0]['gridCharging'] === false);
|
||||
$m['NetzladenErlaubt'] = true; $check($build($m)['operation']['batteries'][0]['gridCharging'] === true);
|
||||
};
|
||||
$ev = ['version' => 1, 'meterId' => 'symcon:9', 'measuredAt' => gmdate('c', $now),
|
||||
$ev = ['version' => 1, 'meterId' => \Belevo\EnelixEMS\NetzfahrplanV4Bezugszaehler::identitaet($m['V4BezugszaehlerQuellen']), 'measuredAt' => gmdate('c', $now),
|
||||
'measuredPeaks' => ['2026-10' => ['kw' => 18.4, 'source' => 'meter_month_register']],
|
||||
'quarterPast' => ['start' => '2026-10-01T12:00:00Z', 'measuredSeconds' => 300, 'importKwh' => 0.5]];
|
||||
$cases['actual_meter_evidence_used'] = static function () use ($build, $ev, $check): void {
|
||||
@@ -117,6 +122,16 @@ final class NativeV4Scenarios
|
||||
$cases['deterministic_retry_event_id'] = static function () use ($build, $check): void {
|
||||
$check($build()['operation']['eventId'] === $build()['operation']['eventId']);
|
||||
};
|
||||
$cases['legacy_meter_identity_not_accepted'] = static function () use ($build, $ev, $fail): void {
|
||||
$ev['meterId'] = 'symcon:9'; $fail(static fn() => $build(null, null, null, null, $ev));
|
||||
};
|
||||
$cases['missing_v4_sources_never_use_legacy'] = static function () use ($build, $m, $fail): void {
|
||||
unset($m['V4BezugszaehlerQuellen']); $fail(static fn() => $build($m));
|
||||
};
|
||||
$cases['counter_snapshot_is_not_billing_evidence'] = static function () use ($build, $check): void {
|
||||
$v = $build(); $check($v['bezugszaehler']['totalKwh'] === 3992.203
|
||||
&& !$v['bezugszaehler']['billingEvidence'] && (array) $v['operation']['measuredPeaks'] === []);
|
||||
};
|
||||
$passed = [];
|
||||
foreach ($cases as $name => $fn) { $fn(); $passed[] = $name; }
|
||||
return $passed;
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixEMS\Tests;
|
||||
|
||||
use Belevo\EnelixEMS\NetzfahrplanV4Bezugszaehler as Meter;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/** Offline only: injected readings, no Symcon, archive, network or actuator access. */
|
||||
final class V4BezugszaehlerScenarios
|
||||
{
|
||||
public static function run(): array
|
||||
{
|
||||
$now = 1790874000;
|
||||
$sources = [
|
||||
['VariableID' => 59607, 'ElternID' => 11490, 'Ident' => 'Energy_0', 'FaktorZuKWh' => 1.0, 'Messgroesse' => 'WirkenergieBezug'],
|
||||
['VariableID' => 26620, 'ElternID' => 11490, 'Ident' => 'Energy_1', 'FaktorZuKWh' => 1.0, 'Messgroesse' => 'WirkenergieBezug'],
|
||||
];
|
||||
$samples = [
|
||||
59607 => ['value' => 3992.203, 'updated' => $now, 'parentID' => 11490, 'ident' => 'Energy_0'],
|
||||
26620 => ['value' => 0.0, 'updated' => $now, 'parentID' => 11490, 'ident' => 'Energy_1'],
|
||||
];
|
||||
$check = static function (bool $value): void { if (!$value) { throw new RuntimeException('Assertion failed'); } };
|
||||
$reject = static function (callable $fn): void {
|
||||
try { $fn(); } catch (InvalidArgumentException $e) { return; }
|
||||
throw new RuntimeException('Expected invalid source/sample rejection');
|
||||
};
|
||||
$snapshot = static function ($ss = null, $mm = null) use ($sources, $samples, $now): array {
|
||||
$mm = $mm ?? $samples;
|
||||
return Meter::lesen($ss ?? $sources, static fn(int $id) => $mm[$id] ?? null, $now);
|
||||
};
|
||||
$cases = [];
|
||||
$cases['zero_t2_is_valid_but_not_history'] = static function () use ($snapshot, $check): void {
|
||||
$v = $snapshot();
|
||||
$check(abs($v['totalKwh'] - 3992.203) < 1e-9 && !$v['historyComplete'] && !$v['billingEvidence']);
|
||||
$check(!isset($v['measuredPeaks']) && !isset($v['quarterPast']));
|
||||
};
|
||||
$cases['both_tariffs_are_summed'] = static function () use ($snapshot, $samples, $check): void {
|
||||
$samples[26620]['value'] = 27.25;
|
||||
$check(abs($snapshot(null, $samples)['totalKwh'] - 4019.453) < 1e-9);
|
||||
};
|
||||
$cases['zero_total_not_replaced'] = static function () use ($snapshot, $samples, $check): void {
|
||||
$samples[59607]['value'] = 0;
|
||||
$check($snapshot(null, $samples)['totalKwh'] === 0.0);
|
||||
};
|
||||
$cases['source_order_does_not_change_identity'] = static function () use ($sources, $check): void {
|
||||
$check(Meter::identitaet($sources) === Meter::identitaet(array_reverse($sources)));
|
||||
};
|
||||
$cases['changed_factor_invalidates_identity'] = static function () use ($sources, $check): void {
|
||||
$old = Meter::identitaet($sources); $sources[0]['FaktorZuKWh'] = 0.001;
|
||||
$check($old !== Meter::identitaet($sources));
|
||||
};
|
||||
$cases['changed_variable_invalidates_identity'] = static function () use ($sources, $check): void {
|
||||
$old = Meter::identitaet($sources); $sources[0]['VariableID'] = 12345;
|
||||
$check($old !== Meter::identitaet($sources));
|
||||
};
|
||||
$cases['no_fallback_to_legacy_source'] = static function () use ($snapshot, $reject): void { $reject(static fn() => $snapshot([])); };
|
||||
$cases['duplicate_variable_rejected'] = static function () use ($sources, $snapshot, $reject): void {
|
||||
$sources[1] = $sources[0]; $reject(static fn() => $snapshot($sources));
|
||||
};
|
||||
$cases['different_meter_boundary_rejected'] = static function () use ($sources, $snapshot, $reject): void {
|
||||
$sources[1]['ElternID'] = 48065; $reject(static fn() => $snapshot($sources));
|
||||
};
|
||||
$cases['reactive_quantity_rejected'] = static function () use ($sources, $snapshot, $reject): void {
|
||||
$sources[0]['Messgroesse'] = 'Blindenergie'; $reject(static fn() => $snapshot($sources));
|
||||
};
|
||||
$cases['explicit_conversion_required'] = static function () use ($sources, $snapshot, $reject): void {
|
||||
unset($sources[0]['FaktorZuKWh']); $reject(static fn() => $snapshot($sources));
|
||||
};
|
||||
$cases['missing_tariff_not_zero'] = static function () use ($snapshot, $samples, $reject): void {
|
||||
unset($samples[26620]); $reject(static fn() => $snapshot(null, $samples));
|
||||
};
|
||||
$cases['stale_zero_tariff_rejected'] = static function () use ($snapshot, $samples, $reject): void {
|
||||
$samples[26620]['updated'] -= 61; $reject(static fn() => $snapshot(null, $samples));
|
||||
};
|
||||
$cases['future_reading_rejected'] = static function () use ($snapshot, $samples, $reject): void {
|
||||
$samples[26620]['updated']++; $reject(static fn() => $snapshot(null, $samples));
|
||||
};
|
||||
$cases['wrong_ident_rejected'] = static function () use ($snapshot, $samples, $reject): void {
|
||||
$samples[59607]['ident'] = 'Energy_6'; $reject(static fn() => $snapshot(null, $samples));
|
||||
};
|
||||
$cases['wrong_parent_rejected'] = static function () use ($snapshot, $samples, $reject): void {
|
||||
$samples[59607]['parentID'] = 48065; $reject(static fn() => $snapshot(null, $samples));
|
||||
};
|
||||
$cases['invalid_numbers_rejected'] = static function () use ($snapshot, $samples, $reject): void {
|
||||
foreach ([false, '3992.203', NAN, INF, -1.0] as $bad) {
|
||||
$samples[59607]['value'] = $bad; $reject(static fn() => $snapshot(null, $samples));
|
||||
}
|
||||
};
|
||||
$cases['skewed_tariff_observations_rejected'] = static function () use ($snapshot, $samples, $reject): void {
|
||||
$samples[59607]['updated'] -= 3; $reject(static fn() => $snapshot(null, $samples));
|
||||
};
|
||||
$cases['concurrent_update_in_same_second_rejected'] = static function () use ($sources, $samples, $now, $reject): void {
|
||||
$calls = 0;
|
||||
$read = static function (int $id) use ($samples, &$calls): array {
|
||||
$v = $samples[$id]; if (++$calls > 2) { $v['value'] += 0.1; } return $v;
|
||||
};
|
||||
$reject(static fn() => Meter::lesen($sources, $read, $now));
|
||||
};
|
||||
$cases['observation_not_fake_exact_billing_time'] = static function () use ($snapshot, $samples, $now, $check): void {
|
||||
$samples[59607]['updated'] -= 1;
|
||||
$v = $snapshot(null, $samples);
|
||||
$check($v['sourceObservationFrom'] === gmdate('c', $now - 1));
|
||||
$check($v['sourceObservationUntil'] === gmdate('c', $now) && $v['observedAt'] === gmdate('c', $now));
|
||||
$check(!$v['billingEvidence']);
|
||||
};
|
||||
$passed = [];
|
||||
foreach ($cases as $name => $fn) { $fn(); $passed[] = $name; }
|
||||
return $passed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user