From 5ae65142b87efd74106012a111da73accbeb3871 Mon Sep 17 00:00:00 2001 From: Andrew Hayworth Date: Mon, 3 Dec 2018 00:25:54 -0600 Subject: [PATCH] Allow verisure locks to be configured with a default code (#18873) * Allow verisure locks to be configured with a default code * linting fix * PR feedback * PR feedback - try harder to prevent future typos A python mock is a magical thing, and will respond to basicaly any method you call on it. It's somewhat better to assert against an explicit variable named 'mock', rather than to assert on the method name you wanted to mock... could prevent a typo from messing up tests. * PR feedback: convert tests to integration-style tests Set up a fake verisure hub, stub out a _lot_ of calls, then test after platform discovery and service calls. It should be noted that we're overriding the `update()` calls in these tests. This was done to prevent even further mocking of the verisure hub's responses. Hopefully, this'll be a foundation for people to write more tests. * more pr feedback --- homeassistant/components/lock/verisure.py | 20 ++- homeassistant/components/verisure.py | 2 + requirements_test_all.txt | 6 + script/gen_requirements_all.py | 2 + tests/components/lock/test_verisure.py | 141 ++++++++++++++++++++++ 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 tests/components/lock/test_verisure.py diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/lock/verisure.py index 877c8a1ddf6..25c7e1aa8ea 100644 --- a/homeassistant/components/lock/verisure.py +++ b/homeassistant/components/lock/verisure.py @@ -8,7 +8,8 @@ import logging from time import sleep from time import time from homeassistant.components.verisure import HUB as hub -from homeassistant.components.verisure import (CONF_LOCKS, CONF_CODE_DIGITS) +from homeassistant.components.verisure import ( + CONF_LOCKS, CONF_DEFAULT_LOCK_CODE, CONF_CODE_DIGITS) from homeassistant.components.lock import LockDevice from homeassistant.const import ( ATTR_CODE, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) @@ -39,6 +40,7 @@ class VerisureDoorlock(LockDevice): self._digits = hub.config.get(CONF_CODE_DIGITS) self._changed_by = None self._change_timestamp = 0 + self._default_lock_code = hub.config.get(CONF_DEFAULT_LOCK_CODE) @property def name(self): @@ -96,13 +98,25 @@ class VerisureDoorlock(LockDevice): """Send unlock command.""" if self._state == STATE_UNLOCKED: return - self.set_lock_state(kwargs[ATTR_CODE], STATE_UNLOCKED) + + code = kwargs.get(ATTR_CODE, self._default_lock_code) + if code is None: + _LOGGER.error("Code required but none provided") + return + + self.set_lock_state(code, STATE_UNLOCKED) def lock(self, **kwargs): """Send lock command.""" if self._state == STATE_LOCKED: return - self.set_lock_state(kwargs[ATTR_CODE], STATE_LOCKED) + + code = kwargs.get(ATTR_CODE, self._default_lock_code) + if code is None: + _LOGGER.error("Code required but none provided") + return + + self.set_lock_state(code, STATE_LOCKED) def set_lock_state(self, code, state): """Send set lock state command.""" diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 2f2fa194846..481aa331e41 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -28,6 +28,7 @@ CONF_DOOR_WINDOW = 'door_window' CONF_GIID = 'giid' CONF_HYDROMETERS = 'hygrometers' CONF_LOCKS = 'locks' +CONF_DEFAULT_LOCK_CODE = 'default_lock_code' CONF_MOUSE = 'mouse' CONF_SMARTPLUGS = 'smartplugs' CONF_THERMOMETERS = 'thermometers' @@ -52,6 +53,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_GIID): cv.string, vol.Optional(CONF_HYDROMETERS, default=True): cv.boolean, vol.Optional(CONF_LOCKS, default=True): cv.boolean, + vol.Optional(CONF_DEFAULT_LOCK_CODE): cv.string, vol.Optional(CONF_MOUSE, default=True): cv.boolean, vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean, vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean, diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5707847a789..f62bb98fa88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -110,6 +110,9 @@ homematicip==0.9.8 # homeassistant.components.sensor.influxdb influxdb==5.2.0 +# homeassistant.components.verisure +jsonpath==0.75 + # homeassistant.components.dyson libpurecoollink==0.4.2 @@ -257,6 +260,9 @@ statsd==3.2.1 # homeassistant.components.camera.uvc uvcclient==0.11.0 +# homeassistant.components.verisure +vsure==1.5.2 + # homeassistant.components.vultr vultr==0.1.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e5840d62e17..82dab374e42 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -64,6 +64,7 @@ TEST_REQUIREMENTS = ( 'home-assistant-frontend', 'homematicip', 'influxdb', + 'jsonpath', 'libpurecoollink', 'libsoundtouch', 'luftdaten', @@ -110,6 +111,7 @@ TEST_REQUIREMENTS = ( 'srpenergy', 'statsd', 'uvcclient', + 'vsure', 'warrant', 'pythonwhois', 'wakeonlan', diff --git a/tests/components/lock/test_verisure.py b/tests/components/lock/test_verisure.py new file mode 100644 index 00000000000..03dd202e838 --- /dev/null +++ b/tests/components/lock/test_verisure.py @@ -0,0 +1,141 @@ +"""Tests for the Verisure platform.""" + +from contextlib import contextmanager +from unittest.mock import patch, call +from homeassistant.const import STATE_UNLOCKED +from homeassistant.setup import async_setup_component +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, SERVICE_UNLOCK) +from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN + + +NO_DEFAULT_LOCK_CODE_CONFIG = { + 'verisure': { + 'username': 'test', + 'password': 'test', + 'locks': True, + 'alarm': False, + 'door_window': False, + 'hygrometers': False, + 'mouse': False, + 'smartplugs': False, + 'thermometers': False, + 'smartcam': False, + } +} + +DEFAULT_LOCK_CODE_CONFIG = { + 'verisure': { + 'username': 'test', + 'password': 'test', + 'locks': True, + 'default_lock_code': '9999', + 'alarm': False, + 'door_window': False, + 'hygrometers': False, + 'mouse': False, + 'smartplugs': False, + 'thermometers': False, + 'smartcam': False, + } +} + +LOCKS = ['door_lock'] + + +@contextmanager +def mock_hub(config, get_response=LOCKS[0]): + """Extensively mock out a verisure hub.""" + hub_prefix = 'homeassistant.components.lock.verisure.hub' + verisure_prefix = 'verisure.Session' + with patch(verisure_prefix) as session, \ + patch(hub_prefix) as hub: + session.login.return_value = True + + hub.config = config['verisure'] + hub.get.return_value = LOCKS + hub.get_first.return_value = get_response.upper() + hub.session.set_lock_state.return_value = { + 'doorLockStateChangeTransactionId': 'test', + } + hub.session.get_lock_state_transaction.return_value = { + 'result': 'OK', + } + + yield hub + + +async def setup_verisure_locks(hass, config): + """Set up mock verisure locks.""" + with mock_hub(config): + await async_setup_component(hass, VERISURE_DOMAIN, config) + await hass.async_block_till_done() + # lock.door_lock, group.all_locks + assert len(hass.states.async_all()) == 2 + + +async def test_verisure_no_default_code(hass): + """Test configs without a default lock code.""" + await setup_verisure_locks(hass, NO_DEFAULT_LOCK_CODE_CONFIG) + with mock_hub(NO_DEFAULT_LOCK_CODE_CONFIG, + STATE_UNLOCKED) as hub: + + mock = hub.session.set_lock_state + await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, { + 'entity_id': 'lock.door_lock', + }) + await hass.async_block_till_done() + assert mock.call_count == 0 + + await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, { + 'entity_id': 'lock.door_lock', + 'code': '12345', + }) + await hass.async_block_till_done() + assert mock.call_args == call('12345', LOCKS[0], 'lock') + + mock.reset_mock() + await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, { + 'entity_id': 'lock.door_lock', + }) + await hass.async_block_till_done() + assert mock.call_count == 0 + + await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, { + 'entity_id': 'lock.door_lock', + 'code': '12345', + }) + await hass.async_block_till_done() + assert mock.call_args == call('12345', LOCKS[0], 'unlock') + + +async def test_verisure_default_code(hass): + """Test configs with a default lock code.""" + await setup_verisure_locks(hass, DEFAULT_LOCK_CODE_CONFIG) + with mock_hub(DEFAULT_LOCK_CODE_CONFIG, STATE_UNLOCKED) as hub: + mock = hub.session.set_lock_state + await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, { + 'entity_id': 'lock.door_lock', + }) + await hass.async_block_till_done() + assert mock.call_args == call('9999', LOCKS[0], 'lock') + + await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, { + 'entity_id': 'lock.door_lock', + }) + await hass.async_block_till_done() + assert mock.call_args == call('9999', LOCKS[0], 'unlock') + + await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, { + 'entity_id': 'lock.door_lock', + 'code': '12345', + }) + await hass.async_block_till_done() + assert mock.call_args == call('12345', LOCKS[0], 'lock') + + await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, { + 'entity_id': 'lock.door_lock', + 'code': '12345', + }) + await hass.async_block_till_done() + assert mock.call_args == call('12345', LOCKS[0], 'unlock')