diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 5ee0a00f04f..e8e43d384b1 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -16,7 +16,6 @@ from homeassistant.const import ( ) from . import DOMAIN -from .const import LOGGER from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE CONF_SUBTYPE = "subtype" @@ -414,16 +413,13 @@ async def async_validate_trigger_config(hass, config): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if ( - not device - or device.model not in REMOTES - or trigger not in REMOTES[device.model] - ): - if not device: - raise InvalidDeviceAutomationConfig( - f"deCONZ trigger {trigger} device with id " - f"{config[CONF_DEVICE_ID]} not found" - ) + if not device: + raise InvalidDeviceAutomationConfig( + f"deCONZ trigger {trigger} device with ID " + f"{config[CONF_DEVICE_ID]} not found" + ) + + if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig( f"deCONZ trigger {trigger} is not valid for device " f"{device} ({config[CONF_DEVICE_ID]})" @@ -443,8 +439,9 @@ async def async_attach_trigger(hass, config, action, automation_info): deconz_event = _get_deconz_event_from_device_id(hass, device.id) if deconz_event is None: - LOGGER.error("No deconz_event tied to device %s found", device.name) - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig( + f'No deconz_event tied to device "{device.name}" found' + ) event_id = deconz_event.serial diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 7a39b6fe48f..640a4b61ce3 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -1,7 +1,10 @@ """deCONZ device automation tests.""" -from unittest.mock import patch +from unittest.mock import Mock, patch +import pytest + +from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.deconz import device_trigger from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.device_trigger import CONF_SUBTYPE @@ -14,33 +17,23 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, ) +from homeassistant.helpers.trigger import async_initialize_triggers +from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.common import assert_lists_same, async_get_device_automations +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -SENSORS = { - "1": { - "config": { - "alert": "none", - "battery": 60, - "group": "10", - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "1b355c0b6d2af28febd7ca9165881952", - "manufacturername": "IKEA of Sweden", - "mode": 1, - "modelid": "TRADFRI on/off switch", - "name": "TRÅDFRI on/off switch ", - "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, - "swversion": "1.4.018", - "type": "ZHASwitch", - "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", - } -} + +@pytest.fixture +def automation_calls(hass): + """Track automation calls to a mock service.""" + return async_mock_service(hass, "test", "automation") async def test_get_triggers(hass, aioclient_mock): @@ -76,8 +69,6 @@ async def test_get_triggers(hass, aioclient_mock): identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) - assert device_trigger._get_deconz_event_from_device_id(hass, device.id) - triggers = await async_get_device_automations(hass, "trigger", device.id) expected_triggers = [ @@ -135,14 +126,279 @@ async def test_get_triggers(hass, aioclient_mock): assert_lists_same(triggers, expected_triggers) -async def test_helper_no_match(hass, aioclient_mock): - """Verify trigger helper returns None when no event could be matched.""" +async def test_get_triggers_manage_unsupported_remotes(hass, aioclient_mock): + """Verify no triggers for an unsupported remote.""" + data = { + "sensors": { + "1": { + "config": { + "alert": "none", + "group": "10", + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1b355c0b6d2af28febd7ca9165881952", + "manufacturername": "IKEA of Sweden", + "mode": 1, + "modelid": "Unsupported model", + "name": "TRÅDFRI on/off switch ", + "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, + "swversion": "1.4.018", + "type": "ZHASwitch", + "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + + expected_triggers = [] + + assert_lists_same(triggers, expected_triggers) + + +async def test_functional_device_trigger( + hass, aioclient_mock, mock_deconz_websocket, automation_calls +): + """Test proper matching and attachment of device trigger automation.""" + await async_setup_component(hass, "persistent_notification", {}) + + data = { + "sensors": { + "1": { + "config": { + "alert": "none", + "battery": 60, + "group": "10", + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1b355c0b6d2af28febd7ca9165881952", + "manufacturername": "IKEA of Sweden", + "mode": 1, + "modelid": "TRADFRI on/off switch", + "name": "TRÅDFRI on/off switch ", + "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, + "swversion": "1.4.018", + "type": "ZHASwitch", + "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} + ) + + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DECONZ_DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: device_trigger.CONF_SHORT_PRESS, + CONF_SUBTYPE: device_trigger.CONF_TURN_ON, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + + assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 1 + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"buttonevent": 1002}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(automation_calls) == 1 + assert automation_calls[0].data["some"] == "test_trigger_button_press" + + +async def test_validate_trigger_unknown_device( + hass, aioclient_mock, mock_deconz_websocket +): + """Test unknown device does not return a trigger config.""" await setup_deconz_integration(hass, aioclient_mock) - deconz_event = device_trigger._get_deconz_event_from_device_id(hass, "mock-id") - assert not deconz_event + + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DECONZ_DOMAIN, + CONF_DEVICE_ID: "unknown device", + CONF_TYPE: device_trigger.CONF_SHORT_PRESS, + CONF_SUBTYPE: device_trigger.CONF_TURN_ON, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 -async def test_helper_no_gateway_exist(hass): - """Verify trigger helper returns None when no gateway exist.""" - deconz_event = device_trigger._get_deconz_event_from_device_id(hass, "mock-id") - assert not deconz_event +async def test_validate_trigger_unsupported_device( + hass, aioclient_mock, mock_deconz_websocket +): + """Test unsupported device doesn't return a trigger config.""" + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, + model="unsupported", + ) + + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DECONZ_DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: device_trigger.CONF_SHORT_PRESS, + CONF_SUBTYPE: device_trigger.CONF_TURN_ON, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 + + +async def test_validate_trigger_unsupported_trigger( + hass, aioclient_mock, mock_deconz_websocket +): + """Test unsupported trigger does not return a trigger config.""" + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, + model="TRADFRI on/off switch", + ) + + trigger_config = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DECONZ_DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "unsupported", + CONF_SUBTYPE: device_trigger.CONF_TURN_ON, + } + + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "trigger": trigger_config, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 + + +async def test_attach_trigger_no_matching_event( + hass, aioclient_mock, mock_deconz_websocket +): + """Test no matching event for device doesn't return a trigger config.""" + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, + name="Tradfri switch", + model="TRADFRI on/off switch", + ) + + trigger_config = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DECONZ_DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: device_trigger.CONF_SHORT_PRESS, + CONF_SUBTYPE: device_trigger.CONF_TURN_ON, + } + + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "trigger": trigger_config, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 1 + + # Assert that deCONZ async_attach_trigger raises InvalidDeviceAutomationConfig + assert not await async_initialize_triggers( + hass, + [trigger_config], + action=Mock(), + domain=AUTOMATION_DOMAIN, + name="mock-name", + log_cb=Mock(), + )