[core][sensor][binary_sensor][text_sensor][debug][xiaomi_rtcgq02lm] Add new_sub_sensor, new_sub_binary_sensor and new_sub_text_sensor helpers (#19725)

This commit is contained in:
J. Nick Koston
2026-09-27 12:08:36 +02:00
committed by GitHub
parent a6d525a44e
commit 06d88e0381
10 changed files with 142 additions and 51 deletions
+10
View File
@@ -322,6 +322,16 @@ file does, and it is the authority when they disagree. The most useful starting
var = await switch.new_switch(config)
```
- **Optional child entities of a hub:** use `new_sub_sensor`, `new_sub_binary_sensor` or
`new_sub_text_sensor`. Each creates the entity only when its key is configured and passes it to
the setter. Always name the setter explicitly; never build it with `getattr` and an f-string.
```python
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await sensor.new_sub_sensor(config, CONF_TEMPERATURE, var.set_temperature_sensor)
await sensor.new_sub_sensor(config, CONF_HUMIDITY, var.set_humidity_sensor)
```
* **Automations (Triggers, Actions, Conditions):**
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
+14 -1
View File
@@ -1,3 +1,4 @@
from collections.abc import Callable
from logging import getLogger
from esphome import automation, core
@@ -64,11 +65,13 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
new_sub_entity,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, Expression, SafeExpType
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -632,6 +635,16 @@ async def new_binary_sensor(config, *args):
return var
async def new_sub_binary_sensor(
config: ConfigType,
key: str,
setter: Callable[[MockObj], Expression],
*args: SafeExpType,
) -> MockObj | None:
"""Create the binary sensor configured under key, if any, and pass it to setter."""
return await new_sub_entity(new_binary_sensor, config, key, setter, *args)
BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(BinarySensor),
+15 -27
View File
@@ -108,30 +108,18 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
if free_conf := config.get(CONF_FREE):
sens = await sensor.new_sensor(free_conf)
cg.add(debug_component.set_free_sensor(sens))
if block_conf := config.get(CONF_BLOCK):
sens = await sensor.new_sensor(block_conf)
cg.add(debug_component.set_block_sensor(sens))
if fragmentation_conf := config.get(CONF_FRAGMENTATION):
sens = await sensor.new_sensor(fragmentation_conf)
cg.add(debug_component.set_fragmentation_sensor(sens))
if min_free_conf := config.get(CONF_MIN_FREE):
sens = await sensor.new_sensor(min_free_conf)
cg.add(debug_component.set_min_free_sensor(sens))
if loop_time_conf := config.get(CONF_LOOP_TIME):
sens = await sensor.new_sensor(loop_time_conf)
cg.add(debug_component.set_loop_time_sensor(sens))
if psram_conf := config.get(CONF_PSRAM):
sens = await sensor.new_sensor(psram_conf)
cg.add(debug_component.set_psram_sensor(sens))
if cpu_freq_conf := config.get(CONF_CPU_FREQUENCY):
sens = await sensor.new_sensor(cpu_freq_conf)
cg.add(debug_component.set_cpu_frequency_sensor(sens))
await sensor.new_sub_sensor(config, CONF_FREE, debug_component.set_free_sensor)
await sensor.new_sub_sensor(config, CONF_BLOCK, debug_component.set_block_sensor)
await sensor.new_sub_sensor(
config, CONF_FRAGMENTATION, debug_component.set_fragmentation_sensor
)
await sensor.new_sub_sensor(
config, CONF_MIN_FREE, debug_component.set_min_free_sensor
)
await sensor.new_sub_sensor(
config, CONF_LOOP_TIME, debug_component.set_loop_time_sensor
)
await sensor.new_sub_sensor(config, CONF_PSRAM, debug_component.set_psram_sensor)
await sensor.new_sub_sensor(
config, CONF_CPU_FREQUENCY, debug_component.set_cpu_frequency_sensor
)
+6 -6
View File
@@ -33,9 +33,9 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
if CONF_DEVICE in config:
sens = await text_sensor.new_text_sensor(config[CONF_DEVICE])
cg.add(debug_component.set_device_info_sensor(sens))
if CONF_RESET_REASON in config:
sens = await text_sensor.new_text_sensor(config[CONF_RESET_REASON])
cg.add(debug_component.set_reset_reason_sensor(sens))
await text_sensor.new_sub_text_sensor(
config, CONF_DEVICE, debug_component.set_device_info_sensor
)
await text_sensor.new_sub_text_sensor(
config, CONF_RESET_REASON, debug_component.set_reset_reason_sensor
)
+13
View File
@@ -1,3 +1,4 @@
from collections.abc import Callable
import logging
import math
@@ -115,6 +116,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
entity_duplicate_validator,
new_sub_entity,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -122,6 +124,7 @@ from esphome.core.entity_helpers import (
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType, Expression, SafeExpType
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -1013,6 +1016,16 @@ async def new_sensor(config, *args):
return var
async def new_sub_sensor(
config: ConfigType,
key: str,
setter: Callable[[MockObj], Expression],
*args: SafeExpType,
) -> MockObj | None:
"""Create the sensor configured under key, if any, and pass it to setter."""
return await new_sub_entity(new_sensor, config, key, setter, *args)
SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(Sensor),
+15 -1
View File
@@ -1,3 +1,5 @@
from collections.abc import Callable
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
@@ -23,11 +25,13 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
new_sub_entity,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, Expression, SafeExpType
from esphome.util import Registry
DEVICE_CLASSES = [
@@ -231,6 +235,16 @@ async def new_text_sensor(config, *args):
return var
async def new_sub_text_sensor(
config: ConfigType,
key: str,
setter: Callable[[MockObj], Expression],
*args: SafeExpType,
) -> MockObj | None:
"""Create the text sensor configured under key, if any, and pass it to setter."""
return await new_sub_entity(new_text_sensor, config, key, setter, *args)
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config):
cg.add_global(text_sensor_ns.using)
@@ -49,16 +49,12 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID])
if CONF_MOTION in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_MOTION])
cg.add(parent.set_motion(sens))
if await binary_sensor.new_sub_binary_sensor(
config, CONF_MOTION, parent.set_motion
):
cg.add(parent.set_motion_timeout(config[CONF_MOTION][CONF_TIMEOUT]))
if CONF_LIGHT in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_LIGHT])
cg.add(parent.set_light(sens))
if CONF_BUTTON in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_BUTTON])
cg.add(parent.set_button(sens))
await binary_sensor.new_sub_binary_sensor(config, CONF_LIGHT, parent.set_light)
if await binary_sensor.new_sub_binary_sensor(
config, CONF_BUTTON, parent.set_button
):
cg.add(parent.set_button_timeout(config[CONF_BUTTON][CONF_TIMEOUT]))
@@ -33,6 +33,4 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID])
if CONF_BATTERY_LEVEL in config:
sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
cg.add(parent.set_battery_level(sens))
await sensor.new_sub_sensor(config, CONF_BATTERY_LEVEL, parent.set_battery_level)
+22 -2
View File
@@ -1,7 +1,8 @@
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
import functools
import logging
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -32,7 +33,7 @@ from esphome.helpers import (
sanitize,
snake_case,
)
from esphome.types import ConfigType, EntityMetadata
from esphome.types import ConfigType, EntityMetadata, Expression, SafeExpType
_LOGGER = logging.getLogger(__name__)
@@ -467,6 +468,25 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
config[_KEY_ICON_IDX] = icon_idx
async def new_sub_entity(
new_entity: Callable[..., Awaitable[MockObj]],
config: ConfigType,
key: str,
setter: Callable[[MockObj], Expression],
*args: SafeExpType,
**kwargs: Any,
) -> MockObj | None:
"""Create the entity configured under key, if any, and pass it to setter.
Returns None only when key is not configured, so the result can be used directly as a condition.
"""
if (conf := config.get(key)) is None:
return None
var = await new_entity(conf, *args, **kwargs)
add(setter(var))
return var
def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
"""Validator that inherits a configuration property from another entity, for use with FINAL_VALIDATE_SCHEMA.
If a property is already set, it will not be inherited.
@@ -1236,3 +1236,42 @@ async def test_finalize_comment_sanitization(
# Newline must be replaced to prevent breaking out of comment
assert "\n" not in comment_line
assert "INJECTED_CODE" in comment_line # still visible but safe in comment
@pytest.mark.asyncio
async def test_new_sub_entity_creates_and_sets(
setup_test_environment: list[str],
) -> None:
"""A configured key, even with an empty config, creates the entity and passes it to the setter."""
sub_config: dict[str, Any] = {}
created = MockObj("child")
calls: list[tuple[Any, ...]] = []
async def new_entity(conf: dict[str, Any], *args: Any, **kwargs: Any) -> MockObj:
calls.append((conf, args, kwargs))
return created
parent = MockObj("parent", "->")
result = await entity_helpers.new_sub_entity(
new_entity, {"child": sub_config}, "child", parent.set_child, 1, step=2
)
assert result is created
assert calls == [(sub_config, (1,), {"step": 2})]
assert setup_test_environment == ["parent->set_child(child)"]
@pytest.mark.asyncio
async def test_new_sub_entity_missing_key(setup_test_environment: list[str]) -> None:
"""A missing key creates nothing and emits no code."""
async def new_entity(conf: dict[str, Any], *args: Any) -> MockObj:
raise AssertionError("must not be called")
parent = MockObj("parent", "->")
result = await entity_helpers.new_sub_entity(
new_entity, {}, "child", parent.set_child
)
assert result is None
assert setup_test_environment == []