From 5c0d237c2dd083d2f6f00eed5dd3e1de0808fed4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 14 May 2020 23:24:19 +0200 Subject: [PATCH] Add device triggers to support setting turn_on event (#35456) * Add device triggers to support setting turn_on event * Add turn on event * Add unique_id based on config entry * Adjust tests for addition of uuid * Supported features are now always same * Switch to player_setup fixture that actually start platform * Update homeassistant/components/arcam_fmj/const.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/arcam_fmj/const.py | 2 + .../components/arcam_fmj/device_trigger.py | 70 ++++++++++++++ .../components/arcam_fmj/media_player.py | 22 +++-- .../components/arcam_fmj/strings.json | 7 ++ .../components/arcam_fmj/translations/en.json | 7 +- tests/components/arcam_fmj/conftest.py | 50 +++++++++- .../arcam_fmj/test_device_trigger.py | 91 +++++++++++++++++++ .../components/arcam_fmj/test_media_player.py | 30 +----- 8 files changed, 241 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/device_trigger.py create mode 100644 homeassistant/components/arcam_fmj/strings.json create mode 100644 tests/components/arcam_fmj/test_device_trigger.py diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index dc5a576acec..5270eb706dc 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -5,6 +5,8 @@ SIGNAL_CLIENT_STARTED = "arcam.client_started" SIGNAL_CLIENT_STOPPED = "arcam.client_stopped" SIGNAL_CLIENT_DATA = "arcam.client_data" +EVENT_TURN_ON = "arcam_fmj.turn_on" + DEFAULT_PORT = 50000 DEFAULT_NAME = "Arcam FMJ" DEFAULT_SCAN_INTERVAL = 5 diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py new file mode 100644 index 00000000000..549b4cf4f82 --- /dev/null +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -0,0 +1,70 @@ +"""Provides device automations for Arcam FMJ Receiver control.""" +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.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, EVENT_TURN_ON + +TRIGGER_TYPES = {"turn_on"} +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 Arcam FMJ Receiver control devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain == "media_player": + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + + 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] == "turn_on": + + @callback + def _handle_event(event: Event): + if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: + hass.async_run_job(action({"trigger": config}, context=event.context)) + + return hass.bus.async_listen(EVENT_TURN_ON, _handle_event) + + return lambda: None diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 125b3bf96b1..27e1497a32d 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, CONF_ZONE, SERVICE_TURN_ON, @@ -31,6 +32,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( DOMAIN, DOMAIN_DATA_ENTRIES, + EVENT_TURN_ON, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, @@ -53,6 +55,7 @@ async def async_setup_entry( [ ArcamFmj( State(client, zone), + config_entry.unique_id or config_entry.entry_id, zone_config[CONF_NAME], zone_config.get(SERVICE_TURN_ON), ) @@ -67,9 +70,12 @@ async def async_setup_entry( class ArcamFmj(MediaPlayerEntity): """Representation of a media device.""" - def __init__(self, state: State, name: str, turn_on: Optional[ConfigType]): + def __init__( + self, state: State, uuid: str, name: str, turn_on: Optional[ConfigType] + ): """Initialize device.""" self._state = state + self._uuid = uuid self._name = name self._turn_on = turn_on self._support = ( @@ -78,6 +84,7 @@ class ArcamFmj(MediaPlayerEntity): | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON ) if state.zn == 1: self._support |= SUPPORT_SELECT_SOUND_MODE @@ -95,6 +102,11 @@ class ArcamFmj(MediaPlayerEntity): ) ) + @property + def unique_id(self): + """Return unique identifier if known.""" + return f"{self._uuid}-{self._state.zn}" + @property def device_info(self): """Return a device description for device registry.""" @@ -124,10 +136,7 @@ class ArcamFmj(MediaPlayerEntity): @property def supported_features(self): """Flag media player features that are supported.""" - support = self._support - if self._state.get_power() is not None or self._turn_on: - support |= SUPPORT_TURN_ON - return support + return self._support async def async_added_to_hass(self): """Once registered, add listener for events.""" @@ -230,7 +239,8 @@ class ArcamFmj(MediaPlayerEntity): validate_config=False, ) else: - _LOGGER.error("Unable to turn on") + _LOGGER.debug("Firing event to turn on device") + self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) async def async_turn_off(self): """Turn the media player off.""" diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json new file mode 100644 index 00000000000..6f60c9e2471 --- /dev/null +++ b/homeassistant/components/arcam_fmj/strings.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} was requested to turn on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/en.json b/homeassistant/components/arcam_fmj/translations/en.json index b78b8cbaa7b..cad1884ea0d 100644 --- a/homeassistant/components/arcam_fmj/translations/en.json +++ b/homeassistant/components/arcam_fmj/translations/en.json @@ -1,3 +1,8 @@ { - "title": "Arcam FMJ" + "title": "Arcam FMJ", + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} was requested to turn on" + } + } } \ No newline at end of file diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index e515b71468b..386cdf9a2b0 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -7,8 +7,9 @@ from homeassistant.components.arcam_fmj import DEVICE_SCHEMA from homeassistant.components.arcam_fmj.const import DOMAIN from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.setup import async_setup_component -from tests.async_mock import Mock +from tests.async_mock import Mock, patch MOCK_HOST = "127.0.0.1" MOCK_PORT = 1234 @@ -17,7 +18,8 @@ MOCK_TURN_ON = { "data": {"entity_id": "switch.test"}, } MOCK_NAME = "dummy" -MOCK_ENTITY_ID = "media_player.arcam_fmj_1" +MOCK_UUID = "1234" +MOCK_ENTITY_ID = "media_player.arcam_fmj_127_0_0_1_1234_1" MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) @@ -36,8 +38,8 @@ def client_fixture(): return client -@pytest.fixture(name="state") -def state_fixture(client): +@pytest.fixture(name="state_1") +def state_1_fixture(client): """Get a mocked state.""" state = Mock(State) state.client = client @@ -50,11 +52,49 @@ def state_fixture(client): return state +@pytest.fixture(name="state_2") +def state_2_fixture(client): + """Get a mocked state.""" + state = Mock(State) + state.client = client + state.zn = 2 + state.get_power.return_value = True + state.get_volume.return_value = 0.0 + state.get_source_list.return_value = [] + state.get_incoming_audio_format.return_value = (0, 0) + state.get_mute.return_value = None + return state + + +@pytest.fixture(name="state") +def state_fixture(state_1): + """Get a mocked state.""" + return state_1 + + @pytest.fixture(name="player") def player_fixture(hass, state): """Get standard player.""" - player = ArcamFmj(state, MOCK_NAME, None) + player = ArcamFmj(state, MOCK_UUID, MOCK_NAME, None) player.entity_id = MOCK_ENTITY_ID player.hass = hass player.async_write_ha_state = Mock() return player + + +@pytest.fixture(name="player_setup") +async def player_setup_fixture(hass, config, state_1, state_2, client): + """Get standard player.""" + + def state_mock(cli, zone): + if zone == 1: + return state_1 + if zone == 2: + return state_2 + + with patch("homeassistant.components.arcam_fmj.Client", return_value=client), patch( + "homeassistant.components.arcam_fmj.media_player.State", side_effect=state_mock + ), patch("homeassistant.components.arcam_fmj._run_client", return_value=None): + assert await async_setup_component(hass, "arcam_fmj", config) + await hass.async_block_till_done() + yield MOCK_ENTITY_ID diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py new file mode 100644 index 00000000000..bff8aa8327f --- /dev/null +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -0,0 +1,91 @@ +"""The tests for Arcam FMJ Receiver control device triggers.""" +import pytest + +from homeassistant.components.arcam_fmj.const import DOMAIN +import homeassistant.components.automation as automation +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, +) + + +@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 arcam_fmj.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "host", 1234)}, + ) + entity_reg.async_get_or_create( + "media_player", DOMAIN, "5678", device_id=device_entry.id + ) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"media_player.arcam_fmj_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state): + """Test for turn_on and turn_off triggers firing.""" + state.get_power.return_value = None + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": player_setup, + "type": "turn_on", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "{{ trigger.entity_id }}"}, + }, + } + ] + }, + ) + + await hass.services.async_call( + "media_player", "turn_on", {"entity_id": player_setup}, blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == player_setup diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 5a73e770129..3d88f337e93 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.core import HomeAssistant -from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT +from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_UUID from tests.async_mock import ANY, MagicMock, Mock, PropertyMock, patch @@ -25,7 +25,7 @@ async def update(player, force_refresh=False): async def test_properties(player, state): """Test standard properties.""" - assert player.unique_id is None + assert player.unique_id == f"{MOCK_UUID}-1" assert player.device_info == { "identifiers": {("arcam_fmj", MOCK_HOST, MOCK_PORT)}, "model": "FMJ", @@ -54,30 +54,8 @@ async def test_powered_on(player, state): assert data.state == "on" -async def test_supported_features_no_service(player, state): +async def test_supported_features(player, state): """Test support when turn on service exist.""" - state.get_power.return_value = None - data = await update(player) - assert data.attributes["supported_features"] == 68876 - - state.get_power.return_value = False - data = await update(player) - assert data.attributes["supported_features"] == 69004 - - -async def test_supported_features_service(hass, state): - """Test support when turn on service exist.""" - from homeassistant.components.arcam_fmj.media_player import ArcamFmj - - player = ArcamFmj(state, "dummy", MOCK_TURN_ON) - player.hass = hass - player.entity_id = MOCK_ENTITY_ID - - state.get_power.return_value = None - data = await update(player) - assert data.attributes["supported_features"] == 69004 - - state.get_power.return_value = False data = await update(player) assert data.attributes["supported_features"] == 69004 @@ -97,7 +75,7 @@ async def test_turn_on_with_service(hass, state): """Test support when turn on service exist.""" from homeassistant.components.arcam_fmj.media_player import ArcamFmj - player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + player = ArcamFmj(state, MOCK_UUID, "dummy", MOCK_TURN_ON) player.hass = Mock(HomeAssistant) player.entity_id = MOCK_ENTITY_ID with patch(