diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 31ba0ff3581..3498b46d879 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -89,13 +89,14 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ + bridgeid = data.get(CONF_BRIDGEID) field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] gateway = get_master_gateway(hass) - if CONF_BRIDGEID in data: - gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + if bridgeid: + gateway = hass.data[DOMAIN][bridgeid] if entity_id: try: diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index d0586565521..7d630498cde 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -3,7 +3,6 @@ from unittest.mock import Mock, patch import asyncio import pytest -import voluptuous as vol from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import deconz @@ -168,98 +167,3 @@ async def test_unload_entry_multiple_gateways(hass): assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] assert hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master - - -async def test_service_configure(hass): - """Test that service invokes pydeconz with the correct path and data.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - }, - ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - - hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].deconz_ids = {"light.test": "/light/1"} - data = {"on": True, "attr1": 10, "attr2": 20} - - # only field - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", "configure", service_data={"field": "/light/42", "data": data} - ) - await hass.async_block_till_done() - - # only entity - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", "configure", service_data={"entity": "light.test", "data": data} - ) - await hass.async_block_till_done() - - # entity + field - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", - "configure", - service_data={"entity": "light.test", "field": "/state", "data": data}, - ) - await hass.async_block_till_done() - - # non-existing entity (or not from deCONZ) - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", - "configure", - service_data={ - "entity": "light.nonexisting", - "field": "/state", - "data": data, - }, - ) - await hass.async_block_till_done() - - # field does not start with / - with pytest.raises(vol.Invalid): - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True) - ): - await hass.services.async_call( - "deconz", - "configure", - service_data={"entity": "light.test", "field": "state", "data": data}, - ) - await hass.async_block_till_done() - - -async def test_service_refresh_devices(hass): - """Test that service can refresh devices.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - }, - ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True) - ): - await hass.services.async_call("deconz", "device_refresh", service_data={}) - await hass.async_block_till_done() - - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(False) - ): - await hass.services.async_call("deconz", "device_refresh", service_data={}) - await hass.async_block_till_done() diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py new file mode 100644 index 00000000000..63934871fcb --- /dev/null +++ b/tests/components/deconz/test_services.py @@ -0,0 +1,245 @@ +"""deCONZ service tests.""" +from asynctest import Mock, patch + +import pytest +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import deconz + +BRIDGEID = "0123456789" + +ENTRY_CONFIG = { + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80, +} + +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} + +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "type": "LightGroup", + "state": {}, + "action": {}, + "scenes": [{"id": "1", "name": "Scene 1"}], + "lights": ["1"], + } +} + +LIGHT = { + "1": { + "id": "Light 1 id", + "name": "Light 1 name", + "state": {"reachable": True}, + "type": "Light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + } +} + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHALightLevel", + "state": {"lightlevel": 30000, "dark": False}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + } +} + + +async def setup_deconz_integration(hass, options): + """Create the deCONZ gateway.""" + config_entry = config_entries.ConfigEntry( + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, + options=options, + entry_id="1", + ) + + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST + ): + await deconz.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][BRIDGEID] + + +async def test_service_setup(hass): + """Verify service setup works.""" + assert deconz.services.DECONZ_SERVICES not in hass.data + with patch( + "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) + ) as async_register: + await deconz.services.async_setup_services(hass) + assert hass.data[deconz.services.DECONZ_SERVICES] is True + assert async_register.call_count == 2 + + +async def test_service_setup_already_registered(hass): + """Make sure that services are only registered once.""" + hass.data[deconz.services.DECONZ_SERVICES] = True + with patch( + "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) + ) as async_register: + await deconz.services.async_setup_services(hass) + async_register.assert_not_called() + + +async def test_service_unload(hass): + """Verify service unload works.""" + hass.data[deconz.services.DECONZ_SERVICES] = True + with patch( + "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) + ) as async_remove: + await deconz.services.async_unload_services(hass) + assert hass.data[deconz.services.DECONZ_SERVICES] is False + assert async_remove.call_count == 2 + + +async def test_service_unload_not_registered(hass): + """Make sure that services can only be unloaded once.""" + with patch( + "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) + ) as async_remove: + await deconz.services.async_unload_services(hass) + assert deconz.services.DECONZ_SERVICES not in hass.data + async_remove.assert_not_called() + + +async def test_configure_service_with_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = { + deconz.services.SERVICE_FIELD: "/light/2", + deconz.CONF_BRIDGEID: BRIDGEID, + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with("/light/2", {"on": True, "attr1": 10, "attr2": 20}) + + +async def test_configure_service_with_entity(hass): + """Test that service invokes pydeconz with the correct path and data.""" + gateway = await setup_deconz_integration(hass, options={}) + + gateway.deconz_ids["light.test"] = "/light/1" + data = { + deconz.services.SERVICE_ENTITY: "light.test", + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with("/light/1", {"on": True, "attr1": 10, "attr2": 20}) + + +async def test_configure_service_with_entity_and_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + gateway = await setup_deconz_integration(hass, options={}) + + gateway.deconz_ids["light.test"] = "/light/1" + data = { + deconz.services.SERVICE_ENTITY: "light.test", + deconz.services.SERVICE_FIELD: "/state", + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with( + "/light/1/state", {"on": True, "attr1": 10, "attr2": 20} + ) + + +async def test_configure_service_with_faulty_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = {deconz.services.SERVICE_FIELD: "light/2", deconz.services.SERVICE_DATA: {}} + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + + +async def test_configure_service_with_faulty_entity(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = { + deconz.services.SERVICE_ENTITY: "light.nonexisting", + deconz.services.SERVICE_DATA: {}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_not_called() + + +async def test_service_refresh_devices(hass): + """Test that service can refresh devices.""" + gateway = await setup_deconz_integration(hass, options={}) + + data = {deconz.CONF_BRIDGEID: BRIDGEID} + + with patch( + "pydeconz.DeconzSession.async_get_state", + return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR}, + ): + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_DEVICE_REFRESH, service_data=data + ) + await hass.async_block_till_done() + + assert gateway.deconz_ids == { + "light.group_1_name": "/groups/1", + "light.light_1_name": "/lights/1", + "scene.group_1_name_scene_1": "/groups/1/scenes/1", + "sensor.sensor_1_name": "/sensors/1", + }