mirror of
https://github.com/esphome/esphome.git
synced 2026-09-27 23:10:21 +00:00
[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:
@@ -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).
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 == []
|
||||
|
||||
Reference in New Issue
Block a user