From 41f908ed391910449da33d90026ee60f38d966ad Mon Sep 17 00:00:00 2001 From: "turbokongen@hotmail.com" Date: Wed, 27 Jan 2016 16:38:43 +0100 Subject: [PATCH] Added support for lock connected to Verisure system. --- homeassistant/components/lock/__init__.py | 53 ++++++++++--- homeassistant/components/lock/verisure.py | 92 +++++++++++++++++++++++ homeassistant/components/verisure.py | 15 +++- 3 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/lock/verisure.py diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 0d67679e82c..74d73d129e5 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, ATTR_ENTITY_ID) -from homeassistant.components import (group, wink) +from homeassistant.components import (group, verisure, wink) DOMAIN = 'lock' SCAN_INTERVAL = 30 @@ -28,12 +28,15 @@ ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks') ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_LOCKED = "locked" +ATTR_CODE = 'code' +ATTR_CODE_FORMAT = 'code_format' MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) # Maps discovered services to their platforms DISCOVERY_PLATFORMS = { - wink.DISCOVER_LOCKS: 'wink' + wink.DISCOVER_LOCKS: 'wink', + verisure.DISCOVER_LOCKS: 'verisure' } _LOGGER = logging.getLogger(__name__) @@ -45,15 +48,25 @@ def is_locked(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_LOCKED) -def lock(hass, entity_id=None): +def lock(hass, entity_id=None, code=None): """ Locks all or specified locks. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + hass.services.call(DOMAIN, SERVICE_LOCK, data) -def unlock(hass, entity_id=None): +def unlock(hass, entity_id=None, code=None): """ Unlocks all or specified locks. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + hass.services.call(DOMAIN, SERVICE_UNLOCK, data) @@ -68,11 +81,16 @@ def setup(hass, config): """ Handles calls to the lock services. """ target_locks = component.extract_from_service(service) + if ATTR_CODE not in service.data: + code = None + else: + code = service.data[ATTR_CODE] + for item in target_locks: if service.service == SERVICE_LOCK: - item.lock() + item.lock(code=code) else: - item.unlock() + item.unlock(code=code) if item.should_poll: item.update_ha_state(True) @@ -91,19 +109,34 @@ class LockDevice(Entity): """ Represents a lock within Home Assistant. """ # pylint: disable=no-self-use + @property + def code_format(self): + """ regex for code format or None if no code is required. """ + return None + @property def is_locked(self): """ Is the lock locked or unlocked. """ return None - def lock(self): + def lock(self, **kwargs): """ Locks the lock. """ raise NotImplementedError() - def unlock(self): + def unlock(self, **kwargs): """ Unlocks the lock. """ raise NotImplementedError() + @property + def state_attributes(self): + """ Return the state attributes. """ + if self.code_format is None: + return None + state_attr = { + ATTR_CODE_FORMAT: self.code_format, + } + return state_attr + @property def state(self): locked = self.is_locked diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/lock/verisure.py new file mode 100644 index 00000000000..818f8621144 --- /dev/null +++ b/homeassistant/components/lock/verisure.py @@ -0,0 +1,92 @@ +""" +homeassistant.components.lock.verisure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Interfaces with Verisure locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/verisure/ +""" +import logging + +import homeassistant.components.verisure as verisure +from homeassistant.components.lock import LockDevice + +from homeassistant.const import ( + STATE_UNKNOWN, + STATE_LOCKED, STATE_UNLOCKED) + +_LOGGER = logging.getLogger(__name__) +ATTR_CODE = 'code' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Verisure platform. """ + + if not verisure.MY_PAGES: + _LOGGER.error('A connection has not been made to Verisure mypages.') + return False + + locks = [] + + locks.extend([VerisureDoorlock(value) + for value in verisure.LOCK_STATUS.values() + if verisure.SHOW_LOCKS]) + + add_devices(locks) + + +# pylint: disable=abstract-method +class VerisureDoorlock(LockDevice): + """ Represents a Verisure doorlock status. """ + + def __init__(self, lock_status, code=None): + self._id = lock_status.id + self._state = STATE_UNKNOWN + self._code = code + + @property + def name(self): + """ Returns the name of the device. """ + return 'Lock {}'.format(self._id) + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def code_format(self): + """ Six digit code required. """ + return '^\\d{%s}$' % verisure.CODE_DIGITS + + def update(self): + """ Update lock status """ + verisure.update_lock() + + if verisure.LOCK_STATUS[self._id].status == 'unlocked': + self._state = STATE_UNLOCKED + elif verisure.LOCK_STATUS[self._id].status == 'locked': + self._state = STATE_LOCKED + elif verisure.LOCK_STATUS[self._id].status != 'pending': + _LOGGER.error( + 'Unknown lock state %s', + verisure.LOCK_STATUS[self._id].status) + + @property + def is_locked(self): + """ True if device is locked. """ + return verisure.LOCK_STATUS[self._id].status + + def unlock(self, **kwargs): + """ Send unlock command. """ + verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED') + _LOGGER.info('verisure doorlock unlocking') + verisure.MY_PAGES.lock.wait_while_pending() + verisure.update_lock() + + def lock(self, **kwargs): + """ Send lock command. """ + verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED') + _LOGGER.info('verisure doorlock locking') + verisure.MY_PAGES.lock.wait_while_pending() + verisure.update_lock() diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index e69321315eb..98a2356954a 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -26,6 +26,7 @@ DOMAIN = "verisure" DISCOVER_SENSORS = 'verisure.sensors' DISCOVER_SWITCHES = 'verisure.switches' DISCOVER_ALARMS = 'verisure.alarm_control_panel' +DISCOVER_LOCKS = 'verisure.lock' DEPENDENCIES = ['alarm_control_panel'] REQUIREMENTS = ['vsure==0.5.0'] @@ -36,6 +37,7 @@ MY_PAGES = None ALARM_STATUS = {} SMARTPLUG_STATUS = {} CLIMATE_STATUS = {} +LOCK_STATUS = {} VERISURE_LOGIN_ERROR = None VERISURE_ERROR = None @@ -44,6 +46,7 @@ SHOW_THERMOMETERS = True SHOW_HYGROMETERS = True SHOW_ALARM = True SHOW_SMARTPLUGS = True +SHOW_LOCKS = True CODE_DIGITS = 4 # if wrong password was given don't try again @@ -63,11 +66,12 @@ def setup(hass, config): from verisure import MyPages, LoginError, Error global SHOW_THERMOMETERS, SHOW_HYGROMETERS,\ - SHOW_ALARM, SHOW_SMARTPLUGS, CODE_DIGITS + SHOW_ALARM, SHOW_SMARTPLUGS, SHOW_LOCKS, CODE_DIGITS SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1')) SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1')) SHOW_ALARM = int(config[DOMAIN].get('alarm', '1')) SHOW_SMARTPLUGS = int(config[DOMAIN].get('smartplugs', '1')) + SHOW_LOCKS = int(config[DOMAIN].get('locks', '1')) CODE_DIGITS = int(config[DOMAIN].get('code_digits', '4')) global MY_PAGES @@ -87,11 +91,13 @@ def setup(hass, config): update_alarm() update_climate() update_smartplug() + update_lock() # Load components for the devices in the ISY controller that we support for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), ('switch', DISCOVER_SWITCHES), - ('alarm_control_panel', DISCOVER_ALARMS))): + ('alarm_control_panel', DISCOVER_ALARMS), + ('lock', DISCOVER_LOCKS))): component = get_component(comp_name) _LOGGER.info(config[DOMAIN]) bootstrap.setup_component(hass, component.DOMAIN, config) @@ -134,6 +140,11 @@ def update_smartplug(): update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS) +def update_lock(): + """ Updates the status of alarms. """ + update_component(MY_PAGES.lock.get, LOCK_STATUS) + + def update_component(get_function, status): """ Updates the status of verisure components. """ if WRONG_PASSWORD_GIVEN: