From 47003fc04f3d3dcea2e81e38da59b382d45c9fc5 Mon Sep 17 00:00:00 2001 From: Leonardo Brondani Schenkel Date: Fri, 26 Oct 2018 09:15:26 +0200 Subject: [PATCH] deCONZ: configure service can now use 'field' as a subpath together with 'entity' (#17722) Allow both 'entity' and 'field' to be used simultaneously, where 'field' is used as a sub-path of the device path that is defined by 'entity'. --- homeassistant/components/deconz/__init__.py | 27 ++++----- homeassistant/components/deconz/services.yaml | 9 ++- tests/components/deconz/test_init.py | 55 ++++++++++++++++++- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 56b03c89a37..648aebc8c89 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -43,11 +43,11 @@ SERVICE_FIELD = 'field' SERVICE_ENTITY = 'entity' SERVICE_DATA = 'data' -SERVICE_SCHEMA = vol.Schema({ - vol.Exclusive(SERVICE_FIELD, 'deconz_id'): cv.string, - vol.Exclusive(SERVICE_ENTITY, 'deconz_id'): cv.entity_id, +SERVICE_SCHEMA = vol.All(vol.Schema({ + vol.Optional(SERVICE_ENTITY): cv.entity_id, + vol.Optional(SERVICE_FIELD): cv.matches_regex('/.*'), vol.Required(SERVICE_DATA): dict, -}) +}), cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD)) SERVICE_DEVICE_REFRESH = 'device_refresh' @@ -139,9 +139,10 @@ async def async_setup_entry(hass, config_entry): async def async_configure(call): """Set attribute of device in deCONZ. - Field is a string representing a specific device in deCONZ - e.g. field='/lights/1/state'. - Entity_id can be used to retrieve the proper field. + Entity is used to resolve to a device path (e.g. '/lights/1'). + Field is a string representing either a full path + (e.g. '/lights/1/state') when entity is not specified, or a + subpath (e.g. '/state') when used together with entity. Data is a json object with what data you want to alter e.g. data={'on': true}. { @@ -151,18 +152,14 @@ async def async_setup_entry(hass, config_entry): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ - field = call.data.get(SERVICE_FIELD) + field = call.data.get(SERVICE_FIELD, '') entity_id = call.data.get(SERVICE_ENTITY) data = call.data.get(SERVICE_DATA) deconz = hass.data[DOMAIN] if entity_id: - - entities = hass.data.get(DATA_DECONZ_ID) - - if entities: - field = entities.get(entity_id) - - if field is None: + try: + field = hass.data[DATA_DECONZ_ID][entity_id] + field + except KeyError: _LOGGER.error('Could not find the entity %s', entity_id) return diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index fa0fb8e14a4..cde7ac79f4c 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -1,12 +1,15 @@ configure: description: Set attribute of device in deCONZ. See https://home-assistant.io/components/deconz/#device-services for details. fields: - field: - description: Field is a string representing a specific device in deCONZ. - example: '/lights/1/state' entity: description: Entity id representing a specific device in deCONZ. example: 'light.rgb_light' + field: + description: >- + Field is a string representing a full path to deCONZ endpoint (when + entity is not specified) or a subpath of the device path for the + entity (when entity is specified). + example: '"/lights/1/state" or "/state"' data: description: Data is a json object with what data you want to alter. example: '{"on": true}' diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index cfda1232e93..8cc8c4bc242 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,10 +1,10 @@ """Test deCONZ component setup process.""" from unittest.mock import Mock, patch +from homeassistant.components import deconz +from homeassistant.components.deconz import DATA_DECONZ_ID from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from homeassistant.components import deconz - from tests.common import mock_coro CONFIG = { @@ -218,3 +218,54 @@ async def test_do_not_allow_clip_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) await hass.async_block_till_done() assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 + + +async def test_service_configure(hass): + """Test that service invokes pydeconz with the correct path and data.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(CONFIG)), \ + patch('pydeconz.DeconzSession.start', return_value=True), \ + patch('homeassistant.helpers.device_registry.async_get_registry', + return_value=mock_coro(Mock())): + assert await deconz.async_setup_entry(hass, entry) is True + + hass.data[DATA_DECONZ_ID] = { + 'light.test': '/light/1' + } + data = {'on': True, 'attr1': 10, 'attr2': 20} + + # only field + with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: + await hass.services.async_call('deconz', 'configure', service_data={ + 'field': '/light/42', 'data': data + }) + await hass.async_block_till_done() + async_put_state.assert_called_with('/light/42', data) + # only entity + with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: + await hass.services.async_call('deconz', 'configure', service_data={ + 'entity': 'light.test', 'data': data + }) + await hass.async_block_till_done() + async_put_state.assert_called_with('/light/1', data) + # entity + field + with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: + await hass.services.async_call('deconz', 'configure', service_data={ + 'entity': 'light.test', 'field': '/state', 'data': data}) + await hass.async_block_till_done() + async_put_state.assert_called_with('/light/1/state', data) + + # non-existing entity (or not from deCONZ) + with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: + await hass.services.async_call('deconz', 'configure', service_data={ + 'entity': 'light.nonexisting', 'field': '/state', 'data': data}) + await hass.async_block_till_done() + async_put_state.assert_not_called() + # field does not start with / + with patch('pydeconz.DeconzSession.async_put_state') as async_put_state: + await hass.services.async_call('deconz', 'configure', service_data={ + 'entity': 'light.test', 'field': 'state', 'data': data}) + await hass.async_block_till_done() + async_put_state.assert_not_called()