[core][sensor][binary_sensor][text_sensor][button][switch][number][select][debug][xiaomi_rtcgq02lm][tfluna][ld2412] Bind sub entity config and parent once with sub_* helpers (#19754)

This commit is contained in:
J. Nick Koston
2026-09-27 19:19:10 +02:00
committed by GitHub
parent 1f175c1942
commit 7cda9a2666
24 changed files with 281 additions and 287 deletions
+14 -5
View File
@@ -322,14 +322,23 @@ 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.
- **Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`,
`sub_selects` in their domains), adding `parent=hub` for entities that derive from `Parented<T>`,
then make one call per key, even when there is only one. A call creates the entity only when its key
is configured, passes it to the setter and returns it (or `None`); extra arguments such as
`min_value` or `options` go on the call. Always name the setter explicitly on the object that owns
it, never with `getattr` and an f-string, and keep that variable short (`var` for the component
itself, `hub` for one fetched with `cg.get_variable`) so the calls fit on one line. Loops whose
setter also takes an index, such as `set_gate_threshold(x, n)`, stay as they are.
```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)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
buttons = button.sub_buttons(config, parent=var)
await buttons(CONF_RESTART, var.set_restart_button)
```
* **Automations (Triggers, Actions, Conditions):**
+8 -12
View File
@@ -1,4 +1,3 @@
from collections.abc import Callable
from logging import getLogger
from esphome import automation, core
@@ -62,16 +61,16 @@ from esphome.const import (
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
new_sub_entity,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, Expression, SafeExpType
from esphome.types import ConfigType
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -635,14 +634,11 @@ 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)
def sub_binary_sensors(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_binary_sensor."""
return SubEntities(new_binary_sensor, config, parent)
BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
+9 -1
View File
@@ -16,8 +16,9 @@ from esphome.const import (
DEVICE_CLASS_RESTART,
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
@@ -114,6 +115,13 @@ async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj:
return var
def sub_buttons(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_button."""
return SubEntities(new_button, config, parent)
BUTTON_PRESS_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Button),
+9 -16
View File
@@ -106,20 +106,13 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
hub = await cg.get_variable(config[CONF_DEBUG_ID])
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
)
sensors = sensor.sub_sensors(config)
await sensors(CONF_FREE, hub.set_free_sensor)
await sensors(CONF_BLOCK, hub.set_block_sensor)
await sensors(CONF_FRAGMENTATION, hub.set_fragmentation_sensor)
await sensors(CONF_MIN_FREE, hub.set_min_free_sensor)
await sensors(CONF_LOOP_TIME, hub.set_loop_time_sensor)
await sensors(CONF_PSRAM, hub.set_psram_sensor)
await sensors(CONF_CPU_FREQUENCY, hub.set_cpu_frequency_sensor)
+4 -7
View File
@@ -31,11 +31,8 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
hub = await cg.get_variable(config[CONF_DEBUG_ID])
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
)
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_DEVICE, hub.set_device_info_sensor)
await text_sensors(CONF_RESET_REASON, hub.set_reset_reason_sensor)
+9 -21
View File
@@ -50,24 +50,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if dynamic_background_correction_status_config := config.get(
CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS
):
sens = await binary_sensor.new_binary_sensor(
dynamic_background_correction_status_config
)
cg.add(
LD2412_component.set_dynamic_background_correction_status_binary_sensor(
sens
)
)
if has_target_config := config.get(CONF_HAS_TARGET):
sens = await binary_sensor.new_binary_sensor(has_target_config)
cg.add(LD2412_component.set_target_binary_sensor(sens))
if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET):
sens = await binary_sensor.new_binary_sensor(has_moving_target_config)
cg.add(LD2412_component.set_moving_target_binary_sensor(sens))
if has_still_target_config := config.get(CONF_HAS_STILL_TARGET):
sens = await binary_sensor.new_binary_sensor(has_still_target_config)
cg.add(LD2412_component.set_still_target_binary_sensor(sens))
hub = await cg.get_variable(config[CONF_LD2412_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(
CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS,
hub.set_dynamic_background_correction_status_binary_sensor,
)
await binary_sensors(CONF_HAS_TARGET, hub.set_target_binary_sensor)
await binary_sensors(CONF_HAS_MOVING_TARGET, hub.set_moving_target_binary_sensor)
await binary_sensors(CONF_HAS_STILL_TARGET, hub.set_still_target_binary_sensor)
+9 -19
View File
@@ -56,22 +56,12 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_factory_reset_button(b))
if query_params_config := config.get(CONF_QUERY_PARAMS):
b = await button.new_button(query_params_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_query_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_restart_button(b))
if start_dynamic_background_correction_config := config.get(
CONF_START_DYNAMIC_BACKGROUND_CORRECTION
):
b = await button.new_button(start_dynamic_background_correction_config)
await cg.register_parented(b, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_start_dynamic_background_correction_button(b))
hub = await cg.get_variable(config[CONF_LD2412_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
await buttons(CONF_RESTART, hub.set_restart_button)
await buttons(
CONF_START_DYNAMIC_BACKGROUND_CORRECTION,
hub.set_start_dynamic_background_correction_button,
)
+37 -34
View File
@@ -87,42 +87,45 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD):
n = await number.new_number(
light_threshold_config, min_value=0, max_value=255, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_light_threshold_number(n))
if max_distance_gate_config := config.get(CONF_MAX_DISTANCE_GATE):
n = await number.new_number(
max_distance_gate_config, min_value=2, max_value=13, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_max_distance_gate_number(n))
if min_distance_gate_config := config.get(CONF_MIN_DISTANCE_GATE):
n = await number.new_number(
min_distance_gate_config, min_value=1, max_value=12, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_min_distance_gate_number(n))
hub = await cg.get_variable(config[CONF_LD2412_ID])
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_LIGHT_THRESHOLD,
hub.set_light_threshold_number,
min_value=0,
max_value=255,
step=1,
)
await numbers(
CONF_MAX_DISTANCE_GATE,
hub.set_max_distance_gate_number,
min_value=2,
max_value=13,
step=1,
)
await numbers(
CONF_MIN_DISTANCE_GATE,
hub.set_min_distance_gate_number,
min_value=1,
max_value=12,
step=1,
)
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = cg.new_Pvariable(move_config[CONF_ID])
await number.register_number(
n, move_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_gate_move_threshold_number(x, n))
n = await number.new_number(move_config, min_value=0, max_value=100, step=1)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID])
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
n = await number.new_number(
still_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_gate_still_threshold_number(x, n))
if timeout_config := config.get(CONF_TIMEOUT):
n = await number.new_number(timeout_config, min_value=0, max_value=900, step=1)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_timeout_number(n))
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
await numbers(
CONF_TIMEOUT,
hub.set_timeout_number,
min_value=0,
max_value=900,
step=1,
)
+31 -33
View File
@@ -50,36 +50,34 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if baud_rate_config := config.get(CONF_BAUD_RATE):
s = await select.new_select(
baud_rate_config,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_baud_rate_select(s))
if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION):
s = await select.new_select(
distance_resolution_config, options=["0.2m", "0.5m", "0.75m"]
)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_distance_resolution_select(s))
if light_function_config := config.get(CONF_LIGHT_FUNCTION):
s = await select.new_select(
light_function_config, options=["off", "below", "above"]
)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_light_function_select(s))
if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL):
s = await select.new_select(out_pin_level_config, options=["low", "high"])
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_out_pin_level_select(s))
hub = await cg.get_variable(config[CONF_LD2412_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await selects(
CONF_DISTANCE_RESOLUTION,
hub.set_distance_resolution_select,
options=["0.2m", "0.5m", "0.75m"],
)
await selects(
CONF_LIGHT_FUNCTION,
hub.set_light_function_select,
options=["off", "below", "above"],
)
await selects(
CONF_OUT_PIN_LEVEL,
hub.set_out_pin_level_select,
options=["low", "high"],
)
+10 -21
View File
@@ -158,30 +158,19 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if detection_distance_config := config.get(CONF_DETECTION_DISTANCE):
sens = await sensor.new_sensor(detection_distance_config)
cg.add(LD2412_component.set_detection_distance_sensor(sens))
if light_config := config.get(CONF_LIGHT):
sens = await sensor.new_sensor(light_config)
cg.add(LD2412_component.set_light_sensor(sens))
if moving_distance_config := config.get(CONF_MOVING_DISTANCE):
sens = await sensor.new_sensor(moving_distance_config)
cg.add(LD2412_component.set_moving_target_distance_sensor(sens))
if moving_energy_config := config.get(CONF_MOVING_ENERGY):
sens = await sensor.new_sensor(moving_energy_config)
cg.add(LD2412_component.set_moving_target_energy_sensor(sens))
if still_distance_config := config.get(CONF_STILL_DISTANCE):
sens = await sensor.new_sensor(still_distance_config)
cg.add(LD2412_component.set_still_target_distance_sensor(sens))
if still_energy_config := config.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_energy_config)
cg.add(LD2412_component.set_still_target_energy_sensor(sens))
hub = await cg.get_variable(config[CONF_LD2412_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DETECTION_DISTANCE, hub.set_detection_distance_sensor)
await sensors(CONF_LIGHT, hub.set_light_sensor)
await sensors(CONF_MOVING_DISTANCE, hub.set_moving_target_distance_sensor)
await sensors(CONF_MOVING_ENERGY, hub.set_moving_target_energy_sensor)
await sensors(CONF_STILL_DISTANCE, hub.set_still_target_distance_sensor)
await sensors(CONF_STILL_ENERGY, hub.set_still_target_energy_sensor)
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
if move_config := gate_conf.get(CONF_MOVE_ENERGY):
sens = await sensor.new_sensor(move_config)
cg.add(LD2412_component.set_gate_move_sensor(x, sens))
cg.add(hub.set_gate_move_sensor(x, sens))
if still_config := gate_conf.get(CONF_STILL_ENERGY):
sens = await sensor.new_sensor(still_config)
cg.add(LD2412_component.set_gate_still_sensor(x, sens))
cg.add(hub.set_gate_still_sensor(x, sens))
+4 -9
View File
@@ -37,12 +37,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if bluetooth_config := config.get(CONF_BLUETOOTH):
s = await switch.new_switch(bluetooth_config)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_bluetooth_switch(s))
if engineering_mode_config := config.get(CONF_ENGINEERING_MODE):
s = await switch.new_switch(engineering_mode_config)
await cg.register_parented(s, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_engineering_mode_switch(s))
hub = await cg.get_variable(config[CONF_LD2412_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)
await switches(CONF_ENGINEERING_MODE, hub.set_engineering_mode_switch)
+4 -7
View File
@@ -28,10 +28,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)
cg.add(LD2412_component.set_version_text_sensor(sens))
if mac_address_config := config.get(CONF_MAC_ADDRESS):
sens = await text_sensor.new_text_sensor(mac_address_config)
cg.add(LD2412_component.set_mac_text_sensor(sens))
hub = await cg.get_variable(config[CONF_LD2412_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
await text_sensors(CONF_MAC_ADDRESS, hub.set_mac_text_sensor)
+11 -2
View File
@@ -79,16 +79,18 @@ from esphome.const import (
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
setup_unit_of_measurement,
)
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
@@ -325,6 +327,13 @@ async def new_number(config, *args, min_value: float, max_value: float, step: fl
return var
def sub_numbers(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_number."""
return SubEntities(new_number, config, parent)
NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All(
{
cv.Required(CONF_ID): cv.use_id(Number),
+10 -1
View File
@@ -20,12 +20,14 @@ from esphome.const import (
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass, TemplateArguments
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArguments
from esphome.cpp_types import global_ns
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -123,6 +125,13 @@ async def new_select(config, *args, options: list[str]):
return var
def sub_selects(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_select."""
return SubEntities(new_select, config, parent)
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config):
cg.add_global(select_ns.using)
+8 -12
View File
@@ -1,4 +1,3 @@
from collections.abc import Callable
import logging
import math
@@ -112,11 +111,11 @@ from esphome.const import (
DEVICE_CLASS_WIND_SPEED,
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
new_sub_entity,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -124,7 +123,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.types import ConfigType
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -1016,14 +1015,11 @@ 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)
def sub_sensors(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_sensor."""
return SubEntities(new_sensor, config, parent)
SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
+11 -2
View File
@@ -20,14 +20,16 @@ from esphome.const import (
DEVICE_CLASS_OUTLET,
DEVICE_CLASS_SWITCH,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
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
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -173,6 +175,13 @@ async def new_switch(config, *args):
return var
def sub_switches(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_switch."""
return SubEntities(new_switch, config, parent)
SWITCH_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(Switch),
+8 -13
View File
@@ -1,5 +1,3 @@
from collections.abc import Callable
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
@@ -22,16 +20,16 @@ from esphome.const import (
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_TIMESTAMP,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
new_sub_entity,
queue_entity_register,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, Expression, SafeExpType
from esphome.types import ConfigType
from esphome.util import Registry
DEVICE_CLASSES = [
@@ -235,14 +233,11 @@ 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)
def sub_text_sensors(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_text_sensor."""
return SubEntities(new_text_sensor, config, parent)
@coroutine_with_priority(CoroPriority.CORE)
+4 -9
View File
@@ -61,12 +61,7 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
tfluna_component = await cg.get_variable(config[CONF_TFLUNA_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_TFLUNA_ID])
cg.add(tfluna_component.set_reset_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_TFLUNA_ID])
cg.add(tfluna_component.set_restart_button(b))
hub = await cg.get_variable(config[CONF_TFLUNA_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_FACTORY_RESET, hub.set_reset_button)
await buttons(CONF_RESTART, hub.set_restart_button)
+6 -13
View File
@@ -60,16 +60,9 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
tfluna_component = await cg.get_variable(config[CONF_TFLUNA_ID])
if distance_config := config.get(CONF_DISTANCE):
sens = await sensor.new_sensor(distance_config)
cg.add(tfluna_component.set_distance_sensor(sens))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(tfluna_component.set_temperature_sensor(sens))
if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH):
sens = await sensor.new_sensor(signal_strength_config)
cg.add(tfluna_component.set_signal_strength_sensor(sens))
if timestamp_config := config.get(CONF_TIMESTAMP):
sens = await sensor.new_sensor(timestamp_config)
cg.add(tfluna_component.set_timestamp_sensor(sens))
hub = await cg.get_variable(config[CONF_TFLUNA_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISTANCE, hub.set_distance_sensor)
await sensors(CONF_TEMPERATURE, hub.set_temperature_sensor)
await sensors(CONF_SIGNAL_STRENGTH, hub.set_signal_strength_sensor)
await sensors(CONF_TIMESTAMP, hub.set_timestamp_sensor)
+3 -4
View File
@@ -17,7 +17,6 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
tfluna_component = await cg.get_variable(config[CONF_TFLUNA_ID])
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)
cg.add(tfluna_component.set_version_text_sensor(sens))
hub = await cg.get_variable(config[CONF_TFLUNA_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
@@ -47,14 +47,11 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID])
hub = await cg.get_variable(config[CONF_ID])
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]))
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]))
binary_sensors = binary_sensor.sub_binary_sensors(config)
if await binary_sensors(CONF_MOTION, hub.set_motion):
cg.add(hub.set_motion_timeout(config[CONF_MOTION][CONF_TIMEOUT]))
await binary_sensors(CONF_LIGHT, hub.set_light)
if await binary_sensors(CONF_BUTTON, hub.set_button):
cg.add(hub.set_button_timeout(config[CONF_BUTTON][CONF_TIMEOUT]))
@@ -31,6 +31,7 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID])
hub = await cg.get_variable(config[CONF_ID])
await sensor.new_sub_sensor(config, CONF_BATTERY_LEVEL, parent.set_battery_level)
sensors = sensor.sub_sensors(config)
await sensors(CONF_BATTERY_LEVEL, hub.set_battery_level)
+24 -15
View File
@@ -468,23 +468,32 @@ 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.
@dataclass(frozen=True, slots=True)
class SubEntities:
"""Create the child entity configured under key, set parent (if bound) and pass it to setter.
Returns None only when key is not configured, so the result can be used directly as a condition.
Extra arguments go to new_entity. Returns None only when key is absent, 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
new_entity: Callable[..., Awaitable[MockObj]]
config: ConfigType
parent: MockObj | ID | None = None
async def __call__(
self,
key: str,
setter: Callable[[MockObj], Expression],
*args: SafeExpType,
**kwargs: Any,
) -> MockObj | None:
if (conf := self.config.get(key)) is None:
return None
var = await self.new_entity(conf, *args, **kwargs)
if self.parent is not None:
await cg.register_parented(var, self.parent)
add(setter(var))
return var
def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
+38 -19
View File
@@ -1239,39 +1239,58 @@ async def test_finalize_comment_sanitization(
@pytest.mark.asyncio
async def test_new_sub_entity_creates_and_sets(
setup_test_environment: list[str],
async def test_sub_entities_creates_and_sets(
monkeypatch: pytest.MonkeyPatch, 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")
"""A configured key creates the entity, sets the parent before the setter and returns it."""
calls: list[tuple[Any, ...]] = []
created = MockObj("child")
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
)
async def fake_register_parented(var: MockObj, parent: MockObj) -> None:
setup_test_environment.append(f"parent {var} {parent}")
assert result is created
assert calls == [(sub_config, (1,), {"step": 2})]
assert setup_test_environment == ["parent->set_child(child)"]
monkeypatch.setattr(entity_helpers.cg, "register_parented", fake_register_parented)
hub = MockObj("hub", "->")
sub = entity_helpers.SubEntities(new_entity, {"child": {}}, parent=hub)
assert await sub("child", hub.set_child, 1, step=2) is created
assert calls == [({}, (1,), {"step": 2})]
assert setup_test_environment == ["parent child hub", "hub->set_child(child)"]
@pytest.mark.asyncio
async def test_new_sub_entity_missing_key(setup_test_environment: list[str]) -> None:
async def test_sub_entities_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:
async def new_entity(conf: dict[str, Any]) -> MockObj:
raise AssertionError("must not be called")
parent = MockObj("parent", "->")
result = await entity_helpers.new_sub_entity(
new_entity, {}, "child", parent.set_child
)
hub = MockObj("hub", "->")
sub = entity_helpers.SubEntities(new_entity, {})
assert result is None
assert await sub("child", hub.set_child) is None
assert setup_test_environment == []
@pytest.mark.asyncio
async def test_sub_entities_without_parent(
monkeypatch: pytest.MonkeyPatch, setup_test_environment: list[str]
) -> None:
"""Without a bound parent, register_parented is not called."""
async def fail_register_parented(var: MockObj, parent: MockObj) -> None:
raise AssertionError("must not be called")
async def new_entity(conf: dict[str, Any]) -> MockObj:
return MockObj("child")
monkeypatch.setattr(entity_helpers.cg, "register_parented", fail_register_parented)
hub = MockObj("hub", "->")
sub = entity_helpers.SubEntities(new_entity, {"child": {CONF_ID: "child"}})
assert await sub("child", hub.set_child) is not None
assert setup_test_environment == ["hub->set_child(child)"]