mirror of
https://github.com/esphome/esphome.git
synced 2026-09-14 08:38:39 +00:00
Merge remote-tracking branch 'origin/sensor/throttle-with-priority-nan-specialize' into integration
This commit is contained in:
@@ -172,10 +172,16 @@ def validate_gpio_pin(pin):
|
||||
exc,
|
||||
)
|
||||
else:
|
||||
# Throw an exception if used for a pin that would not have resulted
|
||||
# in a validation error anyway!
|
||||
# `ignore_pin_validation_error` only suppresses an error raised by the
|
||||
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
|
||||
# numbers). If that didn't raise, the option is a no-op -- warn so the
|
||||
# user can clean it up, but don't block the build.
|
||||
if ignore_pin_validation_warning:
|
||||
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
|
||||
_LOGGER.warning(
|
||||
"GPIO%d has no validation errors to ignore; "
|
||||
"remove `ignore_pin_validation_error: true` from this pin.",
|
||||
pin[CONF_NUMBER],
|
||||
)
|
||||
|
||||
return pin
|
||||
|
||||
|
||||
@@ -275,6 +275,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
||||
ThrottleWithPriorityFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityFilter", ValueListFilter
|
||||
)
|
||||
ThrottleWithPriorityNanFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityNanFilter", Filter
|
||||
)
|
||||
TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
|
||||
TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
|
||||
TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase)
|
||||
@@ -656,9 +659,18 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
THROTTLE_WITH_PRIORITY_SCHEMA,
|
||||
)
|
||||
async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
if not isinstance(config[CONF_VALUE], list):
|
||||
config[CONF_VALUE] = [config[CONF_VALUE]]
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in config[CONF_VALUE]]
|
||||
values = config[CONF_VALUE]
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
# Specialize the common "NaN-only" case (the schema default when the user
|
||||
# omits `value:`) to avoid the TemplatableFn<float> array + NaN lambda the
|
||||
# generic ValueListFilter path requires. Behavior is identical: NaN sensor
|
||||
# readings always bypass the throttle.
|
||||
if values and all(isinstance(v, float) and math.isnan(v) for v in values):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleWithPriorityNanFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in values]
|
||||
return cg.new_Pvariable(
|
||||
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
|
||||
)
|
||||
|
||||
@@ -269,6 +269,18 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co
|
||||
return {};
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityNanFilter
|
||||
ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs)
|
||||
: min_time_between_inputs_(min_time_between_inputs) {}
|
||||
optional<float> ThrottleWithPriorityNanFilter::new_value(float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// DeltaFilter
|
||||
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) {}
|
||||
|
||||
@@ -399,6 +399,19 @@ template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N>
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
/// Specialization of ThrottleWithPriorityFilter for the common "prioritize NaN"
|
||||
/// case: skips the TemplatableFn<float> array + lambda and inlines the check.
|
||||
class ThrottleWithPriorityNanFilter : public Filter {
|
||||
public:
|
||||
explicit ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
// Base class for timeout filters - contains common loop logic
|
||||
class TimeoutFilterBase : public Filter, public Component {
|
||||
public:
|
||||
|
||||
@@ -8,10 +8,16 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import VARIANTS
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
from esphome.components.esp32 import VARIANT_ESP32, VARIANTS
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT
|
||||
from esphome.components.esp32.gpio import validate_gpio_pin
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, PlatformFramework
|
||||
from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_IGNORE_PIN_VALIDATION_ERROR,
|
||||
CONF_NUMBER,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
@@ -149,6 +155,73 @@ def test_execute_from_psram_p4_sdkconfig(
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
|
||||
|
||||
def test_ignore_pin_validation_error_on_clean_pin_warns(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A pin that passes validation but sets `ignore_pin_validation_error: true`
|
||||
should log a warning nudging the user to remove the flag, and not raise."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
|
||||
with caplog.at_level("WARNING"):
|
||||
result = validate_gpio_pin(pin)
|
||||
|
||||
assert result[CONF_NUMBER] == 4
|
||||
assert "GPIO4 has no validation errors to ignore" in caplog.text
|
||||
|
||||
|
||||
def test_ignore_pin_validation_error_on_dirty_pin_suppresses(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A pin that fails validation with `ignore_pin_validation_error: true` should
|
||||
log the suppression warning and not raise (existing behavior)."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
# GPIO6 is a flash pin on ESP32 -> pin_validation raises cv.Invalid
|
||||
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
|
||||
with caplog.at_level("WARNING"):
|
||||
result = validate_gpio_pin(pin)
|
||||
|
||||
assert result[CONF_NUMBER] == 6
|
||||
assert "Ignoring validation error on pin 6" in caplog.text
|
||||
|
||||
|
||||
def test_dirty_pin_without_ignore_flag_raises(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A pin that fails validation without the ignore flag should still raise."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
|
||||
with pytest.raises(cv.Invalid, match="flash interface"):
|
||||
validate_gpio_pin(pin)
|
||||
|
||||
|
||||
def test_clean_pin_without_ignore_flag_does_not_warn(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A clean pin without the ignore flag should pass silently."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
|
||||
)
|
||||
|
||||
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
|
||||
with caplog.at_level("WARNING"):
|
||||
result = validate_gpio_pin(pin)
|
||||
|
||||
assert result[CONF_NUMBER] == 4
|
||||
assert "has no validation errors to ignore" not in caplog.text
|
||||
|
||||
|
||||
def test_execute_from_psram_disabled_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
Reference in New Issue
Block a user