mirror of
https://github.com/esphome/esphome.git
synced 2026-10-20 13:06:45 +00:00
[sensor][number] Build in_range with register_apply_condition
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import math
|
||||
import string
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ABOVE,
|
||||
CONF_ALL,
|
||||
CONF_ANY,
|
||||
CONF_AUTOMATION_ID,
|
||||
CONF_BELOW,
|
||||
CONF_CONDITION,
|
||||
CONF_COUNT,
|
||||
CONF_ELSE,
|
||||
@@ -352,6 +355,29 @@ class ApplyField:
|
||||
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
|
||||
|
||||
|
||||
def fill_in_range_bounds(config: ConfigType) -> ConfigType:
|
||||
"""Give an absent ``above`` or ``below`` the value NaN, which ``IN_RANGE_CHECK`` renders."""
|
||||
return {CONF_ABOVE: math.nan, CONF_BELOW: math.nan, **config}
|
||||
|
||||
|
||||
def _in_range_low(config: ConfigType, value: float) -> str:
|
||||
return "-INFINITY" if math.isnan(value) else str(cg.safe_exp(value))
|
||||
|
||||
|
||||
def _in_range_high(config: ConfigType, value: float) -> str:
|
||||
# With no lower bound, an unset upper bound stays NaN so the check is false, as before.
|
||||
if math.isnan(value):
|
||||
return "NAN" if math.isnan(config[CONF_ABOVE]) else "INFINITY"
|
||||
return str(cg.safe_exp(value))
|
||||
|
||||
|
||||
# ``above <= state <= below`` on the parent's float ``state``, after ``fill_in_range_bounds``.
|
||||
IN_RANGE_CHECK = ApplyCall(
|
||||
"state >= {} && {parent}->state <= {}",
|
||||
((CONF_ABOVE, cg.float_, _in_range_low), (CONF_BELOW, cg.float_, _in_range_high)),
|
||||
)
|
||||
|
||||
|
||||
def _config_lookup(config: ConfigType, key: str | tuple[str, ...]) -> Any:
|
||||
if isinstance(key, str):
|
||||
return config.get(key)
|
||||
|
||||
@@ -167,9 +167,6 @@ ValueRangeTrigger = number_ns.class_(
|
||||
)
|
||||
|
||||
# Conditions
|
||||
NumberInRangeCondition = number_ns.class_(
|
||||
"NumberInRangeCondition", automation.Condition
|
||||
)
|
||||
|
||||
NumberMode = number_ns.enum("NumberMode")
|
||||
|
||||
@@ -342,22 +339,13 @@ NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BELOW): cv.float_,
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
automation.fill_in_range_bounds,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"number.in_range", NumberInRangeCondition, NUMBER_IN_RANGE_CONDITION_SCHEMA
|
||||
automation.register_apply_condition(
|
||||
"number.in_range", NUMBER_IN_RANGE_CONDITION_SCHEMA, automation.IN_RANGE_CHECK
|
||||
)
|
||||
async def number_in_range_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
if (above := config.get(CONF_ABOVE)) is not None:
|
||||
cg.add(var.set_min(above))
|
||||
if (below := config.get(CONF_BELOW)) is not None:
|
||||
cg.add(var.set_max(below))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -26,27 +26,4 @@ class ValueRangeTrigger final : public Trigger<float>, public Component {
|
||||
TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
|
||||
};
|
||||
|
||||
template<typename... Ts> class NumberInRangeCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
NumberInRangeCondition(Number *parent) : parent_(parent) {}
|
||||
|
||||
void set_min(float min) { this->min_ = min; }
|
||||
void set_max(float max) { this->max_ = max; }
|
||||
bool check(const Ts &...x) override {
|
||||
const float state = this->parent_->state;
|
||||
if (std::isnan(this->min_)) {
|
||||
return state <= this->max_;
|
||||
} else if (std::isnan(this->max_)) {
|
||||
return state >= this->min_;
|
||||
} else {
|
||||
return this->min_ <= state && state <= this->max_;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
Number *parent_;
|
||||
float min_{NAN};
|
||||
float max_{NAN};
|
||||
};
|
||||
|
||||
} // namespace esphome::number
|
||||
|
||||
@@ -311,7 +311,6 @@ CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
|
||||
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
|
||||
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
|
||||
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
|
||||
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
|
||||
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
|
||||
RoundFilter = sensor_ns.class_("RoundFilter", Filter)
|
||||
RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
|
||||
@@ -1107,22 +1106,13 @@ SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BELOW): cv.float_,
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
automation.fill_in_range_bounds,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"sensor.in_range", SensorInRangeCondition, SENSOR_IN_RANGE_CONDITION_SCHEMA
|
||||
automation.register_apply_condition(
|
||||
"sensor.in_range", SENSOR_IN_RANGE_CONDITION_SCHEMA, automation.IN_RANGE_CHECK
|
||||
)
|
||||
async def sensor_in_range_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
if (above := config.get(CONF_ABOVE)) is not None:
|
||||
cg.add(var.set_min(above))
|
||||
if (below := config.get(CONF_BELOW)) is not None:
|
||||
cg.add(var.set_max(below))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
def validate_ntc_calibration_parameter(value):
|
||||
|
||||
@@ -58,27 +58,4 @@ class ValueRangeTrigger final : public Trigger<float>, public Component {
|
||||
TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
|
||||
};
|
||||
|
||||
template<typename... Ts> class SensorInRangeCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
SensorInRangeCondition(Sensor *parent) : parent_(parent) {}
|
||||
|
||||
void set_min(float min) { this->min_ = min; }
|
||||
void set_max(float max) { this->max_ = max; }
|
||||
bool check(const Ts &...x) override {
|
||||
const float state = this->parent_->state;
|
||||
if (std::isnan(this->min_)) {
|
||||
return state <= this->max_;
|
||||
} else if (std::isnan(this->max_)) {
|
||||
return state >= this->min_;
|
||||
} else {
|
||||
return this->min_ <= state && state <= this->max_;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
Sensor *parent_;
|
||||
float min_{NAN};
|
||||
float max_{NAN};
|
||||
};
|
||||
|
||||
} // namespace esphome::sensor
|
||||
|
||||
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||
import pytest
|
||||
|
||||
from esphome.automation import (
|
||||
IN_RANGE_CHECK,
|
||||
ApplyAction,
|
||||
ApplyCall,
|
||||
ApplyCondition,
|
||||
@@ -25,6 +26,7 @@ from esphome.automation import (
|
||||
build_parent_callback_automation,
|
||||
build_trigger_automations,
|
||||
build_trigger_callback,
|
||||
fill_in_range_bounds,
|
||||
has_non_synchronous_actions,
|
||||
literal_with_length,
|
||||
maybe_simple_id,
|
||||
@@ -1608,3 +1610,25 @@ async def test_nested_repeat_lambda_parameters_are_distinct() -> None:
|
||||
in text
|
||||
)
|
||||
assert "uint32_t iteration, uint32_t iteration)" not in text
|
||||
|
||||
|
||||
def _render_in_range(config: dict) -> tuple[str, str]:
|
||||
config = fill_in_range_bounds(config)
|
||||
(above_key, _, low), (below_key, _, high) = IN_RANGE_CHECK.members
|
||||
return low(config, config[above_key]), high(config, config[below_key])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected"),
|
||||
[
|
||||
({"above": 10.0}, ("10.0f", "INFINITY")),
|
||||
({"below": 30.0}, ("-INFINITY", "30.0f")),
|
||||
({"above": 10.0, "below": 30.0}, ("10.0f", "30.0f")),
|
||||
# A NaN bound counted as unset before; with no lower bound a NaN upper bound stays false.
|
||||
({"above": float("nan"), "below": 30.0}, ("-INFINITY", "30.0f")),
|
||||
({"above": 10.0, "below": float("nan")}, ("10.0f", "INFINITY")),
|
||||
({"above": float("nan"), "below": float("nan")}, ("-INFINITY", "NAN")),
|
||||
],
|
||||
)
|
||||
def test_in_range_check_renders_bounds(config: dict, expected: tuple[str, str]) -> None:
|
||||
assert _render_in_range(config) == expected
|
||||
|
||||
Reference in New Issue
Block a user