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:
@@ -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
|
||||
|
||||
180
tests/integration/fixtures/sensor_filters_delta.yaml
Normal file
180
tests/integration/fixtures/sensor_filters_delta.yaml
Normal 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
|
||||
163
tests/integration/test_sensor_filters_delta.py
Normal file
163
tests/integration/test_sensor_filters_delta.py
Normal 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']}"
|
||||
)
|
||||
Reference in New Issue
Block a user