From b46640a1c219ec9f3dbdf92ec1c61a1593820205 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 22:45:33 +0000 Subject: [PATCH] Add media_player.paused trigger --- .../components/media_player/strings.json | 5 ++ .../components/media_player/trigger.py | 76 +++++++++++++++++++ .../components/media_player/triggers.yaml | 5 ++ tests/components/media_player/test_trigger.py | 43 +++++++++++ 4 files changed, 129 insertions(+) diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index b9d4bdffe364..65526902262f 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -389,6 +389,11 @@ } }, "name": "When a media player starts playing" + }, + "paused": { + "description": "Triggers when a media player pauses.", + "description_configured": "Triggers when a media player pauses", + "name": "When a media player pauses" } } } diff --git a/homeassistant/components/media_player/trigger.py b/homeassistant/components/media_player/trigger.py index bb972be6f818..f29ce2aacd18 100644 --- a/homeassistant/components/media_player/trigger.py +++ b/homeassistant/components/media_player/trigger.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_OPTIONS, CONF_TARGET, STATE_OFF, + STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, ) @@ -48,6 +49,13 @@ PLAYING_TRIGGER_SCHEMA = vol.Schema( } ) +PAUSED_TRIGGER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_OPTIONS, default={}): {}, + vol.Required(CONF_TARGET): cv.TARGET_FIELDS, + } +) + class MediaPlayerTurnsOnTrigger(Trigger): """Trigger for when a media player turns on.""" @@ -259,10 +267,78 @@ class MediaPlayerPlayingTrigger(Trigger): ) +class MediaPlayerPausedTrigger(Trigger): + """Trigger for when a media player pauses.""" + + @override + @classmethod + async def async_validate_config( + cls, hass: HomeAssistant, config: ConfigType + ) -> ConfigType: + """Validate config.""" + return cast(ConfigType, PAUSED_TRIGGER_SCHEMA(config)) + + def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None: + """Initialize the media player paused trigger.""" + super().__init__(hass, config) + if TYPE_CHECKING: + assert config.target is not None + self._target = config.target + + @override + async def async_attach_runner( + self, run_action: TriggerActionRunner + ) -> CALLBACK_TYPE: + """Attach the trigger to an action runner.""" + + @callback + def state_change_listener( + target_state_change_data: TargetStateChangedData, + ) -> None: + """Listen for state changes and call action.""" + event = target_state_change_data.state_change_event + entity_id = event.data["entity_id"] + from_state = event.data["old_state"] + to_state = event.data["new_state"] + + # Ignore unavailable states + if to_state is None or to_state.state == STATE_UNAVAILABLE: + return + + # Trigger when pausing + if ( + from_state is not None + and from_state.state != STATE_PAUSED + and to_state.state == STATE_PAUSED + ): + run_action( + { + ATTR_ENTITY_ID: entity_id, + "from_state": from_state, + "to_state": to_state, + }, + f"media player {entity_id} paused", + event.context, + ) + + def entity_filter(entities: set[str]) -> set[str]: + """Filter entities of this domain.""" + return { + entity_id + for entity_id in entities + if split_entity_id(entity_id)[0] == DOMAIN + } + + return async_track_target_selector_state_change_event( + self._hass, self._target, state_change_listener, entity_filter + ) + + TRIGGERS: dict[str, type[Trigger]] = { "turns_on": MediaPlayerTurnsOnTrigger, "turns_off": MediaPlayerTurnsOffTrigger, "playing": MediaPlayerPlayingTrigger, + "paused": MediaPlayerPausedTrigger, } diff --git a/homeassistant/components/media_player/triggers.yaml b/homeassistant/components/media_player/triggers.yaml index 8414eb7557cf..90f7eeff4f96 100644 --- a/homeassistant/components/media_player/triggers.yaml +++ b/homeassistant/components/media_player/triggers.yaml @@ -21,3 +21,8 @@ playing: multiple: true custom_value: true options: [] + +paused: + target: + entity: + domain: media_player diff --git a/tests/components/media_player/test_trigger.py b/tests/components/media_player/test_trigger.py index 4d0cbb2f6041..c207df6b6e69 100644 --- a/tests/components/media_player/test_trigger.py +++ b/tests/components/media_player/test_trigger.py @@ -208,3 +208,46 @@ async def test_playing_trigger_with_media_content_type_filter( ) await hass.async_block_till_done() assert len(service_calls) == 0 + + +async def test_paused_trigger( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: + """Test that the paused trigger fires when a media player pauses.""" + entity_id = "media_player.test" + await async_setup_component(hass, "media_player", {}) + + # Set initial state to playing + hass.states.async_set(entity_id, STATE_PLAYING) + await hass.async_block_till_done() + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "triggers": { + "trigger": "media_player.paused", + "target": {CONF_ENTITY_ID: entity_id}, + }, + "actions": { + "action": "test.automation", + "data": { + CONF_ENTITY_ID: "{{ trigger.entity_id }}", + }, + }, + } + }, + ) + + # Pause - should trigger + hass.states.async_set(entity_id, STATE_PAUSED) + await hass.async_block_till_done() + assert len(service_calls) == 1 + assert service_calls[0].data[CONF_ENTITY_ID] == entity_id + service_calls.clear() + + # Already paused - should not trigger + hass.states.async_set(entity_id, STATE_PAUSED) + await hass.async_block_till_done() + assert len(service_calls) == 0