diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py new file mode 100644 index 00000000000..6db5f16cf01 --- /dev/null +++ b/homeassistant/components/media_player/device_trigger.py @@ -0,0 +1,90 @@ +"""Provides device automations for Media player.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import state as state_trigger +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +TRIGGER_TYPES = {"turned_on", "turned_off", "idle", "paused", "playing"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Media player entities.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integration entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + triggers += [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: trigger, + } + for trigger in TRIGGER_TYPES + ] + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "turned_on": + to_state = STATE_ON + elif config[CONF_TYPE] == "turned_off": + to_state = STATE_OFF + elif config[CONF_TYPE] == "idle": + to_state = STATE_IDLE + elif config[CONF_TYPE] == "paused": + to_state = STATE_PAUSED + else: + to_state = STATE_PLAYING + + state_config = { + CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_trigger.CONF_TO: to_state, + } + state_config = state_trigger.TRIGGER_SCHEMA(state_config) + return await state_trigger.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 14f1eea131c..64841413f12 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -7,6 +7,13 @@ "is_idle": "{entity_name} is idle", "is_paused": "{entity_name} is paused", "is_playing": "{entity_name} is playing" + }, + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + "idle": "{entity_name} becomes idle", + "paused": "{entity_name} is paused", + "playing": "{entity_name} starts playing" } }, "state": { diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index 0f2cfaf2893..0cae565f7bb 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -7,7 +7,6 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, - assert_lists_same, async_get_device_automations, async_mock_service, mock_device_registry, @@ -55,7 +54,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): }, ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert_lists_same(triggers, expected_triggers) + + # Test triggers are either arcam_fmj specific or media_player entity triggers + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for expected_trigger in expected_triggers: + assert expected_trigger in triggers + for trigger in triggers: + assert trigger in expected_triggers or trigger["domain"] == "media_player" async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state): diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index cd3f57137bf..ebc50fda8bc 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -63,6 +63,7 @@ async def test_lg_tv(hass): assert device.sw_version == "04.71.04" assert device.via_device_id is None - # A TV doesn't have any triggers + # A TV has media player device triggers triggers = await async_get_device_automations(hass, "trigger", device.id) - assert triggers == [] + for trigger in triggers: + assert trigger["domain"] == "media_player" diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index 8cf6c635393..0dd75b9c357 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -10,7 +10,6 @@ from . import init_integration from tests.common import ( MockConfigEntry, - assert_lists_same, async_get_device_automations, async_mock_service, mock_device_registry, @@ -69,8 +68,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{MP_DOMAIN}.kodi_5678", }, ] + + # Test triggers are either kodi specific triggers or media_player entity triggers triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert_lists_same(triggers, expected_triggers) + for expected_trigger in expected_triggers: + assert expected_trigger in triggers + for trigger in triggers: + assert trigger in expected_triggers or trigger["domain"] == "media_player" async def test_if_fires_on_state_change(hass, calls, kodi_media_player): diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py new file mode 100644 index 00000000000..93d9127f8b8 --- /dev/null +++ b/tests/components/media_player/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The tests for Media player device triggers.""" +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.media_player import DOMAIN +from homeassistant.const import ( + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a media player.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + + trigger_types = {"turned_on", "turned_off", "idle", "paused", "playing"} + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger, + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + for trigger in trigger_types + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test triggers firing.""" + hass.states.async_set("media_player.entity", STATE_OFF) + + data_template = ( + "{label} - {{{{ trigger.platform}}}} - " + "{{{{ trigger.entity_id}}}} - {{{{ trigger.from_state.state}}}} - " + "{{{{ trigger.to_state.state}}}} - {{{{ trigger.for }}}}" + ) + trigger_types = {"turned_on", "turned_off", "idle", "paused", "playing"} + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": trigger, + }, + "action": { + "service": "test.automation", + "data_template": {"some": data_template.format(label=trigger)}, + }, + } + for trigger in trigger_types + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("media_player.entity", STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 1 + assert ( + calls[0].data["some"] + == "turned_on - device - media_player.entity - off - on - None" + ) + + # Fake that the entity is turning off. + hass.states.async_set("media_player.entity", STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert ( + calls[1].data["some"] + == "turned_off - device - media_player.entity - on - off - None" + ) + + # Fake that the entity becomes idle. + hass.states.async_set("media_player.entity", STATE_IDLE) + await hass.async_block_till_done() + assert len(calls) == 3 + assert ( + calls[2].data["some"] + == "idle - device - media_player.entity - off - idle - None" + ) + + # Fake that the entity starts playing. + hass.states.async_set("media_player.entity", STATE_PLAYING) + await hass.async_block_till_done() + assert len(calls) == 4 + assert ( + calls[3].data["some"] + == "playing - device - media_player.entity - idle - playing - None" + ) + + # Fake that the entity is paused. + hass.states.async_set("media_player.entity", STATE_PAUSED) + await hass.async_block_till_done() + assert len(calls) == 5 + assert ( + calls[4].data["some"] + == "paused - device - media_player.entity - playing - paused - None" + )