mirror of
https://github.com/esphome/esphome.git
synced 2026-10-10 05:23:07 +00:00
[sensor] Merge the radar timeout and throttle filters into one (#20118)
This commit is contained in:
@@ -34,83 +34,35 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_LIGHTBULB,
|
||||
),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
@@ -123,29 +75,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
|
||||
@@ -34,84 +34,36 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_LIGHTBULB,
|
||||
unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor
|
||||
),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
@@ -124,29 +76,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
|
||||
@@ -46,41 +46,17 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
|
||||
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ACCOUNT_GROUP,
|
||||
),
|
||||
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_HUMAN_GREETING_PROXIMITY,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ACCOUNT_SWITCH,
|
||||
),
|
||||
}
|
||||
@@ -92,85 +68,37 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_X): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ALPHA_X_BOX_OUTLINE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
cv.Optional(CONF_Y): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_ALPHA_Y_BOX_OUTLINE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_SPEED,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_SPEEDOMETER_SLOW,
|
||||
unit_of_measurement=UNIT_MILLIMETER_PER_SECOND,
|
||||
),
|
||||
cv.Optional(CONF_ANGLE): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
accuracy_decimals=1,
|
||||
),
|
||||
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_DISTANCE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
cv.Optional(CONF_RESOLUTION): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
),
|
||||
@@ -183,41 +111,17 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_ACCOUNT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_ACCOUNT,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_MAP_MARKER_ACCOUNT,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -30,44 +30,32 @@ from .const import (
|
||||
|
||||
DEPENDENCIES = ["ld6002b"]
|
||||
|
||||
# The ld2450 defaults for a streamed value: hold the last reading for a second so a
|
||||
# dropped frame does not read as absence, then rate-limit what reaches the frontend.
|
||||
_VALUE_SENSOR_FILTERS = [
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
]
|
||||
|
||||
TARGET_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_X): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_Y): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_Z): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_DOPPLER_INDEX): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
filters=_VALUE_SENSOR_FILTERS,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CLUSTER_ID): sensor.sensor_schema(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
@@ -43,6 +44,7 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
@@ -222,6 +224,13 @@ def validate_send_first_at(value):
|
||||
FILTER_REGISTRY = Registry()
|
||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
|
||||
|
||||
# Default for streamed readings (radar sensors): hold the last value for a second so a dropped
|
||||
# frame doesn't read as absence, then rate limit. Codegen turns the pair into one TimeoutThrottleFilter.
|
||||
TIMEOUT_THROTTLE_FILTERS = [
|
||||
{"timeout": {"timeout": cv.TimePeriod(milliseconds=1000), "value": "last"}},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
]
|
||||
|
||||
|
||||
def validate_datapoint(value):
|
||||
if isinstance(value, dict):
|
||||
@@ -293,6 +302,7 @@ ThrottleWithPriorityNanFilter = sensor_ns.class_(
|
||||
TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
|
||||
TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
|
||||
TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase)
|
||||
TimeoutThrottleFilter = sensor_ns.class_("TimeoutThrottleFilter", TimeoutFilterLast)
|
||||
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter)
|
||||
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter)
|
||||
DeltaFilter = sensor_ns.class_("DeltaFilter", Filter)
|
||||
@@ -683,6 +693,12 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
def _is_nan_only(values: Any) -> bool:
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
return bool(values) and all(isinstance(v, float) and math.isnan(v) for v in values)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"throttle_with_priority",
|
||||
ThrottleWithPriorityFilter,
|
||||
@@ -696,7 +712,7 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
# 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):
|
||||
if _is_nan_only(values):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleWithPriorityNanFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
@@ -941,6 +957,43 @@ async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
def _timeout_throttle_period(
|
||||
first: ConfigType, second: ConfigType
|
||||
) -> cv.TimePeriod | None:
|
||||
"""Period of a `timeout` (value `last`) directly followed by a NaN only `throttle_with_priority`."""
|
||||
timeout = first.get("timeout")
|
||||
throttle = second.get("throttle_with_priority")
|
||||
if not isinstance(timeout, dict) or not isinstance(throttle, dict):
|
||||
return None
|
||||
if timeout[CONF_VALUE] != "last" or timeout[CONF_TIMEOUT] != throttle[CONF_TIMEOUT]:
|
||||
return None
|
||||
if not _is_nan_only(throttle[CONF_VALUE]):
|
||||
return None
|
||||
return timeout[CONF_TIMEOUT]
|
||||
|
||||
|
||||
async def _build_chain_filters(config: list[ConfigType]) -> list:
|
||||
"""Like build_filters, but merges the radar `timeout` + `throttle_with_priority` pair into one filter."""
|
||||
filters = []
|
||||
i = 0
|
||||
while i < len(config):
|
||||
if (
|
||||
i + 1 < len(config)
|
||||
and (period := _timeout_throttle_period(config[i], config[i + 1]))
|
||||
is not None
|
||||
):
|
||||
filter_id = config[i][CONF_TYPE_ID].copy()
|
||||
filter_id.type = TimeoutThrottleFilter
|
||||
var = cg.new_Pvariable(filter_id, period)
|
||||
await cg.register_component(var, {})
|
||||
filters.append(var)
|
||||
i += 2
|
||||
continue
|
||||
filters.append(await cg.build_registry_entry(FILTER_REGISTRY, config[i]))
|
||||
i += 1
|
||||
return filters
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_VALUE, "add_on_state_callback", [(float, "x")]
|
||||
@@ -979,7 +1032,7 @@ async def setup_sensor_core_(var, config):
|
||||
cg.add(var.set_force_update(True))
|
||||
if config.get(CONF_FILTERS): # must exist and not be empty
|
||||
cg.add_define("USE_SENSOR_FILTER")
|
||||
filters = await build_filters(config[CONF_FILTERS])
|
||||
filters = await _build_chain_filters(config[CONF_FILTERS])
|
||||
cg.add(var.set_filters(filters))
|
||||
|
||||
CORE.add_job(_build_sensor_automations, var, config)
|
||||
|
||||
@@ -13,6 +13,17 @@ namespace esphome::sensor {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sensor.filter");
|
||||
|
||||
/// Shared pass check for throttle_with_priority (NaN only): passes and stamps `last_input` when the
|
||||
/// period has elapsed, on the first value, or for NaN.
|
||||
static bool throttle_nan_passes(uint32_t &last_input, uint32_t period, float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (last_input == 0 || now - last_input >= period || std::isnan(value)) {
|
||||
last_input = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter
|
||||
void Filter::input(float value) {
|
||||
ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value);
|
||||
@@ -266,11 +277,8 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co
|
||||
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;
|
||||
if (throttle_nan_passes(this->last_input_, this->min_time_between_inputs_, value))
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -355,6 +363,23 @@ optional<float> TimeoutFilterConfigured::new_value(float value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// TimeoutThrottleFilter
|
||||
optional<float> TimeoutThrottleFilter::new_value(float value) {
|
||||
TimeoutFilterLast::new_value(value);
|
||||
if (throttle_nan_passes(this->last_input_, this->time_period_, value))
|
||||
return value;
|
||||
return {};
|
||||
}
|
||||
|
||||
void TimeoutThrottleFilter::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->timeout_start_time_ >= this->time_period_) {
|
||||
if (throttle_nan_passes(this->last_input_, this->time_period_, this->pending_value_))
|
||||
this->output(this->pending_value_);
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
// DebounceFilter
|
||||
optional<float> DebounceFilter::new_value(float value) {
|
||||
App.scheduler.set_timeout(this, this->time_period_, [this, value]() { this->output(value); });
|
||||
|
||||
@@ -479,6 +479,19 @@ class TimeoutFilterConfigured : public TimeoutFilterBase {
|
||||
// Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead
|
||||
};
|
||||
|
||||
/// Codegen use only: `timeout` (value `last`) followed by `throttle_with_priority` (NaN only) with the
|
||||
/// same period, as one object. Output matches the two filter chain exactly.
|
||||
class TimeoutThrottleFilter : public TimeoutFilterLast {
|
||||
public:
|
||||
explicit TimeoutThrottleFilter(uint32_t time_period) : TimeoutFilterLast(time_period) {}
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0}; // When the throttle stage last let a value through
|
||||
};
|
||||
|
||||
class DebounceFilter : public Filter {
|
||||
public:
|
||||
explicit DebounceFilter(uint32_t time_period);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""timeout (value last) followed by throttle_with_priority with the same period becomes one filter."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def test_timeout_throttle_pair_is_merged(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
main_cpp = generate_main("tests/component_tests/sensor/timeout_throttle.yaml")
|
||||
|
||||
# Only the `merged` sensor's adjacent pair becomes one object
|
||||
assert (
|
||||
len(re.findall(r"new\(\w+\) sensor::TimeoutThrottleFilter\(1000\);", main_cpp))
|
||||
== 1
|
||||
)
|
||||
merged = re.search(r"merged->set_filters\(\{(\w+)\}\);", main_cpp)
|
||||
assert merged is not None
|
||||
assert f"new({merged.group(1)}) sensor::TimeoutThrottleFilter(1000);" in main_cpp
|
||||
# Every other shape keeps its two filters
|
||||
for sensor_id in ("other_periods", "other_order", "value_list", "configured_value"):
|
||||
call = re.search(rf"{sensor_id}->set_filters\(\{{([^}}]*)\}}\);", main_cpp)
|
||||
assert call is not None and len(call.group(1).split(",")) == 2, sensor_id
|
||||
@@ -0,0 +1,52 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: merged
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 1s
|
||||
value: last
|
||||
- throttle_with_priority: 1s
|
||||
- platform: template
|
||||
id: other_periods
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 1s
|
||||
value: last
|
||||
- throttle_with_priority: 2s
|
||||
- platform: template
|
||||
id: other_order
|
||||
filters:
|
||||
- throttle_with_priority: 1s
|
||||
- timeout:
|
||||
timeout: 1s
|
||||
value: last
|
||||
- platform: template
|
||||
id: value_list
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 1s
|
||||
value: last
|
||||
- throttle_with_priority:
|
||||
timeout: 1s
|
||||
value: [0.0]
|
||||
- platform: template
|
||||
id: configured_value
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 1s
|
||||
value: 0.0
|
||||
- throttle_with_priority: 1s
|
||||
- platform: template
|
||||
id: inside_or
|
||||
filters:
|
||||
- or:
|
||||
- timeout:
|
||||
timeout: 1s
|
||||
value: last
|
||||
- throttle_with_priority: 1s
|
||||
@@ -0,0 +1,67 @@
|
||||
esphome:
|
||||
name: test-timeout-throttle
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Source"
|
||||
id: source_sensor
|
||||
accuracy_decimals: 2
|
||||
|
||||
# The default radar pair; codegen merges it into one TimeoutThrottleFilter
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Merged"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 200ms
|
||||
value: last
|
||||
- throttle_with_priority: 200ms
|
||||
|
||||
# Same chain with an identity filter in between, which keeps the two filter objects
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Reference"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 200ms
|
||||
value: last
|
||||
- multiply: 1.0
|
||||
- throttle_with_priority: 200ms
|
||||
|
||||
script:
|
||||
- id: drive
|
||||
then:
|
||||
# Burst faster than the period: first passes, the rest are dropped, the timeout republishes the last
|
||||
- sensor.template.publish: {id: source_sensor, state: 1.0}
|
||||
- delay: 30ms
|
||||
- sensor.template.publish: {id: source_sensor, state: 2.0}
|
||||
- delay: 30ms
|
||||
- sensor.template.publish: {id: source_sensor, state: 3.0}
|
||||
- delay: 450ms
|
||||
# NaN inside a burst always passes
|
||||
- sensor.template.publish: {id: source_sensor, state: 4.0}
|
||||
- delay: 30ms
|
||||
- sensor.template.publish: {id: source_sensor, state: !lambda "return NAN;"}
|
||||
- delay: 30ms
|
||||
- sensor.template.publish: {id: source_sensor, state: 5.0}
|
||||
- delay: 450ms
|
||||
# Values right around the period
|
||||
- sensor.template.publish: {id: source_sensor, state: 6.0}
|
||||
- delay: 250ms
|
||||
- sensor.template.publish: {id: source_sensor, state: 7.0}
|
||||
- delay: 190ms
|
||||
- sensor.template.publish: {id: source_sensor, state: 8.0}
|
||||
- delay: 600ms
|
||||
- sensor.template.publish: {id: source_sensor, state: 99.0}
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Drive"
|
||||
on_press:
|
||||
- script.execute: drive
|
||||
@@ -0,0 +1,61 @@
|
||||
"""The merged timeout + throttle_with_priority filter publishes exactly what the two filter chain does."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
|
||||
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_timeout_throttle_filter(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
values: dict[str, list[float]] = {"merged": [], "reference": []}
|
||||
done = {name: loop.create_future() for name in values}
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, SensorState):
|
||||
return
|
||||
name = key_to_sensor.get(state.key)
|
||||
if name not in values:
|
||||
return
|
||||
values[name].append(math.nan if state.missing_state else state.state)
|
||||
# 99 is published last and republished once by the timeout
|
||||
if values[name].count(99.0) == 2 and not done[name].done():
|
||||
done[name].set_result(True)
|
||||
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
key_to_sensor = build_key_to_entity_mapping(
|
||||
entities, {"merged": "Merged", "reference": "Reference"}
|
||||
)
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
button = next(
|
||||
e.key for e in entities if isinstance(e, ButtonInfo) and e.name == "Drive"
|
||||
)
|
||||
client.button_command(button)
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.gather(*done.values()), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Timed out: {values}")
|
||||
|
||||
merged, reference = values["merged"], values["reference"]
|
||||
assert len(merged) == len(reference), (merged, reference)
|
||||
for got, want in zip(merged, reference, strict=True):
|
||||
assert (math.isnan(got) and math.isnan(want)) or got == want, (
|
||||
merged,
|
||||
reference,
|
||||
)
|
||||
assert any(math.isnan(v) for v in merged)
|
||||
Reference in New Issue
Block a user