mirror of
https://github.com/esphome/esphome.git
synced 2026-01-20 18:09:10 -07:00
Compare commits
11 Commits
lazy_callb
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c0f43db9e | ||
|
|
6edecd3d45 | ||
|
|
055c00f1ac | ||
|
|
7dc40881e2 | ||
|
|
b04373687e | ||
|
|
b89c127f62 | ||
|
|
47dc5d0a1f | ||
|
|
21886dd3ac | ||
|
|
85a5a26519 | ||
|
|
79ccacd6d6 | ||
|
|
e2319ba651 |
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
CONF_ABOVE,
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_ALPHA,
|
||||
CONF_BASELINE,
|
||||
CONF_BELOW,
|
||||
CONF_CALIBRATION,
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -38,7 +39,6 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
@@ -107,7 +107,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.util import Registry
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -574,38 +574,56 @@ async def lambda_filter_to_code(config, filter_id):
|
||||
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
|
||||
|
||||
|
||||
DELTA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_VALUE): cv.positive_float,
|
||||
cv.Optional(CONF_TYPE, default="absolute"): cv.one_of(
|
||||
"absolute", "percentage", lower=True
|
||||
),
|
||||
}
|
||||
def validate_delta_value(value):
|
||||
if isinstance(value, str) and value.endswith("%"):
|
||||
# Check it's a well-formed percentage, but return the string as-is
|
||||
try:
|
||||
cv.positive_float(value[:-1])
|
||||
return value
|
||||
except cv.Invalid as exc:
|
||||
raise cv.Invalid("Malformed delta % value") from exc
|
||||
return cv.positive_float(value)
|
||||
|
||||
|
||||
# This ideally would be done with `cv.maybe_simple_value` but it doesn't seem to respect the default for min_value.
|
||||
DELTA_SCHEMA = cv.Any(
|
||||
cv.All(
|
||||
{
|
||||
# Ideally this would be 'default=float("inf")' but it doesn't translate well to C++
|
||||
cv.Optional(CONF_MAX_VALUE): validate_delta_value,
|
||||
cv.Optional(CONF_MIN_VALUE, default="0.0"): validate_delta_value,
|
||||
cv.Optional(CONF_BASELINE): cv.templatable(cv.float_),
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_MAX_VALUE, CONF_MIN_VALUE),
|
||||
),
|
||||
validate_delta_value,
|
||||
)
|
||||
|
||||
|
||||
def validate_delta(config):
|
||||
try:
|
||||
value = cv.positive_float(config)
|
||||
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "absolute"})
|
||||
except cv.Invalid:
|
||||
pass
|
||||
try:
|
||||
value = cv.percentage(config)
|
||||
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "percentage"})
|
||||
except cv.Invalid:
|
||||
pass
|
||||
raise cv.Invalid("Delta filter requires a positive number or percentage value.")
|
||||
def _get_delta(value):
|
||||
if isinstance(value, str):
|
||||
assert value.endswith("%")
|
||||
return 0.0, float(value[:-1])
|
||||
return value, 0.0
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("delta", DeltaFilter, cv.Any(DELTA_SCHEMA, validate_delta))
|
||||
@FILTER_REGISTRY.register("delta", DeltaFilter, DELTA_SCHEMA)
|
||||
async def delta_filter_to_code(config, filter_id):
|
||||
percentage = config[CONF_TYPE] == "percentage"
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_VALUE],
|
||||
percentage,
|
||||
)
|
||||
# The config could be just the min_value, or it could be a dict.
|
||||
max = MockObj("std::numeric_limits<float>::infinity()"), 0
|
||||
if isinstance(config, dict):
|
||||
min = _get_delta(config[CONF_MIN_VALUE])
|
||||
if CONF_MAX_VALUE in config:
|
||||
max = _get_delta(config[CONF_MAX_VALUE])
|
||||
else:
|
||||
min = _get_delta(config)
|
||||
var = cg.new_Pvariable(filter_id, *min, *max)
|
||||
if isinstance(config, dict) and (baseline_lambda := config.get(CONF_BASELINE)):
|
||||
baseline = await cg.process_lambda(
|
||||
baseline_lambda, [(float, "x")], return_type=float
|
||||
)
|
||||
cg.add(var.set_baseline(baseline))
|
||||
return var
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("or", OrFilter, validate_filters)
|
||||
|
||||
@@ -291,22 +291,27 @@ optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
// DeltaFilter
|
||||
DeltaFilter::DeltaFilter(float delta, bool percentage_mode)
|
||||
: delta_(delta), current_delta_(delta), last_value_(NAN), percentage_mode_(percentage_mode) {}
|
||||
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
|
||||
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}
|
||||
|
||||
void DeltaFilter::set_baseline(float (*fn)(float)) { this->baseline_ = fn; }
|
||||
|
||||
optional<float> DeltaFilter::new_value(float value) {
|
||||
if (std::isnan(value)) {
|
||||
if (std::isnan(this->last_value_)) {
|
||||
return {};
|
||||
} else {
|
||||
return this->last_value_ = value;
|
||||
}
|
||||
// Always yield the first value.
|
||||
if (std::isnan(this->last_value_)) {
|
||||
this->last_value_ = value;
|
||||
return value;
|
||||
}
|
||||
float diff = fabsf(value - this->last_value_);
|
||||
if (std::isnan(this->last_value_) || (diff > 0.0f && diff >= this->current_delta_)) {
|
||||
if (this->percentage_mode_) {
|
||||
this->current_delta_ = fabsf(value * this->delta_);
|
||||
}
|
||||
return this->last_value_ = value;
|
||||
// calculate min and max using the linear equation
|
||||
float ref = this->baseline_(this->last_value_);
|
||||
float min = fabsf(this->min_a0_ + ref * this->min_a1_);
|
||||
float max = fabsf(this->max_a0_ + ref * this->max_a1_);
|
||||
float delta = fabsf(value - ref);
|
||||
// if there is no reference, e.g. for the first value, just accept this one,
|
||||
// otherwise accept only if within range.
|
||||
if (delta > min && delta <= max) {
|
||||
this->last_value_ = value;
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -452,15 +452,21 @@ class HeartbeatFilter : public Filter, public Component {
|
||||
|
||||
class DeltaFilter : public Filter {
|
||||
public:
|
||||
explicit DeltaFilter(float delta, bool percentage_mode);
|
||||
explicit DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1);
|
||||
|
||||
void set_baseline(float (*fn)(float));
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
float delta_;
|
||||
float current_delta_;
|
||||
// These values represent linear equations for the min and max values but in practice only one of a0 and a1 will be
|
||||
// non-zero Each limit is calculated as fabs(a0 + value * a1)
|
||||
|
||||
float min_a0_, min_a1_, max_a0_, max_a1_;
|
||||
// default baseline is the previous value
|
||||
float (*baseline_)(float) = [](float last_value) { return last_value; };
|
||||
|
||||
float last_value_{NAN};
|
||||
bool percentage_mode_;
|
||||
};
|
||||
|
||||
class OrFilter : public Filter {
|
||||
|
||||
@@ -79,13 +79,17 @@ async def setup_conf(config, key):
|
||||
|
||||
async def to_code(config):
|
||||
# Request specific WiFi listeners based on which sensors are configured
|
||||
# Each sensor needs its own listener slot - call request for EACH sensor
|
||||
|
||||
# SSID and BSSID use WiFiConnectStateListener
|
||||
if CONF_SSID in config or CONF_BSSID in config:
|
||||
wifi.request_wifi_connect_state_listener()
|
||||
for key in (CONF_SSID, CONF_BSSID):
|
||||
if key in config:
|
||||
wifi.request_wifi_connect_state_listener()
|
||||
|
||||
# IP address and DNS use WiFiIPStateListener
|
||||
if CONF_IP_ADDRESS in config or CONF_DNS_ADDRESS in config:
|
||||
wifi.request_wifi_ip_state_listener()
|
||||
for key in (CONF_IP_ADDRESS, CONF_DNS_ADDRESS):
|
||||
if key in config:
|
||||
wifi.request_wifi_ip_state_listener()
|
||||
|
||||
# Scan results use WiFiScanResultsListener
|
||||
if CONF_SCAN_RESULTS in config:
|
||||
|
||||
@@ -348,6 +348,8 @@ template<typename T> class FixedVector {
|
||||
|
||||
size_t size() const { return size_; }
|
||||
bool empty() const { return size_ == 0; }
|
||||
size_t capacity() const { return capacity_; }
|
||||
bool full() const { return size_ == capacity_; }
|
||||
|
||||
/// Access element without bounds checking (matches std::vector behavior)
|
||||
/// Caller must ensure index is valid (i < size())
|
||||
@@ -1343,30 +1345,16 @@ template<typename... X> class LazyCallbackManager;
|
||||
*
|
||||
* Memory overhead comparison (32-bit systems):
|
||||
* - CallbackManager: 12 bytes (empty std::vector)
|
||||
* - LazyCallbackManager: 4 bytes (nullptr pointer)
|
||||
*
|
||||
* Uses plain pointer instead of unique_ptr to avoid template instantiation overhead.
|
||||
* The class is explicitly non-copyable/non-movable for Rule of Five compliance.
|
||||
* - LazyCallbackManager: 4 bytes (nullptr unique_ptr)
|
||||
*
|
||||
* @tparam Ts The arguments for the callbacks, wrapped in void().
|
||||
*/
|
||||
template<typename... Ts> class LazyCallbackManager<void(Ts...)> {
|
||||
public:
|
||||
LazyCallbackManager() = default;
|
||||
/// Destructor - clean up allocated CallbackManager if any.
|
||||
/// In practice this never runs (entities live for device lifetime) but included for correctness.
|
||||
~LazyCallbackManager() { delete this->callbacks_; }
|
||||
|
||||
// Non-copyable and non-movable (entities are never copied or moved)
|
||||
LazyCallbackManager(const LazyCallbackManager &) = delete;
|
||||
LazyCallbackManager &operator=(const LazyCallbackManager &) = delete;
|
||||
LazyCallbackManager(LazyCallbackManager &&) = delete;
|
||||
LazyCallbackManager &operator=(LazyCallbackManager &&) = delete;
|
||||
|
||||
/// Add a callback to the list. Allocates the underlying CallbackManager on first use.
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = new CallbackManager<void(Ts...)>();
|
||||
this->callbacks_ = make_unique<CallbackManager<void(Ts...)>>();
|
||||
}
|
||||
this->callbacks_->add(std::move(callback));
|
||||
}
|
||||
@@ -1388,7 +1376,7 @@ template<typename... Ts> class LazyCallbackManager<void(Ts...)> {
|
||||
void operator()(Ts... args) { this->call(args...); }
|
||||
|
||||
protected:
|
||||
CallbackManager<void(Ts...)> *callbacks_{nullptr};
|
||||
std::unique_ptr<CallbackManager<void(Ts...)>> callbacks_;
|
||||
};
|
||||
|
||||
/// Helper class to deduplicate items in a series of values.
|
||||
|
||||
@@ -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