1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

Add the max_delta filter (#12605)

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
polyfloyd
2026-01-21 00:58:47 +01:00
committed by GitHub
parent 6edecd3d45
commit 3c0f43db9e
6 changed files with 420 additions and 46 deletions

View File

@@ -121,6 +121,8 @@ sensor:
min_value: -10.0
- debounce: 0.1s
- delta: 5.0
- delta:
max_value: 2%
- exponential_moving_average:
alpha: 0.1
send_every: 15

View File

@@ -0,0 +1,180 @@
esphome:
name: test-delta-filters
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: DEBUG
sensor:
- platform: template
name: "Source Sensor 1"
id: source_sensor_1
accuracy_decimals: 1
- platform: template
name: "Source Sensor 2"
id: source_sensor_2
accuracy_decimals: 1
- platform: template
name: "Source Sensor 3"
id: source_sensor_3
accuracy_decimals: 1
- platform: template
name: "Source Sensor 4"
id: source_sensor_4
accuracy_decimals: 1
- platform: copy
source_id: source_sensor_1
name: "Filter Min"
id: filter_min
filters:
- delta:
min_value: 10
- platform: copy
source_id: source_sensor_2
name: "Filter Max"
id: filter_max
filters:
- delta:
max_value: 10
- platform: copy
source_id: source_sensor_3
id: test_3_baseline
filters:
- median:
window_size: 6
send_every: 1
send_first_at: 1
- platform: copy
source_id: source_sensor_3
name: "Filter Baseline Max"
id: filter_baseline_max
filters:
- delta:
max_value: 10
baseline: !lambda return id(test_3_baseline).state;
- platform: copy
source_id: source_sensor_4
name: "Filter Zero Delta"
id: filter_zero_delta
filters:
- delta: 0
script:
- id: test_filter_min
then:
- sensor.template.publish:
id: source_sensor_1
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: 5.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: 12.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: 8.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: -2.0
- id: test_filter_max
then:
- sensor.template.publish:
id: source_sensor_2
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: 5.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: 40.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: 10.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: -40.0 # Filtered out
- id: test_filter_baseline_max
then:
- sensor.template.publish:
id: source_sensor_3
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 2.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 3.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 40.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 20.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 20.0
- id: test_filter_zero_delta
then:
- sensor.template.publish:
id: source_sensor_4
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_4
state: 1.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_4
state: 2.0
button:
- platform: template
name: "Test Filter Min"
id: btn_filter_min
on_press:
- script.execute: test_filter_min
- platform: template
name: "Test Filter Max"
id: btn_filter_max
on_press:
- script.execute: test_filter_max
- platform: template
name: "Test Filter Baseline Max"
id: btn_filter_baseline_max
on_press:
- script.execute: test_filter_baseline_max
- platform: template
name: "Test Filter Zero Delta"
id: btn_filter_zero_delta
on_press:
- script.execute: test_filter_zero_delta

View File

@@ -0,0 +1,163 @@
"""Test sensor DeltaFilter functionality."""
from __future__ import annotations
import asyncio
from aioesphomeapi import ButtonInfo, EntityState, SensorState
import pytest
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_sensor_filters_delta(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
loop = asyncio.get_running_loop()
sensor_values: dict[str, list[float]] = {
"filter_min": [],
"filter_max": [],
"filter_baseline_max": [],
"filter_zero_delta": [],
}
filter_min_done = loop.create_future()
filter_max_done = loop.create_future()
filter_baseline_max_done = loop.create_future()
filter_zero_delta_done = loop.create_future()
def on_state(state: EntityState) -> None:
if not isinstance(state, SensorState) or state.missing_state:
return
sensor_name = key_to_sensor.get(state.key)
if sensor_name not in sensor_values:
return
sensor_values[sensor_name].append(state.state)
# Check completion conditions
if (
sensor_name == "filter_min"
and len(sensor_values[sensor_name]) == 3
and not filter_min_done.done()
):
filter_min_done.set_result(True)
elif (
sensor_name == "filter_max"
and len(sensor_values[sensor_name]) == 3
and not filter_max_done.done()
):
filter_max_done.set_result(True)
elif (
sensor_name == "filter_baseline_max"
and len(sensor_values[sensor_name]) == 4
and not filter_baseline_max_done.done()
):
filter_baseline_max_done.set_result(True)
elif (
sensor_name == "filter_zero_delta"
and len(sensor_values[sensor_name]) == 2
and not filter_zero_delta_done.done()
):
filter_zero_delta_done.set_result(True)
async with (
run_compiled(yaml_config),
api_client_connected() as client,
):
# Get entities and build key mapping
entities, _ = await client.list_entities_services()
key_to_sensor = build_key_to_entity_mapping(
entities,
{
"filter_min": "Filter Min",
"filter_max": "Filter Max",
"filter_baseline_max": "Filter Baseline Max",
"filter_zero_delta": "Filter Zero Delta",
},
)
# Set up initial state helper with all entities
initial_state_helper = InitialStateHelper(entities)
# Subscribe to state changes with wrapper
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
# Wait for initial states
await initial_state_helper.wait_for_initial_states()
# Find all buttons
button_name_map = {
"Test Filter Min": "filter_min",
"Test Filter Max": "filter_max",
"Test Filter Baseline Max": "filter_baseline_max",
"Test Filter Zero Delta": "filter_zero_delta",
}
buttons = {}
for entity in entities:
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
buttons[button_name_map[entity.name]] = entity.key
assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}"
# Test 1: Min
sensor_values["filter_min"].clear()
client.button_command(buttons["filter_min"])
try:
await asyncio.wait_for(filter_min_done, timeout=2.0)
except TimeoutError:
pytest.fail(f"Test 1 timed out. Values: {sensor_values['filter_min']}")
expected = [1.0, 12.0, -2.0]
assert sensor_values["filter_min"] == pytest.approx(expected), (
f"Test 1 failed: expected {expected}, got {sensor_values['filter_min']}"
)
# Test 2: Max
sensor_values["filter_max"].clear()
client.button_command(buttons["filter_max"])
try:
await asyncio.wait_for(filter_max_done, timeout=2.0)
except TimeoutError:
pytest.fail(f"Test 2 timed out. Values: {sensor_values['filter_max']}")
expected = [1.0, 5.0, 10.0]
assert sensor_values["filter_max"] == pytest.approx(expected), (
f"Test 2 failed: expected {expected}, got {sensor_values['filter_max']}"
)
# Test 3: Baseline Max
sensor_values["filter_baseline_max"].clear()
client.button_command(buttons["filter_baseline_max"])
try:
await asyncio.wait_for(filter_baseline_max_done, timeout=2.0)
except TimeoutError:
pytest.fail(
f"Test 3 timed out. Values: {sensor_values['filter_baseline_max']}"
)
expected = [1.0, 2.0, 3.0, 20.0]
assert sensor_values["filter_baseline_max"] == pytest.approx(expected), (
f"Test 3 failed: expected {expected}, got {sensor_values['filter_baseline_max']}"
)
# Test 4: Zero Delta
sensor_values["filter_zero_delta"].clear()
client.button_command(buttons["filter_zero_delta"])
try:
await asyncio.wait_for(filter_zero_delta_done, timeout=2.0)
except TimeoutError:
pytest.fail(
f"Test 4 timed out. Values: {sensor_values['filter_zero_delta']}"
)
expected = [1.0, 2.0]
assert sensor_values["filter_zero_delta"] == pytest.approx(expected), (
f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}"
)