From f75f11b550b50089c57b25c1f939da3457ec9744 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Oct 2025 19:57:29 -1000 Subject: [PATCH] add --- esphome/components/sensor/__init__.py | 27 ++++--- .../fixtures/sensor_filters_batch_window.yaml | 58 ++++++++++++++ .../fixtures/sensor_filters_ring_buffer.yaml | 75 +++++++++++++++++++ 3 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 tests/integration/fixtures/sensor_filters_batch_window.yaml create mode 100644 tests/integration/fixtures/sensor_filters_ring_buffer.yaml diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index a7a92d3968..0538531354 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -453,7 +453,7 @@ async def skip_initial_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) -@FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA) +@FILTER_REGISTRY.register("min", Filter, MIN_SCHEMA) async def min_filter_to_code(config, filter_id): window_size: int = config[CONF_WINDOW_SIZE] send_every: int = config[CONF_SEND_EVERY] @@ -463,10 +463,11 @@ async def min_filter_to_code(config, filter_id): # Saves 99.98% memory for large windows (e.g., 20KB → 4 bytes for window_size=5000) if window_size == send_every: # Use streaming filter - O(1) memory instead of O(n) - rhs = cg.new_Pvariable(StreamingMinFilter, window_size, send_first_at) - return cg.Pvariable(filter_id, rhs) + rhs = StreamingMinFilter.new(window_size, send_first_at) + return cg.Pvariable(filter_id, rhs, StreamingMinFilter) # Use sliding window filter - maintains ring buffer - return cg.new_Pvariable(filter_id, window_size, send_every, send_first_at) + rhs = MinFilter.new(window_size, send_every, send_first_at) + return cg.Pvariable(filter_id, rhs, MinFilter) MAX_SCHEMA = cv.All( @@ -481,7 +482,7 @@ MAX_SCHEMA = cv.All( ) -@FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA) +@FILTER_REGISTRY.register("max", Filter, MAX_SCHEMA) async def max_filter_to_code(config, filter_id): window_size: int = config[CONF_WINDOW_SIZE] send_every: int = config[CONF_SEND_EVERY] @@ -489,9 +490,10 @@ async def max_filter_to_code(config, filter_id): # Optimization: Use streaming filter for batch windows (window_size == send_every) if window_size == send_every: - rhs = cg.new_Pvariable(StreamingMaxFilter, window_size, send_first_at) - return cg.Pvariable(filter_id, rhs) - return cg.new_Pvariable(filter_id, window_size, send_every, send_first_at) + rhs = StreamingMaxFilter.new(window_size, send_first_at) + return cg.Pvariable(filter_id, rhs, StreamingMaxFilter) + rhs = MaxFilter.new(window_size, send_every, send_first_at) + return cg.Pvariable(filter_id, rhs, MaxFilter) SLIDING_AVERAGE_SCHEMA = cv.All( @@ -508,7 +510,7 @@ SLIDING_AVERAGE_SCHEMA = cv.All( @FILTER_REGISTRY.register( "sliding_window_moving_average", - SlidingWindowMovingAverageFilter, + Filter, SLIDING_AVERAGE_SCHEMA, ) async def sliding_window_moving_average_filter_to_code(config, filter_id): @@ -518,9 +520,10 @@ async def sliding_window_moving_average_filter_to_code(config, filter_id): # Optimization: Use streaming filter for batch windows (window_size == send_every) if window_size == send_every: - rhs = cg.new_Pvariable(StreamingMovingAverageFilter, window_size, send_first_at) - return cg.Pvariable(filter_id, rhs) - return cg.new_Pvariable(filter_id, window_size, send_every, send_first_at) + rhs = StreamingMovingAverageFilter.new(window_size, send_first_at) + return cg.Pvariable(filter_id, rhs, StreamingMovingAverageFilter) + rhs = SlidingWindowMovingAverageFilter.new(window_size, send_every, send_first_at) + return cg.Pvariable(filter_id, rhs, SlidingWindowMovingAverageFilter) EXPONENTIAL_AVERAGE_SCHEMA = cv.All( diff --git a/tests/integration/fixtures/sensor_filters_batch_window.yaml b/tests/integration/fixtures/sensor_filters_batch_window.yaml new file mode 100644 index 0000000000..58a254c215 --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_batch_window.yaml @@ -0,0 +1,58 @@ +esphome: + name: test-batch-window-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensor that we'll use to publish values +sensor: + - platform: template + name: "Source Sensor" + id: source_sensor + accuracy_decimals: 2 + + # Batch window filters (window_size == send_every) - use streaming filters + - platform: copy + source_id: source_sensor + name: "Min Sensor" + id: min_sensor + filters: + - min: + window_size: 5 + send_every: 5 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Max Sensor" + id: max_sensor + filters: + - max: + window_size: 5 + send_every: 5 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Moving Avg Sensor" + id: moving_avg_sensor + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 5 + send_first_at: 1 + +# Button to trigger publishing test values +button: + - platform: template + name: "Publish Values Button" + id: publish_button + on_press: + - lambda: |- + // Publish 10 values: 1.0, 2.0, ..., 10.0 + for (int i = 1; i <= 10; i++) { + id(source_sensor).publish_state(float(i)); + } diff --git a/tests/integration/fixtures/sensor_filters_ring_buffer.yaml b/tests/integration/fixtures/sensor_filters_ring_buffer.yaml new file mode 100644 index 0000000000..0d603ee9ce --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_ring_buffer.yaml @@ -0,0 +1,75 @@ +esphome: + name: test-sliding-window-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensor that we'll use to publish values +sensor: + - platform: template + name: "Source Sensor" + id: source_sensor + accuracy_decimals: 2 + + # ACTUAL sliding window filters (window_size != send_every) - use ring buffers + # Window of 5, send every 2 values + - platform: copy + source_id: source_sensor + name: "Sliding Min Sensor" + id: sliding_min_sensor + filters: + - min: + window_size: 5 + send_every: 2 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Sliding Max Sensor" + id: sliding_max_sensor + filters: + - max: + window_size: 5 + send_every: 2 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Sliding Median Sensor" + id: sliding_median_sensor + filters: + - median: + window_size: 5 + send_every: 2 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Sliding Moving Avg Sensor" + id: sliding_moving_avg_sensor + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 2 + send_first_at: 1 + +# Button to trigger publishing test values +button: + - platform: template + name: "Publish Values Button" + id: publish_button + on_press: + - lambda: |- + // Publish 10 values: 1.0, 2.0, ..., 10.0 + // With window_size=5, send_every=2, send_first_at=1: + // - Output at position 1: window=[1], min=1, max=1, median=1, avg=1 + // - Output at position 3: window=[1,2,3], min=1, max=3, median=2, avg=2 + // - Output at position 5: window=[1,2,3,4,5], min=1, max=5, median=3, avg=3 + // - Output at position 7: window=[3,4,5,6,7], min=3, max=7, median=5, avg=5 + // - Output at position 9: window=[5,6,7,8,9], min=5, max=9, median=7, avg=7 + for (int i = 1; i <= 10; i++) { + id(source_sensor).publish_state(float(i)); + }