diff --git a/packages/modules/common/utils/peak_filter.py b/packages/modules/common/utils/peak_filter.py index bfb26cfbad..5e2edde576 100644 --- a/packages/modules/common/utils/peak_filter.py +++ b/packages/modules/common/utils/peak_filter.py @@ -7,6 +7,12 @@ log = logging.getLogger(__name__) +# Vorwärtssprünge werden nur bis zu dieser Vielfachheit von allowed_deviation geglättet (siehe +# check_total_energy). Das deckt die Größenordnung eines einzelnen groben Zähler-/WR-Registertakts mit +# Puffer ab, lässt aber deutlich größere Sprünge (zB nach einer längeren Verbindungsunterbrechung) weiter +# sofort nach einer Bestätigung im nächsten Zyklus durch, statt sie über eine ggf. sehr lange Zeit zu glätten. +MAX_SMOOTHABLE_JUMP_FACTOR = 10 + class PeakFilter: def __init__(self, component_type: ComponentType, component_id: int, fault_state: FaultState): @@ -89,9 +95,21 @@ def check_total_energy( f"aktueller Zählerwert: {total_energy / 1000 }kWh. " "Warte einen Regelintervall.") elif allowed_deviation > 0 and (total_energy - previous_total_energy) > allowed_deviation: + jump = total_energy - previous_total_energy log.debug(f"PeakFilter: Unplausibler Zählerwert: {total_energy / 1000}kWh. " - f"Differenz zum vorherigen Wert: {total_energy - previous_total_energy}Wh. " + f"Differenz zum vorherigen Wert: {jump}Wh. " f"erlaubte Differenz: {round(allowed_deviation, 2)}Wh.") + if jump <= allowed_deviation * MAX_SMOOTHABLE_JUMP_FACTOR: + # Manche Zähler/Wechselrichter aktualisieren ihren Stand nicht kontinuierlich, sondern in + # Sprüngen. Ein einmaliges Verwerfen würde den vollen Sprung im nächsten Zyklus ungebremst + # durchreichen (der Sprung bleibt ja bestehen und ist dann keine Abweichung mehr). + # Stattdessen hier auf die erlaubte Differenz begrenzen und als neue Vergleichsbasis merken, + # sodass der Sprung über die folgenden Zyklen gleichmäßig nachgezogen wird. Nur für Sprünge + # bis MAX_SMOOTHABLE_JUMP_FACTOR × allowed_deviation, damit ein Sprung nach einer längeren + # Verbindungsunterbrechung (echter, großer Nachholbedarf) nicht ebenso ausgebremst wird. + checked_energy = previous_total_energy + allowed_deviation + prev_energy = checked_energy + return checked_energy, prev_energy elif (total_energy - previous_total_energy) < 0: if previous_total_energy > max_energy: log.debug("PeakFilter: Erholung nach unplausibel hohem Zählerwert erkannt. " diff --git a/packages/modules/common/utils/test_peak_filter.py b/packages/modules/common/utils/test_peak_filter.py index 9af278ece9..e0fc5f90f7 100644 --- a/packages/modules/common/utils/test_peak_filter.py +++ b/packages/modules/common/utils/test_peak_filter.py @@ -97,46 +97,84 @@ def test_check_values_valid(params): assert exp == params.expected_exported -def test_check_total_energy_forward_jump_then_accept_second_high_and_wait_after_backward_jump(): +def test_check_total_energy_forward_jump_within_bound_is_capped_and_catches_up_over_cycles(): fs = DummyFaultState() pf = PeakFilter(ComponentType.COUNTER, 1, fs) allowed_deviation = 10 max_energy = 1_000_000_000 - # Vorwaertssprung: erster Wert wird verworfen, aber als Vergleichsbasis gespeichert. - checked, previous = pf.check_total_energy(2000, 1000, allowed_deviation, max_energy) - assert checked is None - assert previous == 2000 + # Vorwaertssprung genau am Rand von MAX_SMOOTHABLE_JUMP_FACTOR * allowed_deviation (zB Zaehler/WR, + # der seinen Stand nur sprunghaft aktualisiert): wird auf die erlaubte Differenz begrenzt statt + # verworfen, und als neue Basis gemerkt. + checked, previous = pf.check_total_energy(1100, 1000, allowed_deviation, max_energy) + assert checked == 1010 + assert previous == 1010 - # Zweiter hoher Wert mit kleiner Differenz wird uebernommen. - checked, previous = pf.check_total_energy(2005, previous, allowed_deviation, max_energy) - assert checked == 2005 - assert previous == 2005 + # Rohwert bleibt stehen - naechster Zyklus holt schrittweise weiter auf. + checked, previous = pf.check_total_energy(1100, previous, allowed_deviation, max_energy) + assert checked == 1020 + assert previous == 1020 + + for _ in range(8): + checked, previous = pf.check_total_energy(1100, previous, allowed_deviation, max_energy) + assert checked == 1100 + assert previous == 1100 + + # Danach wird mit kleinen, plausiblen Differenzen normal weitergezaehlt. + checked, previous = pf.check_total_energy(1105, previous, allowed_deviation, max_energy) + assert checked == 1105 + assert previous == 1105 # Rueckwaertssprung: letzter gueltiger Hoechststand bleibt erhalten. - checked, previous = pf.check_total_energy(1900, previous, allowed_deviation, max_energy) - assert checked == 2005 - assert previous == 2005 + checked, previous = pf.check_total_energy(1000, previous, allowed_deviation, max_energy) + assert checked == 1105 + assert previous == 1105 # Unterhalb des Hoechststands wird weiterhin gewartet. - checked, previous = pf.check_total_energy(1990, previous, allowed_deviation, max_energy) - assert checked == 2005 - assert previous == 2005 + checked, previous = pf.check_total_energy(1090, previous, allowed_deviation, max_energy) + assert checked == 1105 + assert previous == 1105 # Naechster gleicher oder hoeherer Wert wird wieder zugelassen. - checked, previous = pf.check_total_energy(2005, previous, allowed_deviation, max_energy) - assert checked == 2005 - assert previous == 2005 + checked, previous = pf.check_total_energy(1105, previous, allowed_deviation, max_energy) + assert checked == 1105 + assert previous == 1105 + + checked, previous = pf.check_total_energy(1110, previous, allowed_deviation, max_energy) + assert checked == 1110 + assert previous == 1110 + + +def test_check_total_energy_forward_jump_beyond_bound_falls_back_to_reject_then_accept(): + fs = DummyFaultState() + pf = PeakFilter(ComponentType.COUNTER, 1, fs) + + allowed_deviation = 10 + max_energy = 1_000_000_000 + + # Sprung deutlich groesser als MAX_SMOOTHABLE_JUMP_FACTOR * allowed_deviation (zB nach einer + # laengeren Verbindungsunterbrechung mit echtem Nachholbedarf, nicht nur einem groben Zaehlertakt): + # wird NICHT geglaettet, sondern wie vor dieser Aenderung einmal verworfen und bei Bestaetigung im + # naechsten Zyklus sofort in voller Hoehe uebernommen. + checked, previous = pf.check_total_energy(2000, 1000, allowed_deviation, max_energy) + assert checked is None + assert previous == 2000 - checked, previous = pf.check_total_energy(2010, previous, allowed_deviation, max_energy) - assert checked == 2010 - assert previous == 2010 + checked, previous = pf.check_total_energy(2000, previous, allowed_deviation, max_energy) + assert checked == 2000 + assert previous == 2000 + + +def test_check_total_energy_recovery_after_too_high_previous_value(): + fs = DummyFaultState() + pf = PeakFilter(ComponentType.COUNTER, 1, fs) - # Der niedrigere Folgewert wird einmal verworfen und als neue Vergleichsbasis gesetzt. allowed_deviation = 10 max_energy = 3000 + # Vorheriger Wert lag ueber dem 50-Jahres-Limit - niedrigerer Folgewert wird einmal verworfen + # und als neue Vergleichsbasis gesetzt (unabhaengig vom Cap-Verhalten bei Vorwaertssprueingen). checked, previous = pf.check_total_energy(4000, 2900, allowed_deviation, max_energy) assert checked is None assert previous == 4000