Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion packages/modules/common/utils/peak_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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. "
Expand Down
82 changes: 60 additions & 22 deletions packages/modules/common/utils/test_peak_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading