diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 79688130a36..47d365ff75a 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -6,6 +6,7 @@ from collections.abc import Iterable from typing import Any from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, @@ -33,6 +34,7 @@ from .const import ( SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, + MediaPlayerEntityFeature, ) # mypy: allow-untyped-defs @@ -46,6 +48,8 @@ async def _async_reproduce_states( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" + cur_state = hass.states.get(state.entity_id) + features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0 async def call_service(service: str, keys: Iterable) -> None: """Call service with set of attributes given.""" @@ -59,28 +63,48 @@ async def _async_reproduce_states( ) if state.state == STATE_OFF: - await call_service(SERVICE_TURN_OFF, []) + if features & MediaPlayerEntityFeature.TURN_OFF: + await call_service(SERVICE_TURN_OFF, []) # entities that are off have no other attributes to restore return - if state.state in ( - STATE_ON, - STATE_PLAYING, - STATE_IDLE, - STATE_PAUSED, + if ( + state.state + in ( + STATE_ON, + STATE_PLAYING, + STATE_IDLE, + STATE_PAUSED, + ) + and features & MediaPlayerEntityFeature.TURN_ON ): await call_service(SERVICE_TURN_ON, []) - if ATTR_MEDIA_VOLUME_LEVEL in state.attributes: + cur_state = hass.states.get(state.entity_id) + features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0 + + if ( + ATTR_MEDIA_VOLUME_LEVEL in state.attributes + and features & MediaPlayerEntityFeature.VOLUME_SET + ): await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL]) - if ATTR_MEDIA_VOLUME_MUTED in state.attributes: + if ( + ATTR_MEDIA_VOLUME_MUTED in state.attributes + and features & MediaPlayerEntityFeature.VOLUME_MUTE + ): await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED]) - if ATTR_INPUT_SOURCE in state.attributes: + if ( + ATTR_INPUT_SOURCE in state.attributes + and features & MediaPlayerEntityFeature.SELECT_SOURCE + ): await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE]) - if ATTR_SOUND_MODE in state.attributes: + if ( + ATTR_SOUND_MODE in state.attributes + and features & MediaPlayerEntityFeature.SELECT_SOUND_MODE + ): await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE]) already_playing = False @@ -88,18 +112,25 @@ async def _async_reproduce_states( if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and ( ATTR_MEDIA_CONTENT_ID in state.attributes ): - await call_service( - SERVICE_PLAY_MEDIA, - [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE], - ) + if features & MediaPlayerEntityFeature.PLAY_MEDIA: + await call_service( + SERVICE_PLAY_MEDIA, + [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE], + ) already_playing = True - if state.state == STATE_PLAYING and not already_playing: + if ( + not already_playing + and state.state == STATE_PLAYING + and features & MediaPlayerEntityFeature.PLAY + ): await call_service(SERVICE_MEDIA_PLAY, []) elif state.state == STATE_IDLE: - await call_service(SERVICE_MEDIA_STOP, []) + if features & MediaPlayerEntityFeature.STOP: + await call_service(SERVICE_MEDIA_STOP, []) elif state.state == STATE_PAUSED: - await call_service(SERVICE_MEDIA_PAUSE, []) + if features & MediaPlayerEntityFeature.PAUSE: + await call_service(SERVICE_MEDIA_PAUSE, []) async def async_reproduce_states( diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index ba0072bc2f8..c2e454d1a04 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -14,9 +14,11 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, + MediaPlayerEntityFeature, ) from homeassistant.components.media_player.reproduce_state import async_reproduce_states from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, @@ -39,31 +41,47 @@ ENTITY_2 = "media_player.test2" @pytest.mark.parametrize( - "service,state", + "service,state,supported_feature", [ - (SERVICE_TURN_ON, STATE_ON), - (SERVICE_TURN_OFF, STATE_OFF), - (SERVICE_MEDIA_PLAY, STATE_PLAYING), - (SERVICE_MEDIA_STOP, STATE_IDLE), - (SERVICE_MEDIA_PAUSE, STATE_PAUSED), + (SERVICE_TURN_ON, STATE_ON, MediaPlayerEntityFeature.TURN_ON), + (SERVICE_TURN_OFF, STATE_OFF, MediaPlayerEntityFeature.TURN_OFF), + (SERVICE_MEDIA_PLAY, STATE_PLAYING, MediaPlayerEntityFeature.PLAY), + (SERVICE_MEDIA_STOP, STATE_IDLE, MediaPlayerEntityFeature.STOP), + (SERVICE_MEDIA_PAUSE, STATE_PAUSED, MediaPlayerEntityFeature.PAUSE), ], ) -async def test_state(hass, service, state): +async def test_state(hass, service, state, supported_feature): """Test that we can turn a state into a service call.""" calls_1 = async_mock_service(hass, DOMAIN, service) if service != SERVICE_TURN_ON: async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + # Don't support the feature won't call the service + hass.states.async_set(ENTITY_1, "something", {ATTR_SUPPORTED_FEATURES: 0}) await async_reproduce_states(hass, [State(ENTITY_1, state)]) await hass.async_block_till_done() + assert len(calls_1) == 0 + hass.states.async_set( + ENTITY_1, "something", {ATTR_SUPPORTED_FEATURES: supported_feature} + ) + await async_reproduce_states(hass, [State(ENTITY_1, state)]) assert len(calls_1) == 1 assert calls_1[0].data == {"entity_id": ENTITY_1} async def test_turn_on_with_mode(hass): """Test that state with additional attributes call multiple services.""" + hass.states.async_set( + ENTITY_1, + "something", + { + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.SELECT_SOUND_MODE + }, + ) + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls_2 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE) @@ -82,6 +100,13 @@ async def test_turn_on_with_mode(hass): async def test_multiple_same_state(hass): """Test that multiple states with same state gets calls.""" + for entity in ENTITY_1, ENTITY_2: + hass.states.async_set( + entity, + "something", + {ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON}, + ) + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) await async_reproduce_states(hass, [State(ENTITY_1, "on"), State(ENTITY_2, "on")]) @@ -96,6 +121,16 @@ async def test_multiple_same_state(hass): async def test_multiple_different_state(hass): """Test that multiple states with different state gets calls.""" + for entity in ENTITY_1, ENTITY_2: + hass.states.async_set( + entity, + "something", + { + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + }, + ) + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) @@ -111,6 +146,12 @@ async def test_multiple_different_state(hass): async def test_state_with_context(hass): """Test that context is forwarded.""" + hass.states.async_set( + ENTITY_1, + "something", + {ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON}, + ) + calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) context = Context() @@ -126,6 +167,16 @@ async def test_state_with_context(hass): async def test_attribute_no_state(hass): """Test that no state service call is made with none state.""" + hass.states.async_set( + ENTITY_1, + "something", + { + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.SELECT_SOUND_MODE + }, + ) + calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) calls_3 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE) @@ -145,16 +196,38 @@ async def test_attribute_no_state(hass): @pytest.mark.parametrize( - "service,attribute", + "service,attribute,supported_feature", [ - (SERVICE_VOLUME_SET, ATTR_MEDIA_VOLUME_LEVEL), - (SERVICE_VOLUME_MUTE, ATTR_MEDIA_VOLUME_MUTED), - (SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE), - (SERVICE_SELECT_SOUND_MODE, ATTR_SOUND_MODE), + ( + SERVICE_VOLUME_SET, + ATTR_MEDIA_VOLUME_LEVEL, + MediaPlayerEntityFeature.VOLUME_SET, + ), + ( + SERVICE_VOLUME_MUTE, + ATTR_MEDIA_VOLUME_MUTED, + MediaPlayerEntityFeature.VOLUME_MUTE, + ), + ( + SERVICE_SELECT_SOURCE, + ATTR_INPUT_SOURCE, + MediaPlayerEntityFeature.SELECT_SOURCE, + ), + ( + SERVICE_SELECT_SOUND_MODE, + ATTR_SOUND_MODE, + MediaPlayerEntityFeature.SELECT_SOUND_MODE, + ), ], ) -async def test_attribute(hass, service, attribute): +async def test_attribute(hass, service, attribute, supported_feature): """Test that service call is made for each attribute.""" + hass.states.async_set( + ENTITY_1, + "something", + {ATTR_SUPPORTED_FEATURES: supported_feature}, + ) + calls_1 = async_mock_service(hass, DOMAIN, service) value = "dummy" @@ -168,7 +241,12 @@ async def test_attribute(hass, service, attribute): async def test_play_media(hass): - """Test that no state service call is made with none state.""" + """Test playing media.""" + hass.states.async_set( + ENTITY_1, + "something", + {ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY_MEDIA}, + ) calls_1 = async_mock_service(hass, DOMAIN, SERVICE_PLAY_MEDIA) value_1 = "dummy_1"