From b7d651dd179ad165e9921bd9c7840de58da3987a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:10:53 -0500 Subject: [PATCH] [core] Defer entity automation codegen to prevent sibling ID deadlocks (#14381) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/binary_sensor/__init__.py | 37 +++++++++++--------- esphome/components/number/__init__.py | 31 +++++++++------- esphome/components/sensor/__init__.py | 37 +++++++++++--------- esphome/components/switch/__init__.py | 16 ++++++--- esphome/components/text_sensor/__init__.py | 19 ++++++---- 5 files changed, 83 insertions(+), 57 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 4500168cb1d..036d78da736 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -550,22 +550,8 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) -async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config, "binary_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) - trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( - CONF_PUBLISH_INITIAL_STATE, False - ) - cg.add(var.set_trigger_on_initial_state(trigger)) - if inverted := config.get(CONF_INVERTED): - cg.add(var.set_inverted(inverted)) - if filters_config := config.get(CONF_FILTERS): - cg.add_define("USE_BINARY_SENSOR_FILTER") - filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) - cg.add(var.add_filters(filters)) - +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_binary_sensor_automations(var, config): for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -617,6 +603,25 @@ async def setup_binary_sensor_core_(var, config): conf, ) + +async def setup_binary_sensor_core_(var, config): + await setup_entity(var, config, "binary_sensor") + + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( + CONF_PUBLISH_INITIAL_STATE, False + ) + cg.add(var.set_trigger_on_initial_state(trigger)) + if inverted := config.get(CONF_INVERTED): + cg.add(var.set_inverted(inverted)) + if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") + filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) + cg.add(var.add_filters(filters)) + + CORE.add_job(_build_binary_sensor_automations, var, config) + if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index b23da7799f1..d12ec7463b5 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -240,6 +240,23 @@ def number_schema( return _NUMBER_SCHEMA.extend(schema) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_number_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if CONF_ABOVE in conf: + template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if CONF_BELOW in conf: + template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): @@ -254,19 +271,7 @@ async def setup_number_core_( if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: cg.add(var.traits.set_mode(config[CONF_MODE])) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if CONF_ABOVE in conf: - template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if CONF_BELOW in conf: - template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_number_automations, var, config) if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 770c044efb9..338aaae0b53 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -888,6 +888,26 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if (above := conf.get(CONF_ABOVE)) is not None: + template_ = await cg.templatable(above, [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if (below := conf.get(CONF_BELOW)) is not None: + template_ = await cg.templatable(below, [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_sensor_core_(var, config): await setup_entity(var, config, "sensor") @@ -907,22 +927,7 @@ async def setup_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if (above := conf.get(CONF_ABOVE)) is not None: - template_ = await cg.templatable(above, [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if (below := conf.get(CONF_BELOW)) is not None: - template_ = await cg.templatable(below, [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 7424d7c92fe..cfc5e2b6e82 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -141,11 +141,8 @@ def switch_schema( return _SWITCH_SCHEMA.extend(schema) -async def setup_switch_core_(var, config): - await setup_entity(var, config, "switch") - - if (inverted := config.get(CONF_INVERTED)) is not None: - cg.add(var.set_inverted(inverted)) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_switch_automations(var, config): for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) @@ -156,6 +153,15 @@ async def setup_switch_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + +async def setup_switch_core_(var, config): + await setup_entity(var, config, "switch") + + if (inverted := config.get(CONF_INVERTED)) is not None: + cg.add(var.set_inverted(inverted)) + + CORE.add_job(_build_switch_automations, var, config) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 2e8edb43c90..2edf202cd23 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -197,6 +197,17 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_text_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + async def setup_text_sensor_core_(var, config): await setup_entity(var, config, "text_sensor") @@ -208,13 +219,7 @@ async def setup_text_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + CORE.add_job(_build_text_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var)