From 2be4bd7a841c3c6de2d236aaf924738c2e12b79c Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 May 2019 05:17:43 -0400 Subject: [PATCH 1/9] Add support for DoorLock cluster --- .../components/zha/core/channels/closures.py | 38 +++++ .../components/zha/core/channels/registry.py | 4 +- homeassistant/components/zha/core/const.py | 3 + .../components/zha/core/registries.py | 8 +- homeassistant/components/zha/lock.py | 140 ++++++++++++++++++ 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/zha/lock.py diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index ba3b6b2e71617f..57c545ceb9a369 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -5,5 +5,43 @@ https://home-assistant.io/components/zha/ """ import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import ZigbeeChannel +from ..const import SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) + +class DoorLockChannel(ZigbeeChannel): + """Door lock channel.""" + + _value_attribute = 0 + + async def async_update(self): + """Retrieve latest state.""" + result = await self.get_attribute_value('lock_state', from_cache=True) + + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + result + ) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute update from lock cluster.""" + attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + _LOGGER.debug("%s: Attribute report '%s'[%s] = %s", + self.unique_id, self.cluster.name, attr_name, value) + if attrid == self._value_attribute: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value( + self._value_attribute, from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py index 8f7335d82a9cd8..8b50ff4149731c 100644 --- a/homeassistant/components/zha/core/channels/registry.py +++ b/homeassistant/components/zha/core/channels/registry.py @@ -5,6 +5,8 @@ https://home-assistant.io/components/zha/ """ from . import ZigbeeChannel + +from .closures import DoorLockChannel from .general import ( OnOffChannel, LevelControlChannel, PowerConfigurationChannel, BasicChannel ) @@ -13,7 +15,6 @@ from .lighting import ColorChannel from .security import IASZoneChannel - ZIGBEE_CHANNEL_REGISTRY = {} @@ -44,4 +45,5 @@ def populate_channel_registry(): zcl.clusters.security.IasZone.cluster_id: IASZoneChannel, zcl.clusters.hvac.Fan.cluster_id: FanChannel, zcl.clusters.lightlink.LightLink.cluster_id: ZigbeeChannel, + zcl.clusters.closures.DoorLock.cluster_id: DoorLockChannel, }) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 9e42f6343a150e..97e2364619aa5c 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH @@ -27,6 +28,7 @@ BINARY_SENSOR, FAN, LIGHT, + LOCK, SENSOR, SWITCH, ) @@ -92,6 +94,7 @@ ELECTRICAL_MEASUREMENT_CHANNEL = 'electrical_measurement' POWER_CONFIGURATION_CHANNEL = 'power' EVENT_RELAY_CHANNEL = 'event_relay' +DOORLOCK_CHANNEL = 'door_lock' SIGNAL_ATTR_UPDATED = 'attribute_updated' SIGNAL_MOVE_LEVEL = "move_level" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index b585ce5f48a679..90edf717a07af0 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH @@ -154,7 +155,8 @@ def get_deconz_radio(): zcl.clusters.hvac.Fan: FAN, SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, zcl.clusters.general.MultistateInput.cluster_id: SENSOR, - zcl.clusters.general.AnalogInput.cluster_id: SENSOR + zcl.clusters.general.AnalogInput.cluster_id: SENSOR, + zcl.clusters.closures.DoorLock: LOCK }) SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ @@ -282,6 +284,10 @@ def get_deconz_radio(): 'attr': 'fan_mode', 'config': REPORT_CONFIG_OP }], + zcl.clusters.closures.DoorLock.cluster_id: [{ + 'attr': 'lock_state', + 'config': REPORT_CONFIG_DEFAULT + }], }) BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py new file mode 100644 index 00000000000000..9e53c4eb81810d --- /dev/null +++ b/homeassistant/components/zha/lock.py @@ -0,0 +1,140 @@ +"""Locks on Zigbee Home Automation networks.""" +import logging + +from homeassistant.core import callback +from homeassistant.components.lock import ( + DOMAIN, STATE_UNLOCKED, STATE_LOCKED, LockDevice) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .core.const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, DOORLOCK_CHANNEL, + SIGNAL_ATTR_UPDATED +) +from .entity import ZhaEntity + +_LOGGER = logging.getLogger(__name__) + +""" The first state is Zigbee 'Not fully locked' """ + +STATE_LIST = [ + STATE_UNLOCKED, + STATE_LOCKED, + STATE_UNLOCKED +] + +VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)} + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Old way of setting up Zigbee Home Automation locks.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Zigbee Home Automation Door Lock from config entry.""" + async def async_discover(discovery_info): + await _async_setup_entities(hass, config_entry, async_add_entities, + [discovery_info]) + + unsub = async_dispatcher_connect( + hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover) + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + + locks = hass.data.get(DATA_ZHA, {}).get(DOMAIN) + if locks is not None: + await _async_setup_entities(hass, config_entry, async_add_entities, + locks.values()) + del hass.data[DATA_ZHA][DOMAIN] + + +async def _async_setup_entities(hass, config_entry, async_add_entities, + discovery_infos): + """Set up the ZHA locks.""" + entities = [] + for discovery_info in discovery_infos: + entities.append(ZhaDoorLock(**discovery_info)) + + async_add_entities(entities, update_before_add=True) + + +class ZhaDoorLock(ZhaEntity, LockDevice): + """Representation of a ZHA lock.""" + + _domain = DOMAIN + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this sensor.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._doorlock_channel = self.cluster_channels.get(DOORLOCK_CHANNEL) + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._doorlock_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + self._state = VALUE_TO_STATE.get(last_state.state, last_state.state) + + @property + def is_locked(self) -> bool: + """Return true if entity is locked.""" + if self._state is None: + return False + return self._state == STATE_LOCKED + + @property + def device_state_attributes(self): + """Return state attributes.""" + return self.state_attributes + + async def async_lock(self, **kwargs): + """Lock the lock. + This method must be run in the event loop and returns a coroutine. + """ + success = await self._doorlock_channel.lock_door() + t_log = {} + t_log['lock'] = success + if not success: + self.debug("locked: %s", t_log) + return + self.async_schedule_update_ha_state() + + async def async_unlock(self, **kwargs): + """Lock the lock. + This method must be run in the event loop and returns a coroutine. + """ + success = await self._doorlock_channel.unlock_door() + t_log = {} + t_log['unlock'] = success + if not success: + self.debug("unlocked: %s", t_log) + return + self.async_schedule_update_ha_state() + + async def async_update(self): + """Attempt to retrieve state from the lock.""" + await super().async_update() + await self.async_get_state() + + def async_set_state(self, state): + """Handle state update from channel.""" + self._state = VALUE_TO_STATE.get(state, self._state) + self.async_schedule_update_ha_state() + + async def async_get_state(self, from_cache=True): + """Attempt to retrieve state from the lock.""" + if self._doorlock_channel: + state = await self._doorlock_channel.get_attribute_value('lock_state', from_cache=from_cache) + if state is not None: + self._state = VALUE_TO_STATE.get(state, self._state) + + async def refresh(self, time): + """Call async_get_state at an interval.""" + await self.async_get_state(from_cache=False) + + def debug(self, msg, *args): + """Log debug message.""" + _LOGGER.debug('%s: ' + msg, self.entity_id, *args) + From 0268fe05dddc077cd9483888513453615de4a9ea Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 May 2019 09:38:28 -0400 Subject: [PATCH 2/9] Add test for zha lock --- tests/components/zha/test_lock.py | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/components/zha/test_lock.py diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py new file mode 100644 index 00000000000000..3851ca9081c132 --- /dev/null +++ b/tests/components/zha/test_lock.py @@ -0,0 +1,90 @@ +"""Test zha lock.""" +from unittest.mock import call, patch +from homeassistant.components import lock +from homeassistant.const import ( + STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE) +from homeassistant.components.lock import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from tests.common import mock_coro +from .common import ( + async_init_zigpy_device, make_attribute, make_entity_id, + async_test_device_join, async_enable_traffic +) + +LOCK_DOOR = 0 +UNLOCK_DOOR = 1 + +async def test_lock(hass, config_entry, zha_gateway): + """Test zha lock platform.""" + from zigpy.zcl.clusters.closures import DoorLock + from zigpy.zcl.clusters.general import Basic + from zigpy.zcl.foundation import Status + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [DoorLock.cluster_id, Basic.cluster_id], [], None, zha_gateway) + + # load up lock domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + cluster = zigpy_device.endpoints.get(1).door_lock + entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # test that the lock was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the state has changed from unavailable to unlocked + assert hass.states.get(entity_id).state == STATE_UNLOCKED + + # set state to locked + attr = make_attribute(0, 1) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_LOCKED + + # set state to unlocked + attr.value.value = 2 + cluster.handle_message(False, 0, 0x0a, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNLOCKED + + # lock from HA + await async_lock(hass, cluster, entity_id) + + # unlock from HA + await async_lock(hass, cluster, entity_id) + + +async def async_lock(hass, cluster, entity_id): + """Test lock functionality from hass.""" + from zigpy.zcl.foundation import Status + with patch( + 'zigpy.zcl.Cluster.request', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # lock via UI + await hass.services.async_call(DOMAIN, 'lock', { + 'entity_id': entity_id + }, blocking=True) + assert cluster.request.call_count == 1 + assert cluster.request.call_args == call( + False, LOCK_DOOR, (), expect_reply=True, manufacturer=None) + +async def async_unlock(hass, cluster, entity_id): + """Test lock functionality from hass.""" + from zigpy.zcl.foundation import Status + with patch( + 'zigpy.zcl.Cluster.request', + return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + # lock via UI + await hass.services.async_call(DOMAIN, 'unlock', { + 'entity_id': entity_id + }, blocking=True) + assert cluster.request.call_count == 1 + assert cluster.request.call_args == call( + False, UNLOCK_DOOR, (), expect_reply=True, manufacturer=None) From 33506ed283d16a44bf82b72bb3e835d86d3b26b9 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 May 2019 12:59:12 -0400 Subject: [PATCH 3/9] Change lock_state report to REPORT_CONFIG_IMMEDIATE --- homeassistant/components/zha/core/registries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 90edf717a07af0..0824426b8d7e50 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -286,7 +286,7 @@ def get_deconz_radio(): }], zcl.clusters.closures.DoorLock.cluster_id: [{ 'attr': 'lock_state', - 'config': REPORT_CONFIG_DEFAULT + 'config': REPORT_CONFIG_IMMEDIATE }], }) From 6ac908b1c7d80922e95ae856bcfbf546d30150a4 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 May 2019 14:08:10 -0400 Subject: [PATCH 4/9] Update channel command wrapper to return the entire result This allows for return values other than result[1] --- .../components/zha/core/channels/__init__.py | 8 ++--- homeassistant/components/zha/light.py | 34 +++++++++---------- homeassistant/components/zha/lock.py | 17 ++++------ homeassistant/components/zha/switch.py | 9 ++--- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 3eb24050195103..83ade5894652df 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -44,7 +44,6 @@ def decorate_command(channel, command): """Wrap a cluster command to make it safe.""" @wraps(command) async def wrapper(*args, **kwds): - from zigpy.zcl.foundation import Status from zigpy.exceptions import DeliveryError try: result = await command(*args, **kwds) @@ -54,9 +53,8 @@ async def wrapper(*args, **kwds): "{}: {}".format("with args", args), "{}: {}".format("with kwargs", kwds), "{}: {}".format("and result", result)) - if isinstance(result, bool): - return result - return result[1] is Status.SUCCESS + return result + except (DeliveryError, Timeout) as ex: _LOGGER.debug( "%s: command failed: %s exception: %s", @@ -64,7 +62,7 @@ async def wrapper(*args, **kwds): command.__name__, str(ex) ) - return False + return ex return wrapper diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index c3aa0e50f44228..dfc98ca5779d92 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -13,7 +13,7 @@ ON_OFF_CHANNEL, LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL ) from .entity import ZhaEntity - +from zigpy.zcl.foundation import Status _LOGGER = logging.getLogger(__name__) @@ -173,12 +173,12 @@ async def async_turn_on(self, **kwargs): level = min(254, brightness) else: level = self._brightness or 254 - success = await self._level_channel.move_to_level_with_on_off( + result = await self._level_channel.move_to_level_with_on_off( level, duration ) - t_log['move_to_level_with_on_off'] = success - if not success: + t_log['move_to_level_with_on_off'] = result + if not isinstance(result, list) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = bool(level) @@ -186,9 +186,9 @@ async def async_turn_on(self, **kwargs): self._brightness = level if brightness is None or brightness: - success = await self._on_off_channel.on() - t_log['on_off'] = success - if not success: + result = await self._on_off_channel.on() + t_log['on_off'] = result + if not isinstance(result, list) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True @@ -196,10 +196,10 @@ async def async_turn_on(self, **kwargs): if light.ATTR_COLOR_TEMP in kwargs and \ self.supported_features & light.SUPPORT_COLOR_TEMP: temperature = kwargs[light.ATTR_COLOR_TEMP] - success = await self._color_channel.move_to_color_temp( + result = await self._color_channel.move_to_color_temp( temperature, duration) - t_log['move_to_color_temp'] = success - if not success: + t_log['move_to_color_temp'] = result + if not isinstance(result, list) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._color_temp = temperature @@ -208,13 +208,13 @@ async def async_turn_on(self, **kwargs): self.supported_features & light.SUPPORT_COLOR: hs_color = kwargs[light.ATTR_HS_COLOR] xy_color = color_util.color_hs_to_xy(*hs_color) - success = await self._color_channel.move_to_color( + result = await self._color_channel.move_to_color( int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration, ) - t_log['move_to_color'] = success - if not success: + t_log['move_to_color'] = result + if not isinstance(result, list) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._hs_color = hs_color @@ -227,14 +227,14 @@ async def async_turn_off(self, **kwargs): duration = kwargs.get(light.ATTR_TRANSITION) supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS if duration and supports_level: - success = await self._level_channel.move_to_level_with_on_off( + result = await self._level_channel.move_to_level_with_on_off( 0, duration*10 ) else: - success = await self._on_off_channel.off() - self.debug("turned off: %s", success) - if not success: + result = await self._on_off_channel.off() + self.debug("turned off: %s", result) + if not isinstance(result, list) or result[1] is not Status.SUCCESS: return self._state = False self.async_schedule_update_ha_state() diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 9e53c4eb81810d..b9e5d7b1575d78 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -10,6 +10,7 @@ SIGNAL_ATTR_UPDATED ) from .entity import ZhaEntity +from zigpy.zcl.foundation import Status _LOGGER = logging.getLogger(__name__) @@ -93,11 +94,9 @@ async def async_lock(self, **kwargs): """Lock the lock. This method must be run in the event loop and returns a coroutine. """ - success = await self._doorlock_channel.lock_door() - t_log = {} - t_log['lock'] = success - if not success: - self.debug("locked: %s", t_log) + result = await self._doorlock_channel.lock_door() + if not isinstance(result, list) or result[0] is not Status.SUCCESS: + _LOGGER.error("Error with lock_door: %s", result) return self.async_schedule_update_ha_state() @@ -105,11 +104,9 @@ async def async_unlock(self, **kwargs): """Lock the lock. This method must be run in the event loop and returns a coroutine. """ - success = await self._doorlock_channel.unlock_door() - t_log = {} - t_log['unlock'] = success - if not success: - self.debug("unlocked: %s", t_log) + result = await self._doorlock_channel.unlock_door() + if not isinstance(result, list) or result[0] is not Status.SUCCESS: + _LOGGER.error("Error with unlock_door: %s", result) return self.async_schedule_update_ha_state() diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 7efcbabd74e1be..ec596adf6c038e 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -10,6 +10,7 @@ SIGNAL_ATTR_UPDATED ) from .entity import ZhaEntity +from zigpy.zcl.foundation import Status _LOGGER = logging.getLogger(__name__) @@ -66,16 +67,16 @@ def is_on(self) -> bool: async def async_turn_on(self, **kwargs): """Turn the entity on.""" - success = await self._on_off_channel.on() - if not success: + result = await self._on_off_channel.on() + if not isinstance(result, list) or result[1] is not Status.SUCCESS: return self._state = True self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - success = await self._on_off_channel.off() - if not success: + result = await self._on_off_channel.off() + if not isinstance(result, list) or result[1] is not Status.SUCCESS: return self._state = False self.async_schedule_update_ha_state() From 7dbc0563a24546c114fce1d5c249505377fed47b Mon Sep 17 00:00:00 2001 From: root Date: Fri, 24 May 2019 17:45:40 -0400 Subject: [PATCH 5/9] Fix tests --- tests/components/zha/test_light.py | 8 ++++---- tests/components/zha/test_lock.py | 12 +++++------- tests/components/zha/test_switch.py | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index e9d6370575b7a8..02a0eba46a389d 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -57,9 +57,9 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off level_device_level_cluster = zigpy_device_level.endpoints.get(1).level on_off_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock( - return_value=(sentinel.data, Status.SUCCESS)))) + return_value=[sentinel.data, Status.SUCCESS]))) level_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock( - return_value=(sentinel.data, Status.SUCCESS)))) + return_value=[sentinel.data, Status.SUCCESS]))) monkeypatch.setattr(level_device_on_off_cluster, 'request', on_off_mock) monkeypatch.setattr(level_device_level_cluster, 'request', level_mock) level_entity_id = make_entity_id(DOMAIN, zigpy_device_level, @@ -137,7 +137,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): from zigpy.zcl.foundation import Status with patch( 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + return_value=mock_coro([0x00, Status.SUCCESS])): # turn on via UI await hass.services.async_call(DOMAIN, 'turn_on', { 'entity_id': entity_id @@ -154,7 +154,7 @@ async def async_test_off_from_hass(hass, cluster, entity_id): from zigpy.zcl.foundation import Status with patch( 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + return_value=mock_coro([0x01, Status.SUCCESS])): # turn off via UI await hass.services.async_call(DOMAIN, 'turn_off', { 'entity_id': entity_id diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 3851ca9081c132..5f682cb9b45936 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,24 +1,21 @@ """Test zha lock.""" from unittest.mock import call, patch -from homeassistant.components import lock from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE) from homeassistant.components.lock import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID from tests.common import mock_coro from .common import ( async_init_zigpy_device, make_attribute, make_entity_id, - async_test_device_join, async_enable_traffic -) + async_enable_traffic) LOCK_DOOR = 0 UNLOCK_DOOR = 1 + async def test_lock(hass, config_entry, zha_gateway): """Test zha lock platform.""" from zigpy.zcl.clusters.closures import DoorLock from zigpy.zcl.clusters.general import Basic - from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( @@ -66,7 +63,7 @@ async def async_lock(hass, cluster, entity_id): from zigpy.zcl.foundation import Status with patch( 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + return_value=mock_coro([Status.SUCCESS, ])): # lock via UI await hass.services.async_call(DOMAIN, 'lock', { 'entity_id': entity_id @@ -75,12 +72,13 @@ async def async_lock(hass, cluster, entity_id): assert cluster.request.call_args == call( False, LOCK_DOOR, (), expect_reply=True, manufacturer=None) + async def async_unlock(hass, cluster, entity_id): """Test lock functionality from hass.""" from zigpy.zcl.foundation import Status with patch( 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + return_value=mock_coro([Status.SUCCESS, ])): # lock via UI await hass.services.async_call(DOMAIN, 'unlock', { 'entity_id': entity_id diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index b0bbc103a9e3a0..2120bd6baf550c 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -54,7 +54,7 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on from HA with patch( 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + return_value=mock_coro([0x00, Status.SUCCESS])): # turn on via UI await hass.services.async_call(DOMAIN, 'turn_on', { 'entity_id': entity_id @@ -66,7 +66,7 @@ async def test_switch(hass, config_entry, zha_gateway): # turn off from HA with patch( 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): + return_value=mock_coro([0x01, Status.SUCCESS])): # turn off via UI await hass.services.async_call(DOMAIN, 'turn_off', { 'entity_id': entity_id From 5986c99bee2438a900b01acd3586f324333ba7d5 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 24 May 2019 17:47:58 -0400 Subject: [PATCH 6/9] Fix lint --- .../components/zha/core/channels/closures.py | 1 + homeassistant/components/zha/light.py | 2 +- homeassistant/components/zha/lock.py | 15 ++++++--------- homeassistant/components/zha/switch.py | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 57c545ceb9a369..f2f8d07fde9299 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -12,6 +12,7 @@ _LOGGER = logging.getLogger(__name__) + class DoorLockChannel(ZigbeeChannel): """Door lock channel.""" diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index dfc98ca5779d92..64c515b06b0919 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from zigpy.zcl.foundation import Status from homeassistant.components import light from homeassistant.const import STATE_ON from homeassistant.core import callback @@ -13,7 +14,6 @@ ON_OFF_CHANNEL, LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL ) from .entity import ZhaEntity -from zigpy.zcl.foundation import Status _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index b9e5d7b1575d78..5ac4a0c2e30825 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -1,6 +1,7 @@ """Locks on Zigbee Home Automation networks.""" import logging +from zigpy.zcl.foundation import Status from homeassistant.core import callback from homeassistant.components.lock import ( DOMAIN, STATE_UNLOCKED, STATE_LOCKED, LockDevice) @@ -10,7 +11,6 @@ SIGNAL_ATTR_UPDATED ) from .entity import ZhaEntity -from zigpy.zcl.foundation import Status _LOGGER = logging.getLogger(__name__) @@ -24,6 +24,7 @@ VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up Zigbee Home Automation locks.""" @@ -91,9 +92,7 @@ def device_state_attributes(self): return self.state_attributes async def async_lock(self, **kwargs): - """Lock the lock. - This method must be run in the event loop and returns a coroutine. - """ + """Lock the lock.""" result = await self._doorlock_channel.lock_door() if not isinstance(result, list) or result[0] is not Status.SUCCESS: _LOGGER.error("Error with lock_door: %s", result) @@ -101,9 +100,7 @@ async def async_lock(self, **kwargs): self.async_schedule_update_ha_state() async def async_unlock(self, **kwargs): - """Lock the lock. - This method must be run in the event loop and returns a coroutine. - """ + """Unlock the lock.""" result = await self._doorlock_channel.unlock_door() if not isinstance(result, list) or result[0] is not Status.SUCCESS: _LOGGER.error("Error with unlock_door: %s", result) @@ -123,7 +120,8 @@ def async_set_state(self, state): async def async_get_state(self, from_cache=True): """Attempt to retrieve state from the lock.""" if self._doorlock_channel: - state = await self._doorlock_channel.get_attribute_value('lock_state', from_cache=from_cache) + state = await self._doorlock_channel.get_attribute_value( + 'lock_state', from_cache=from_cache) if state is not None: self._state = VALUE_TO_STATE.get(state, self._state) @@ -134,4 +132,3 @@ async def refresh(self, time): def debug(self, msg, *args): """Log debug message.""" _LOGGER.debug('%s: ' + msg, self.entity_id, *args) - diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index ec596adf6c038e..89452f00d9f2f7 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,6 +1,7 @@ """Switches on Zigbee Home Automation networks.""" import logging +from zigpy.zcl.foundation import Status from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import STATE_ON from homeassistant.core import callback @@ -10,7 +11,6 @@ SIGNAL_ATTR_UPDATED ) from .entity import ZhaEntity -from zigpy.zcl.foundation import Status _LOGGER = logging.getLogger(__name__) From 93377a939433232adfc3f797b49bca7154d451be Mon Sep 17 00:00:00 2001 From: root Date: Thu, 6 Jun 2019 12:44:14 -0400 Subject: [PATCH 7/9] Update DoorLock test to work with updated zigpy schema --- tests/components/zha/test_lock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 5f682cb9b45936..d1e1e40ff13879 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -69,8 +69,8 @@ async def async_lock(hass, cluster, entity_id): 'entity_id': entity_id }, blocking=True) assert cluster.request.call_count == 1 - assert cluster.request.call_args == call( - False, LOCK_DOOR, (), expect_reply=True, manufacturer=None) + assert cluster.request.call_args[0][0] is False + assert cluster.request.call_args[0][1] == LOCK_DOOR async def async_unlock(hass, cluster, entity_id): @@ -84,5 +84,5 @@ async def async_unlock(hass, cluster, entity_id): 'entity_id': entity_id }, blocking=True) assert cluster.request.call_count == 1 - assert cluster.request.call_args == call( - False, UNLOCK_DOOR, (), expect_reply=True, manufacturer=None) + assert cluster.request.call_args[0][0] is False + assert cluster.request.call_args[0][1] == UNLOCK_DOOR From 4d388bd0040920dc907792598f60ff6cb0b2a71c Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Jun 2019 05:22:35 -0400 Subject: [PATCH 8/9] Fix lint --- tests/components/zha/test_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index d1e1e40ff13879..c07b7fe1fff75d 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,5 +1,5 @@ """Test zha lock.""" -from unittest.mock import call, patch +from unittest.mock import patch from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE) from homeassistant.components.lock import DOMAIN From 66579d8ecce59f71ced59326b767b08db204b4c6 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 7 Jun 2019 07:12:11 -0400 Subject: [PATCH 9/9] Fix unlock test --- tests/components/zha/test_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index c07b7fe1fff75d..4951c3537a0c4c 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -55,7 +55,7 @@ async def test_lock(hass, config_entry, zha_gateway): await async_lock(hass, cluster, entity_id) # unlock from HA - await async_lock(hass, cluster, entity_id) + await async_unlock(hass, cluster, entity_id) async def async_lock(hass, cluster, entity_id):