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; } }