mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 22:06:12 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
049c0e7aa0 | ||
|
|
c5999377ba | ||
|
|
110f7d27c7 | ||
|
|
db9c2db761 | ||
|
|
c10081416b | ||
|
|
10f817c1a8 | ||
|
|
c248b7375c | ||
|
|
8fcd37c2ef | ||
|
|
f4d640bbd2 | ||
|
|
1c79e65e4f | ||
|
|
3901ccccba | ||
|
|
20d0ab9ed0 |
+30
-1
@@ -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,
|
||||
@@ -361,6 +364,25 @@ class ApplyField:
|
||||
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
|
||||
|
||||
|
||||
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``; register with ``IN_RANGE_DEFAULTS``.
|
||||
IN_RANGE_CHECK = ApplyCall(
|
||||
"state >= {} && {parent}->state <= {}",
|
||||
((CONF_ABOVE, cg.float_, _in_range_low), (CONF_BELOW, cg.float_, _in_range_high)),
|
||||
)
|
||||
IN_RANGE_DEFAULTS = {CONF_ABOVE: math.nan, CONF_BELOW: math.nan}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyBytes:
|
||||
"""A byte payload key forwarded as ``target``, whose ``{}`` stands for ``data, len``.
|
||||
@@ -675,7 +697,11 @@ async def _render_check(
|
||||
|
||||
|
||||
def register_apply_condition(
|
||||
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
|
||||
name: str,
|
||||
schema: cv.Schema,
|
||||
check: str | ApplyCall,
|
||||
id_key: str = CONF_ID,
|
||||
defaults: ConfigType | None = None,
|
||||
) -> None:
|
||||
"""Register a condition that is one expression on its parent, with no C++ class.
|
||||
|
||||
@@ -684,6 +710,7 @@ def register_apply_condition(
|
||||
against config values, all of which must be present. Write ``== false`` to negate.
|
||||
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=``
|
||||
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
|
||||
``defaults`` fills absent keys at code generation only, so the validated config is unchanged.
|
||||
Generates one static predicate and an ``ApplyCondition`` templated on it.
|
||||
"""
|
||||
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
|
||||
@@ -698,6 +725,8 @@ def register_apply_condition(
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
if defaults:
|
||||
config = {**defaults, **config}
|
||||
parent = await _apply_parent(config, id_key)
|
||||
lambda_args = _apply_lambda_args(args)
|
||||
values = _apply_values(config, members)
|
||||
|
||||
@@ -168,11 +168,6 @@ ValueRangeTrigger = number_ns.class_(
|
||||
"ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component
|
||||
)
|
||||
|
||||
# Conditions
|
||||
NumberInRangeCondition = number_ns.class_(
|
||||
"NumberInRangeCondition", automation.Condition
|
||||
)
|
||||
|
||||
NumberMode = number_ns.enum("NumberMode")
|
||||
|
||||
# Schema default that also matches the C++ initializer in number_traits.h; codegen
|
||||
@@ -347,17 +342,12 @@ NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@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,
|
||||
defaults=automation.IN_RANGE_DEFAULTS,
|
||||
)
|
||||
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)
|
||||
|
||||
cg.add_optional(var.set_min, config.get(CONF_ABOVE))
|
||||
cg.add_optional(var.set_max, config.get(CONF_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
|
||||
|
||||
@@ -315,7 +315,6 @@ ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
|
||||
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
|
||||
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", 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)
|
||||
@@ -1112,17 +1111,12 @@ SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@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,
|
||||
defaults=automation.IN_RANGE_DEFAULTS,
|
||||
)
|
||||
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)
|
||||
|
||||
cg.add_optional(var.set_min, config.get(CONF_ABOVE))
|
||||
cg.add_optional(var.set_max, config.get(CONF_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
|
||||
|
||||
@@ -11,6 +11,8 @@ from esphome.automation import (
|
||||
ACTION_REGISTRY,
|
||||
CONF_CHOICE_ID,
|
||||
CONF_CHOOSE,
|
||||
IN_RANGE_CHECK,
|
||||
IN_RANGE_DEFAULTS,
|
||||
ApplyAction,
|
||||
ApplyBytes,
|
||||
ApplyCall,
|
||||
@@ -988,10 +990,11 @@ async def _run_apply_condition(
|
||||
args: list[tuple[object, str]] | None = None,
|
||||
platform: str = "esp32",
|
||||
id_key: str = CONF_ID,
|
||||
defaults: dict[str, object] | None = None,
|
||||
) -> RegistryEntry:
|
||||
"""Register an apply condition and run its builder with the given config."""
|
||||
_, conditions = registries
|
||||
register_apply_condition("my.check", None, check, id_key=id_key)
|
||||
register_apply_condition("my.check", None, check, id_key=id_key, defaults=defaults)
|
||||
return await _run_entry(conditions["my.check"], config, args, platform, id_key)
|
||||
|
||||
|
||||
@@ -1486,6 +1489,21 @@ async def test_apply_condition_compares_config_value(
|
||||
await _run_apply_condition(registries, check, {})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_condition_defaults_fill_absent_keys(
|
||||
registries: tuple[Registry, Registry], mock_cg: MockCodegen
|
||||
) -> None:
|
||||
config = {"above": 10.0}
|
||||
await _run_apply_condition(
|
||||
registries, IN_RANGE_CHECK, config, defaults=IN_RANGE_DEFAULTS
|
||||
)
|
||||
assert (
|
||||
f"return ::{PARENT_OBJ}->state >= 10.0f && ::{PARENT_OBJ}->state <= INFINITY;"
|
||||
in _apply_definition(mock_cg)
|
||||
)
|
||||
assert config == {"above": 10.0}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("platform", "value", "expected"),
|
||||
@@ -2007,6 +2025,28 @@ async def test_nested_repeat_lambda_parameters_are_distinct() -> None:
|
||||
assert "uint32_t iteration, uint32_t iteration)" not in text
|
||||
|
||||
|
||||
def _render_in_range(config: dict) -> tuple[str, str]:
|
||||
config = {**IN_RANGE_DEFAULTS, **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
|
||||
|
||||
|
||||
def test_maybe_simple_id_schema_extract() -> None:
|
||||
validator = maybe_simple_id({cv.Required(CONF_ID): cv.string})
|
||||
_, key = validator(SCHEMA_EXTRACT)
|
||||
|
||||
Reference in New Issue
Block a user