[automation] Add CallbackAutomation dataclass and build_callback_automations helper (#15246)

This commit is contained in:
J. Nick Koston
2026-04-07 10:09:27 -10:00
committed by GitHub
parent 674d030cbb
commit 0d809a7481
33 changed files with 794 additions and 427 deletions

View File

@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
import logging
import esphome.codegen as cg
@@ -715,3 +716,35 @@ async def build_callback_automation(
# MockObjs (not user input), and there's no Expression type for positional
# aggregate initialization (StructInitializer uses named fields).
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
@dataclass(frozen=True, slots=True)
class CallbackAutomation:
"""A single callback automation entry for build_callback_automations."""
conf_key: str
callback_method: str
args: TemplateArgsType = field(default_factory=list)
forwarder: MockObj | MockObjClass | None = None
async def build_callback_automations(
parent: MockObj,
config: ConfigType,
entries: tuple[CallbackAutomation, ...],
) -> None:
"""Build multiple callback automations from a tuple of entries.
:param parent: The component object (e.g., button, sensor).
:param config: The full component config dict.
:param entries: Tuple of CallbackAutomation entries to process.
"""
for entry in entries:
for conf in config.get(entry.conf_key, []):
await build_callback_automation(
parent,
entry.callback_method,
entry.args,
conf,
forwarder=entry.forwarder,
)

View File

@@ -111,42 +111,66 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder
),
automation.CallbackAutomation(
CONF_ON_TRIGGERED,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_TRIGGERED
),
),
automation.CallbackAutomation(
CONF_ON_ARMING,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(AlarmControlPanelState.ACP_STATE_ARMING),
),
automation.CallbackAutomation(
CONF_ON_PENDING,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_PENDING
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_HOME,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_HOME
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_NIGHT,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_NIGHT
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_AWAY,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_AWAY
),
),
automation.CallbackAutomation(
CONF_ON_DISARMED,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_DISARMED
),
),
automation.CallbackAutomation(CONF_ON_CLEARED, "add_on_cleared_callback"),
automation.CallbackAutomation(CONF_ON_CHIME, "add_on_chime_callback"),
automation.CallbackAutomation(CONF_ON_READY, "add_on_ready_callback"),
)
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder
)
_STATE_ENTER_MAP = {
CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED,
CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING,
CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING,
CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME,
CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT,
CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY,
CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED,
}
for conf_key, state_enum in _STATE_ENTER_MAP.items():
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
forwarder=StateEnterForwarder.template(state_enum),
)
for conf in config.get(CONF_ON_CLEARED, []):
await automation.build_callback_automation(
var, "add_on_cleared_callback", [], conf
)
for conf in config.get(CONF_ON_CHIME, []):
await automation.build_callback_automation(
var, "add_on_chime_callback", [], conf
)
for conf in config.get(CONF_ON_READY, []):
await automation.build_callback_automation(
var, "add_on_ready_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):

View File

@@ -531,16 +531,31 @@ def binary_sensor_schema(
return _BINARY_SENSOR_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_PRESS,
"add_on_state_callback",
forwarder=automation.TriggerOnTrueForwarder,
),
automation.CallbackAutomation(
CONF_ON_RELEASE,
"add_on_state_callback",
forwarder=automation.TriggerOnFalseForwarder,
),
automation.CallbackAutomation(
CONF_ON_STATE, "add_on_state_callback", [(bool, "x")]
),
automation.CallbackAutomation(
CONF_ON_STATE_CHANGE,
"add_full_state_callback",
[(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")],
),
)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_binary_sensor_automations(var, config):
for conf_key, forwarder in (
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
for conf in config.get(CONF_ON_CLICK, []):
trigger = cg.new_Pvariable(
@@ -572,22 +587,6 @@ async def _build_binary_sensor_automations(var, config):
await cg.register_component(trigger, conf)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
await automation.build_callback_automation(
var,
"add_full_state_callback",
[
(cg.optional.template(bool), "x_previous"),
(cg.optional.template(bool), "x"),
],
conf,
)
@setup_entity("binary_sensor")
async def setup_binary_sensor_core_(var, config):

View File

@@ -79,12 +79,14 @@ def button_schema(
return _BUTTON_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_PRESS, "add_on_press_callback"),
)
@setup_entity("button")
async def setup_button_core_(var, config):
for conf in config.get(CONF_ON_PRESS, []):
await automation.build_callback_automation(
var, "add_on_press_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
setup_device_class(config)

View File

@@ -64,15 +64,19 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_FINISHED_PLAYBACK, "add_on_finished_playback_callback"
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
await automation.build_callback_automation(
var, "add_on_finished_playback_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(

View File

@@ -82,12 +82,16 @@ def event_schema(
return _EVENT_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_EVENT, "add_on_event_callback", [(cg.StringRef, "event_type")]
),
)
@setup_entity("event")
async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []):
await automation.build_callback_automation(
var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
cg.add(var.set_event_types(event_types))

View File

@@ -38,33 +38,30 @@ CONFIG_SCHEMA = (
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_CUSTOM, "add_custom_callback", [(cg.std_string, "x")]
),
automation.CallbackAutomation(CONF_ON_LED, "add_led_state_callback", [(bool, "x")]),
automation.CallbackAutomation(
CONF_ON_DEVICE_INFORMATION,
"add_device_infomation_callback",
[(cg.std_string, "x")],
),
automation.CallbackAutomation(
CONF_ON_SLOPE, "add_slope_callback", [(cg.std_string, "x")]
),
automation.CallbackAutomation(
CONF_ON_CALIBRATION, "add_calibration_callback", [(cg.std_string, "x")]
),
automation.CallbackAutomation(CONF_ON_T, "add_t_callback", [(cg.std_string, "x")]),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
await i2c.register_i2c_device(var, config)
for conf in config.get(CONF_ON_CUSTOM, []):
await automation.build_callback_automation(
var, "add_custom_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_LED, []):
await automation.build_callback_automation(
var, "add_led_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_DEVICE_INFORMATION, []):
await automation.build_callback_automation(
var, "add_device_infomation_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_SLOPE, []):
await automation.build_callback_automation(
var, "add_slope_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_CALIBRATION, []):
await automation.build_callback_automation(
var, "add_calibration_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_T, []):
await automation.build_callback_automation(
var, "add_t_callback", [(cg.std_string, "x")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)

View File

@@ -73,6 +73,15 @@ def _final_validate(config):
FINAL_VALIDATE_SCHEMA = _final_validate
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_INCREMENT,
"add_increment_callback",
[(cg.uint8, "x"), (cg.uint8, "target")],
),
)
async def to_code(config):
if reset_count := config.get(CONF_RESETS_REQUIRED):
var = cg.new_Pvariable(
@@ -81,10 +90,4 @@ async def to_code(config):
config[CONF_MAX_DELAY].total_seconds,
)
await cg.register_component(var, config)
for conf in config.get(CONF_ON_INCREMENT, []):
await automation.build_callback_automation(
var,
"add_increment_callback",
[(cg.uint8, "x"), (cg.uint8, "target")],
conf,
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)

View File

@@ -116,6 +116,44 @@ CONFIG_SCHEMA = cv.All(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_FINGER_SCAN_START, "add_on_finger_scan_start_callback"
),
automation.CallbackAutomation(
CONF_ON_FINGER_SCAN_MATCHED,
"add_on_finger_scan_matched_callback",
[(cg.uint16, "finger_id"), (cg.uint16, "confidence")],
),
automation.CallbackAutomation(
CONF_ON_FINGER_SCAN_UNMATCHED,
"add_on_finger_scan_unmatched_callback",
),
automation.CallbackAutomation(
CONF_ON_FINGER_SCAN_MISPLACED,
"add_on_finger_scan_misplaced_callback",
),
automation.CallbackAutomation(
CONF_ON_FINGER_SCAN_INVALID, "add_on_finger_scan_invalid_callback"
),
automation.CallbackAutomation(
CONF_ON_ENROLLMENT_SCAN,
"add_on_enrollment_scan_callback",
[(cg.uint8, "scan_num"), (cg.uint16, "finger_id")],
),
automation.CallbackAutomation(
CONF_ON_ENROLLMENT_DONE,
"add_on_enrollment_done_callback",
[(cg.uint16, "finger_id")],
),
automation.CallbackAutomation(
CONF_ON_ENROLLMENT_FAILED,
"add_on_enrollment_failed_callback",
[(cg.uint16, "finger_id")],
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -140,44 +178,7 @@ async def to_code(config):
idle_period_to_sleep_ms = config[CONF_IDLE_PERIOD_TO_SLEEP]
cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms))
for conf in config.get(CONF_ON_FINGER_SCAN_START, []):
await automation.build_callback_automation(
var, "add_on_finger_scan_start_callback", [], conf
)
for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
await automation.build_callback_automation(
var,
"add_on_finger_scan_matched_callback",
[(cg.uint16, "finger_id"), (cg.uint16, "confidence")],
conf,
)
for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []):
await automation.build_callback_automation(
var, "add_on_finger_scan_unmatched_callback", [], conf
)
for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []):
await automation.build_callback_automation(
var, "add_on_finger_scan_misplaced_callback", [], conf
)
for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []):
await automation.build_callback_automation(
var, "add_on_finger_scan_invalid_callback", [], conf
)
for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
await automation.build_callback_automation(
var,
"add_on_enrollment_scan_callback",
[(cg.uint8, "scan_num"), (cg.uint16, "finger_id")],
conf,
)
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
await automation.build_callback_automation(
var, "add_on_enrollment_done_callback", [(cg.uint16, "finger_id")], conf
)
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
await automation.build_callback_automation(
var, "add_on_enrollment_failed_callback", [(cg.uint16, "finger_id")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(

View File

@@ -456,6 +456,25 @@ def _final_validate(config):
FINAL_VALIDATE_SCHEMA = _final_validate
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_ALARM_START,
"add_alarm_start_callback",
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
),
automation.CallbackAutomation(
CONF_ON_ALARM_END,
"add_alarm_end_callback",
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
),
automation.CallbackAutomation(
CONF_ON_STATUS_MESSAGE,
"add_status_message_callback",
[(cg.const_char_ptr, "data"), (cg.size_t, "data_size")],
),
)
async def to_code(config):
cg.add(haier_ns.init_haier_protocol_logging())
var = await climate.new_climate(config)
@@ -497,26 +516,6 @@ async def to_code(config):
cg.add(
var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE])
)
for conf in config.get(CONF_ON_ALARM_START, []):
await automation.build_callback_automation(
var,
"add_alarm_start_callback",
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
conf,
)
for conf in config.get(CONF_ON_ALARM_END, []):
await automation.build_callback_automation(
var,
"add_alarm_end_callback",
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
conf,
)
for conf in config.get(CONF_ON_STATUS_MESSAGE, []):
await automation.build_callback_automation(
var,
"add_status_message_callback",
[(cg.const_char_ptr, "data"), (cg.size_t, "data_size")],
conf,
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
# https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.31")

View File

@@ -52,58 +52,53 @@ CONFIG_SCHEMA = cv.All(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_FACE_SCAN_MATCHED,
"add_on_face_scan_matched_callback",
[(cg.int16, "face_id"), (cg.std_string, "name")],
),
automation.CallbackAutomation(
CONF_ON_FACE_SCAN_UNMATCHED, "add_on_face_scan_unmatched_callback"
),
automation.CallbackAutomation(
CONF_ON_FACE_SCAN_INVALID,
"add_on_face_scan_invalid_callback",
[(cg.uint8, "error")],
),
automation.CallbackAutomation(
CONF_ON_FACE_INFO,
"add_on_face_info_callback",
[
(cg.int16, "status"),
(cg.int16, "left"),
(cg.int16, "top"),
(cg.int16, "right"),
(cg.int16, "bottom"),
(cg.int16, "yaw"),
(cg.int16, "pitch"),
(cg.int16, "roll"),
],
),
automation.CallbackAutomation(
CONF_ON_ENROLLMENT_DONE,
"add_on_enrollment_done_callback",
[(cg.int16, "face_id"), (cg.uint8, "direction")],
),
automation.CallbackAutomation(
CONF_ON_ENROLLMENT_FAILED,
"add_on_enrollment_failed_callback",
[(cg.uint8, "error")],
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []):
await automation.build_callback_automation(
var,
"add_on_face_scan_matched_callback",
[(cg.int16, "face_id"), (cg.std_string, "name")],
conf,
)
for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []):
await automation.build_callback_automation(
var, "add_on_face_scan_unmatched_callback", [], conf
)
for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []):
await automation.build_callback_automation(
var, "add_on_face_scan_invalid_callback", [(cg.uint8, "error")], conf
)
for conf in config.get(CONF_ON_FACE_INFO, []):
await automation.build_callback_automation(
var,
"add_on_face_info_callback",
[
(cg.int16, "status"),
(cg.int16, "left"),
(cg.int16, "top"),
(cg.int16, "right"),
(cg.int16, "bottom"),
(cg.int16, "yaw"),
(cg.int16, "pitch"),
(cg.int16, "roll"),
],
conf,
)
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
await automation.build_callback_automation(
var,
"add_on_enrollment_done_callback",
[(cg.int16, "face_id"), (cg.uint8, "direction")],
conf,
)
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
await automation.build_callback_automation(
var, "add_on_enrollment_failed_callback", [(cg.uint8, "error")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(

View File

@@ -44,11 +44,13 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_DATA, "add_on_data_callback"),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_DATA, []):
await automation.build_callback_automation(
var, "add_on_data_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)

View File

@@ -81,20 +81,23 @@ def lock_schema(
return _LOCK_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_LOCK,
"add_on_state_callback",
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_LOCKED),
),
automation.CallbackAutomation(
CONF_ON_UNLOCK,
"add_on_state_callback",
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_UNLOCKED),
),
)
@setup_entity("lock")
async def _setup_lock_core(var, config):
for conf_key, state_enum in (
(CONF_ON_LOCK, LockState.LOCK_STATE_LOCKED),
(CONF_ON_UNLOCK, LockState.LOCK_STATE_UNLOCKED),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
forwarder=LockStateForwarder.template(state_enum),
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)

View File

@@ -211,6 +211,16 @@ CONFIG_SCHEMA = cv.All(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_PS_HIGH_THRESHOLD, "add_on_ps_high_trigger_callback"
),
automation.CallbackAutomation(
CONF_ON_PS_LOW_THRESHOLD, "add_on_ps_low_trigger_callback"
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -240,14 +250,7 @@ async def to_code(config):
sens = await sensor.new_sensor(prox_cnt_config)
cg.add(var.set_proximity_counts_sensor(sens))
for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []):
await automation.build_callback_automation(
var, "add_on_ps_high_trigger_callback", [], conf
)
for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []):
await automation.build_callback_automation(
var, "add_on_ps_low_trigger_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
cg.add(var.set_ltr_type(config[CONF_TYPE]))

View File

@@ -201,6 +201,16 @@ CONFIG_SCHEMA = cv.All(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_PS_HIGH_THRESHOLD, "add_on_ps_high_trigger_callback"
),
automation.CallbackAutomation(
CONF_ON_PS_LOW_THRESHOLD, "add_on_ps_low_trigger_callback"
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -230,14 +240,7 @@ async def to_code(config):
sens = await sensor.new_sensor(prox_cnt_config)
cg.add(var.set_proximity_counts_sensor(sens))
for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []):
await automation.build_callback_automation(
var, "add_on_ps_high_trigger_callback", [], conf
)
for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []):
await automation.build_callback_automation(
var, "add_on_ps_low_trigger_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
cg.add(var.set_ltr_type(config[CONF_TYPE]))

View File

@@ -69,7 +69,7 @@ StateEnterForwarder = media_player_ns.class_("StateEnterForwarder")
MediaPlayerState = media_player_ns.enum("MediaPlayerState")
# State triggers: (config_key, state enum or None for any-state)
_STATE_TRIGGERS = [
_STATE_TRIGGERS = (
(CONF_ON_STATE, None),
(CONF_ON_IDLE, MediaPlayerState.MEDIA_PLAYER_STATE_IDLE),
(CONF_ON_PLAY, MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING),
@@ -77,7 +77,7 @@ _STATE_TRIGGERS = [
(CONF_ON_ANNOUNCEMENT, MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING),
(CONF_ON_TURN_ON, MediaPlayerState.MEDIA_PLAYER_STATE_ON),
(CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF),
]
)
# State conditions that all share the same schema and codegen handler
_STATE_CONDITIONS = [
@@ -102,17 +102,54 @@ VolumeSetAction = media_player_ns.class_(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder
),
automation.CallbackAutomation(
CONF_ON_IDLE,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
MediaPlayerState.MEDIA_PLAYER_STATE_IDLE
),
),
automation.CallbackAutomation(
CONF_ON_PLAY,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING
),
),
automation.CallbackAutomation(
CONF_ON_PAUSE,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED
),
),
automation.CallbackAutomation(
CONF_ON_ANNOUNCEMENT,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING
),
),
automation.CallbackAutomation(
CONF_ON_TURN_ON,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_ON),
),
automation.CallbackAutomation(
CONF_ON_TURN_OFF,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_OFF),
),
)
@setup_entity("media_player")
async def setup_media_player_core_(var, config):
for conf_key, state_enum in _STATE_TRIGGERS:
for conf in config.get(conf_key, []):
if state_enum is None:
forwarder = StateAnyForwarder
else:
forwarder = StateEnterForwarder.template(state_enum)
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
async def register_media_player(var, config):

View File

@@ -205,6 +205,25 @@ async def add_modbus_base_properties(
cg.add(var.set_template(template_))
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_COMMAND_SENT,
"add_on_command_sent_callback",
[(cg.int_, "function_code"), (cg.int_, "address")],
),
automation.CallbackAutomation(
CONF_ON_ONLINE,
"add_on_online_callback",
[(cg.int_, "function_code"), (cg.int_, "address")],
),
automation.CallbackAutomation(
CONF_ON_OFFLINE,
"add_on_offline_callback",
[(cg.int_, "function_code"), (cg.int_, "address")],
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
@@ -257,27 +276,7 @@ async def to_code(config):
)
cg.add(var.add_server_register(server_register_var))
await register_modbus_device(var, config)
for conf in config.get(CONF_ON_COMMAND_SENT, []):
await automation.build_callback_automation(
var,
"add_on_command_sent_callback",
[(cg.int_, "function_code"), (cg.int_, "address")],
conf,
)
for conf in config.get(CONF_ON_ONLINE, []):
await automation.build_callback_automation(
var,
"add_on_online_callback",
[(cg.int_, "function_code"), (cg.int_, "address")],
conf,
)
for conf in config.get(CONF_ON_OFFLINE, []):
await automation.build_callback_automation(
var,
"add_on_offline_callback",
[(cg.int_, "function_code"), (cg.int_, "address")],
conf,
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
async def register_modbus_device(var, config):

View File

@@ -144,6 +144,28 @@ async def nextion_set_brightness_to_code(config, action_id, template_arg, args):
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_SETUP, "add_setup_state_callback"),
automation.CallbackAutomation(CONF_ON_SLEEP, "add_sleep_state_callback"),
automation.CallbackAutomation(CONF_ON_WAKE, "add_wake_state_callback"),
automation.CallbackAutomation(
CONF_ON_PAGE, "add_new_page_callback", [(cg.uint8, "x")]
),
automation.CallbackAutomation(
CONF_ON_TOUCH,
"add_touch_event_callback",
[
(cg.uint8, "page_id"),
(cg.uint8, "component_id"),
(cg.bool_, "touch_event"),
],
),
automation.CallbackAutomation(
CONF_ON_BUFFER_OVERFLOW, "add_buffer_overflow_event_callback"
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await uart.register_uart_device(var, config)
@@ -232,34 +254,4 @@ async def to_code(config):
await display.register_display(var, config)
for conf in config.get(CONF_ON_SETUP, []):
await automation.build_callback_automation(
var, "add_setup_state_callback", [], conf
)
for conf in config.get(CONF_ON_SLEEP, []):
await automation.build_callback_automation(
var, "add_sleep_state_callback", [], conf
)
for conf in config.get(CONF_ON_WAKE, []):
await automation.build_callback_automation(
var, "add_wake_state_callback", [], conf
)
for conf in config.get(CONF_ON_PAGE, []):
await automation.build_callback_automation(
var, "add_new_page_callback", [(cg.uint8, "x")], conf
)
for conf in config.get(CONF_ON_TOUCH, []):
await automation.build_callback_automation(
var,
"add_touch_event_callback",
[
(cg.uint8, "page_id"),
(cg.uint8, "component_id"),
(cg.bool_, "touch_event"),
],
conf,
)
for conf in config.get(CONF_ON_BUFFER_OVERFLOW, []):
await automation.build_callback_automation(
var, "add_buffer_overflow_event_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)

View File

@@ -243,12 +243,16 @@ def number_schema(
return _NUMBER_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_VALUE, "add_on_state_callback", [(float, "x")]
),
)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_number_automations(var, config):
for conf in config.get(CONF_ON_VALUE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [(float, "x")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
for conf in config.get(CONF_ON_VALUE_RANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await cg.register_component(trigger, conf)

View File

@@ -105,6 +105,14 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
async def to_code(config):
# Use the enhanced helper function to get all runtime image parameters
settings = await runtime_image.process_runtime_image_config(config)
@@ -139,12 +147,4 @@ async def to_code(config):
else:
cg.add(var.add_request_header(key, value))
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
await automation.build_callback_automation(
var, "add_on_finished_callback", [(bool, "cached")], conf
)
for conf in config.get(CONF_ON_ERROR, []):
await automation.build_callback_automation(
var, "add_on_error_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)

View File

@@ -49,6 +49,13 @@ def CONFIG_SCHEMA(conf):
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
),
)
async def setup_pn532(var, config):
await cg.register_component(var, config)
@@ -66,10 +73,7 @@ async def setup_pn532(var, config):
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
)
for conf in config.get(CONF_ON_FINISHED_WRITE, []):
await automation.build_callback_automation(
var, "add_on_finished_write_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_condition(

View File

@@ -164,6 +164,16 @@ async def pn7150_simple_action_to_code(config, action_id, template_arg, args):
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback"
),
automation.CallbackAutomation(
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
),
)
async def setup_pn7150(var, config):
await cg.register_component(var, config)
@@ -194,15 +204,7 @@ async def setup_pn7150(var, config):
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
)
for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []):
await automation.build_callback_automation(
var, "add_on_emulated_tag_scan_callback", [], conf
)
for conf in config.get(CONF_ON_FINISHED_WRITE, []):
await automation.build_callback_automation(
var, "add_on_finished_write_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_condition(

View File

@@ -168,6 +168,16 @@ async def pn7160_simple_action_to_code(config, action_id, template_arg, args):
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback"
),
automation.CallbackAutomation(
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
),
)
async def setup_pn7160(var, config):
await cg.register_component(var, config)
@@ -206,15 +216,7 @@ async def setup_pn7160(var, config):
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
)
for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []):
await automation.build_callback_automation(
var, "add_on_emulated_tag_scan_callback", [], conf
)
for conf in config.get(CONF_ON_FINISHED_WRITE, []):
await automation.build_callback_automation(
var, "add_on_finished_write_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_condition(

View File

@@ -67,22 +67,26 @@ CONFIG_SCHEMA = cv.All(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_CODE_RECEIVED,
"add_on_code_received_callback",
[(RFBridgeData, "data")],
),
automation.CallbackAutomation(
CONF_ON_ADVANCED_CODE_RECEIVED,
"add_on_advanced_code_received_callback",
[(RFBridgeAdvancedData, "data")],
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_CODE_RECEIVED, []):
await automation.build_callback_automation(
var, "add_on_code_received_callback", [(RFBridgeData, "data")], conf
)
for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []):
await automation.build_callback_automation(
var,
"add_on_advanced_code_received_callback",
[(RFBridgeAdvancedData, "data")],
conf,
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema(

View File

@@ -84,6 +84,14 @@ CONFIG_SCHEMA = cv.All(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_CLOCKWISE, "add_on_clockwise_callback"),
automation.CallbackAutomation(
CONF_ON_ANTICLOCKWISE, "add_on_anticlockwise_callback"
),
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
@@ -104,14 +112,7 @@ async def to_code(config):
if CONF_MAX_VALUE in config:
cg.add(var.set_max_value(config[CONF_MAX_VALUE]))
for conf in config.get(CONF_ON_CLOCKWISE, []):
await automation.build_callback_automation(
var, "add_on_clockwise_callback", [], conf
)
for conf in config.get(CONF_ON_ANTICLOCKWISE, []):
await automation.build_callback_automation(
var, "add_on_anticlockwise_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(

View File

@@ -71,6 +71,13 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_FINISHED_PLAYBACK, "add_on_finished_playback_callback"
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -86,10 +93,7 @@ async def to_code(config):
cg.add(var.set_gain(config[CONF_GAIN]))
for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
await automation.build_callback_automation(
var, "add_on_finished_playback_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(

View File

@@ -65,18 +65,22 @@ async def safe_mode_mark_successful_to_code(config, action_id, template_arg, arg
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_SAFE_MODE, "add_on_safe_mode_callback"),
)
@coroutine_with_priority(CoroPriority.APPLICATION)
async def to_code(config):
if not config[CONF_DISABLED]:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if on_safe_mode_config := config.get(CONF_ON_SAFE_MODE):
if config.get(CONF_ON_SAFE_MODE):
cg.add_define("USE_SAFE_MODE_CALLBACK")
for conf in on_safe_mode_config:
await automation.build_callback_automation(
var, "add_on_safe_mode_callback", [], conf
)
await automation.build_callback_automations(
var, config, _CALLBACK_AUTOMATIONS
)
condition = var.should_enter_safe_mode(
config[CONF_NUM_ATTEMPTS],

View File

@@ -892,16 +892,19 @@ async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_VALUE, "add_on_state_callback", [(float, "x")]
),
automation.CallbackAutomation(
CONF_ON_RAW_VALUE, "add_on_raw_state_callback", [(float, "x")]
),
)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_sensor_automations(var, config):
for conf_key, callback in (
(CONF_ON_VALUE, "add_on_state_callback"),
(CONF_ON_RAW_VALUE, "add_on_raw_state_callback"),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, callback, [(float, "x")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
for conf in config.get(CONF_ON_VALUE_RANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await cg.register_component(trigger, conf)

View File

@@ -48,34 +48,37 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_SMS_RECEIVED,
"add_on_sms_received_callback",
[(cg.std_string, "message"), (cg.std_string, "sender")],
),
automation.CallbackAutomation(
CONF_ON_INCOMING_CALL,
"add_on_incoming_call_callback",
[(cg.std_string, "caller_id")],
),
automation.CallbackAutomation(
CONF_ON_CALL_CONNECTED, "add_on_call_connected_callback"
),
automation.CallbackAutomation(
CONF_ON_CALL_DISCONNECTED, "add_on_call_disconnected_callback"
),
automation.CallbackAutomation(
CONF_ON_USSD_RECEIVED,
"add_on_ussd_received_callback",
[(cg.std_string, "ussd")],
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_SMS_RECEIVED, []):
await automation.build_callback_automation(
var,
"add_on_sms_received_callback",
[(cg.std_string, "message"), (cg.std_string, "sender")],
conf,
)
for conf in config.get(CONF_ON_INCOMING_CALL, []):
await automation.build_callback_automation(
var, "add_on_incoming_call_callback", [(cg.std_string, "caller_id")], conf
)
for conf in config.get(CONF_ON_CALL_CONNECTED, []):
await automation.build_callback_automation(
var, "add_on_call_connected_callback", [], conf
)
for conf in config.get(CONF_ON_CALL_DISCONNECTED, []):
await automation.build_callback_automation(
var, "add_on_call_disconnected_callback", [], conf
)
for conf in config.get(CONF_ON_USSD_RECEIVED, []):
await automation.build_callback_automation(
var, "add_on_ussd_received_callback", [(cg.std_string, "ussd")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
SIM800L_SEND_SMS_SCHEMA = cv.Schema(

View File

@@ -31,23 +31,26 @@ CONFIG_SCHEMA = (
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DATA,
"add_on_data_callback",
[
(
cg.std_vector.template(cg.uint8).operator("ref").operator("const"),
"bytes",
),
(cg.bool_, "valid"),
],
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_DATA, []):
await automation.build_callback_automation(
var,
"add_on_data_callback",
[
(
cg.std_vector.template(cg.uint8).operator("ref").operator("const"),
"bytes",
),
(cg.bool_, "valid"),
],
conf,
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
def obis_code(value):

View File

@@ -121,17 +121,26 @@ def switch_schema(
return _SWITCH_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_STATE, "add_on_state_callback", [(bool, "x")]
),
automation.CallbackAutomation(
CONF_ON_TURN_ON,
"add_on_state_callback",
forwarder=automation.TriggerOnTrueForwarder,
),
automation.CallbackAutomation(
CONF_ON_TURN_OFF,
"add_on_state_callback",
forwarder=automation.TriggerOnFalseForwarder,
),
)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_switch_automations(var, config):
for conf_key, args, forwarder in (
(CONF_ON_STATE, [(bool, "x")], None),
(CONF_ON_TURN_ON, [], automation.TriggerOnTrueForwarder),
(CONF_ON_TURN_OFF, [], automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", args, conf, forwarder=forwarder
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@setup_entity("switch")

View File

@@ -184,16 +184,19 @@ async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_VALUE, "add_on_state_callback", [(cg.std_string, "x")]
),
automation.CallbackAutomation(
CONF_ON_RAW_VALUE, "add_on_raw_state_callback", [(cg.std_string, "x")]
),
)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_text_sensor_automations(var, config):
for conf_key, callback in (
(CONF_ON_VALUE, "add_on_state_callback"),
(CONF_ON_RAW_VALUE, "add_on_raw_state_callback"),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, callback, [(cg.std_string, "x")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@setup_entity("text_sensor")

View File

@@ -1,14 +1,16 @@
"""Tests for esphome.automation module."""
from collections.abc import Generator
from unittest.mock import patch
from unittest.mock import AsyncMock, call, patch
import pytest
from esphome.automation import (
CallbackAutomation,
TriggerForwarder,
TriggerOnFalseForwarder,
TriggerOnTrueForwarder,
build_callback_automations,
has_non_synchronous_actions,
)
from esphome.cpp_generator import MockObj, RawExpression
@@ -254,3 +256,222 @@ def test_trigger_forwarder_custom_type() -> None:
custom = MockObj("MyForwarder", "")
result = _build_forwarder("auto_1", [], forwarder=custom)
assert result == "MyForwarder{auto_1}"
@pytest.fixture
def mock_build_callback() -> Generator[AsyncMock]:
"""Patch build_callback_automation to capture calls."""
with patch(
"esphome.automation.build_callback_automation", new_callable=AsyncMock
) as mock:
yield mock
@pytest.mark.asyncio
async def test_build_callback_automations_empty_entries(
mock_build_callback: AsyncMock,
) -> None:
"""No entries means no calls."""
parent = MockObj("var", "->")
await build_callback_automations(parent, {}, ())
mock_build_callback.assert_not_called()
@pytest.mark.asyncio
async def test_build_callback_automations_missing_config_key(
mock_build_callback: AsyncMock,
) -> None:
"""Entry present but config key missing -- no calls."""
parent = MockObj("var", "->")
await build_callback_automations(
parent,
{},
(CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),),
)
mock_build_callback.assert_not_called()
@pytest.mark.asyncio
async def test_build_callback_automations_single_entry(
mock_build_callback: AsyncMock,
) -> None:
"""Single entry with one config triggers one call."""
parent = MockObj("var", "->")
conf: dict[str, object] = {"automation_id": "auto_1", "then": []}
config: dict[str, list[dict[str, object]]] = {"on_state": [conf]}
await build_callback_automations(
parent,
config,
(CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),),
)
mock_build_callback.assert_called_once_with(
parent, "add_on_state_callback", [(bool, "x")], conf, forwarder=None
)
@pytest.mark.asyncio
async def test_build_callback_automations_multiple_configs(
mock_build_callback: AsyncMock,
) -> None:
"""Single entry with multiple configs triggers multiple calls."""
parent = MockObj("var", "->")
conf1: dict[str, object] = {"automation_id": "auto_1", "then": []}
conf2: dict[str, object] = {"automation_id": "auto_2", "then": []}
config: dict[str, list[dict[str, object]]] = {"on_state": [conf1, conf2]}
await build_callback_automations(
parent,
config,
(CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),),
)
assert mock_build_callback.call_count == 2
mock_build_callback.assert_any_call(
parent, "add_on_state_callback", [(bool, "x")], conf1, forwarder=None
)
mock_build_callback.assert_any_call(
parent, "add_on_state_callback", [(bool, "x")], conf2, forwarder=None
)
@pytest.mark.asyncio
async def test_build_callback_automations_multiple_entries(
mock_build_callback: AsyncMock,
) -> None:
"""Multiple entries each with one config."""
parent = MockObj("var", "->")
conf_a: dict[str, object] = {"automation_id": "auto_a", "then": []}
conf_b: dict[str, object] = {"automation_id": "auto_b", "then": []}
config: dict[str, list[dict[str, object]]] = {
"on_value": [conf_a],
"on_raw_value": [conf_b],
}
await build_callback_automations(
parent,
config,
(
CallbackAutomation("on_value", "add_on_value_callback", [(float, "x")]),
CallbackAutomation(
"on_raw_value", "add_on_raw_value_callback", [(float, "x")]
),
),
)
assert mock_build_callback.call_count == 2
assert mock_build_callback.call_args_list == [
call(parent, "add_on_value_callback", [(float, "x")], conf_a, forwarder=None),
call(
parent, "add_on_raw_value_callback", [(float, "x")], conf_b, forwarder=None
),
]
@pytest.mark.asyncio
async def test_build_callback_automations_with_forwarder(
mock_build_callback: AsyncMock,
) -> None:
"""Entry with forwarder passes it through."""
parent = MockObj("var", "->")
conf: dict[str, object] = {"automation_id": "auto_1", "then": []}
config: dict[str, list[dict[str, object]]] = {"on_press": [conf]}
await build_callback_automations(
parent,
config,
(
CallbackAutomation(
"on_press", "add_on_state_callback", forwarder=TriggerOnTrueForwarder
),
),
)
mock_build_callback.assert_called_once_with(
parent, "add_on_state_callback", [], conf, forwarder=TriggerOnTrueForwarder
)
@pytest.mark.asyncio
async def test_build_callback_automations_mixed_entries(
mock_build_callback: AsyncMock,
) -> None:
"""Mix of entries with args, forwarders, and defaults."""
parent = MockObj("var", "->")
conf_state: dict[str, object] = {"automation_id": "auto_1", "then": []}
conf_press: dict[str, object] = {"automation_id": "auto_2", "then": []}
conf_release: dict[str, object] = {"automation_id": "auto_3", "then": []}
config: dict[str, list[dict[str, object]]] = {
"on_state": [conf_state],
"on_press": [conf_press],
"on_release": [conf_release],
}
await build_callback_automations(
parent,
config,
(
CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),
CallbackAutomation(
"on_press", "add_on_state_callback", forwarder=TriggerOnTrueForwarder
),
CallbackAutomation(
"on_release", "add_on_state_callback", forwarder=TriggerOnFalseForwarder
),
),
)
assert mock_build_callback.call_count == 3
assert mock_build_callback.call_args_list == [
call(
parent, "add_on_state_callback", [(bool, "x")], conf_state, forwarder=None
),
call(
parent,
"add_on_state_callback",
[],
conf_press,
forwarder=TriggerOnTrueForwarder,
),
call(
parent,
"add_on_state_callback",
[],
conf_release,
forwarder=TriggerOnFalseForwarder,
),
]
@pytest.mark.asyncio
async def test_build_callback_automations_skips_missing_keys(
mock_build_callback: AsyncMock,
) -> None:
"""Entries whose config keys are absent are silently skipped."""
parent = MockObj("var", "->")
conf: dict[str, object] = {"automation_id": "auto_1", "then": []}
config: dict[str, list[dict[str, object]]] = {"on_press": [conf]}
await build_callback_automations(
parent,
config,
(
CallbackAutomation(
"on_press", "add_on_state_callback", forwarder=TriggerOnTrueForwarder
),
CallbackAutomation(
"on_release", "add_on_state_callback", forwarder=TriggerOnFalseForwarder
),
),
)
mock_build_callback.assert_called_once_with(
parent, "add_on_state_callback", [], conf, forwarder=TriggerOnTrueForwarder
)
@pytest.mark.asyncio
async def test_build_callback_automations_defaults(
mock_build_callback: AsyncMock,
) -> None:
"""Verify CallbackAutomation with only required fields defaults args=[] and forwarder=None."""
parent = MockObj("var", "->")
conf: dict[str, object] = {"automation_id": "auto_1", "then": []}
config: dict[str, list[dict[str, object]]] = {"on_press": [conf]}
await build_callback_automations(
parent,
config,
(CallbackAutomation("on_press", "add_on_press_callback"),),
)
mock_build_callback.assert_called_once_with(
parent, "add_on_press_callback", [], conf, forwarder=None
)