From c78899c4f305101cce19c23058e35ccc181fb7c0 Mon Sep 17 00:00:00 2001 From: miniconfig Date: Tue, 17 Nov 2015 10:17:57 -0500 Subject: [PATCH 1/4] Added support for Locks, including those connected through a wink hub. --- homeassistant/components/lock/__init__.py | 124 ++++++++++++++++++++ homeassistant/components/lock/demo.py | 56 +++++++++ homeassistant/components/lock/services.yaml | 0 homeassistant/components/lock/wink.py | 73 ++++++++++++ homeassistant/components/wink.py | 4 +- homeassistant/const.py | 8 ++ 6 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lock/__init__.py create mode 100644 homeassistant/components/lock/demo.py create mode 100644 homeassistant/components/lock/services.yaml create mode 100644 homeassistant/components/lock/wink.py diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py new file mode 100644 index 00000000000..88768208c1e --- /dev/null +++ b/homeassistant/components/lock/__init__.py @@ -0,0 +1,124 @@ +""" +homeassistant.components.lock +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component to interface with various locks that can be controlled remotely. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/lock/ +""" +from datetime import timedelta +import logging +import os + +from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import ToggleEntity + +from homeassistant.const import ( + STATE_LOCKED, SERVICE_LOCK, SERVICE_UNLOCK, ATTR_ENTITY_ID) +from homeassistant.components import ( + group, wink) + +DOMAIN = 'lock' +DEPENDENCIES = [] +SCAN_INTERVAL = 30 + +GROUP_NAME_ALL_LOCKS = 'all locks' +ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks') + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +ATTR_LOCKED = "locked" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = { + wink.DISCOVER_LOCKS: 'wink' +} + +PROP_TO_ATTR = { + 'locked': ATTR_LOCKED +} + +_LOGGER = logging.getLogger(__name__) + + +def is_locked(hass, entity_id=None): + """ Returns if the lock is locked based on the statemachine. """ + entity_id = entity_id or ENTITY_ID_ALL_LOCKS + return hass.states.is_state(entity_id, STATE_LOCKED) + + +def do_lock(hass, entity_id=None): + """ Locks all or specified locks. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_LOCK, data) + + +def do_unlock(hass, entity_id=None): + """ Unlocks all or specified locks. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_UNLOCK, data) + + +def setup(hass, config): + """ Track states and offer events for locks. """ + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, + GROUP_NAME_ALL_LOCKS) + component.setup(config) + + def handle_lock_service(service): + """ Handles calls to the lock services. """ + target_locks = component.extract_from_service(service) + + for lock in target_locks: + if service.service == SERVICE_LOCK: + lock.do_lock() + else: + lock.do_unlock() + + if lock.should_poll: + lock.update_ha_state(True) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + hass.services.register(DOMAIN, SERVICE_UNLOCK, handle_lock_service, + descriptions.get(SERVICE_UNLOCK)) + hass.services.register(DOMAIN, SERVICE_LOCK, handle_lock_service, + descriptions.get(SERVICE_LOCK)) + + return True + + +class LockDevice(ToggleEntity): + """ Represents a lock within Home Assistant. """ + # pylint: disable=no-self-use + + @property + def locked(self): + """ Is the lock locked or unlocked. """ + return None + + @property + def device_state_attributes(self): + """ Returns device specific state attributes. """ + return None + + @property + def state_attributes(self): + """ Returns optional state attributes. """ + data = {} + + for prop, attr in PROP_TO_ATTR.items(): + value = getattr(self, prop) + if value: + data[attr] = value + + device_attr = self.device_state_attributes + + if device_attr is not None: + data.update(device_attr) + + return data diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py new file mode 100644 index 00000000000..76eea0104fb --- /dev/null +++ b/homeassistant/components/lock/demo.py @@ -0,0 +1,56 @@ +""" +homeassistant.components.lock.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Demo platform that has two fake locks. +""" +from homeassistant.components.lock import LockDevice +from homeassistant.const import ( + DEVICE_DEFAULT_NAME, STATE_LOCKED, STATE_UNLOCKED) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return demo locks. """ + add_devices_callback([ + DemoLock('Left Door', STATE_LOCKED, None), + DemoLock('Right Door', STATE_UNLOCKED, None) + ]) + + +class DemoLock(LockDevice): + """ Provides a demo lock. """ + def __init__(self, name, state, icon): + self._name = name or DEVICE_DEFAULT_NAME + self._state = state + self._icon = icon + + @property + def should_poll(self): + """ No polling needed for a demo lock. """ + return False + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def icon(self): + """ Returns the icon to use for device if any. """ + return self._icon + + @property + def is_locked(self): + """ True if device is locked. """ + return self._state + + def do_lock(self, **kwargs): + """ Lock the device. """ + self._state = STATE_LOCKED + self.update_ha_state() + + def do_unlock(self, **kwargs): + """ Unlock the device. """ + self._state = STATE_UNLOCKED + self.update_ha_state() diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py new file mode 100644 index 00000000000..059c3a56611 --- /dev/null +++ b/homeassistant/components/lock/wink.py @@ -0,0 +1,73 @@ +""" +homeassistant.components.lock.wink +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Wink locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.wink/ +""" +import logging + +from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_ACCESS_TOKEN, STATE_LOCKED, STATE_UNLOCKED + +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + '9eb39eaba0717922815e673ad1114c685839d890.zip' + '#python-wink==0.1.1'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Wink platform. """ + import pywink + + if discovery_info is None: + token = config.get(CONF_ACCESS_TOKEN) + + if token is None: + logging.getLogger(__name__).error( + "Missing wink access_token. " + "Get one at https://winkbearertoken.appspot.com/") + return + + pywink.set_bearer_token(token) + + add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) + + +class WinkLockDevice(Entity): + """ Represents a Wink lock. """ + + def __init__(self, wink): + self.wink = wink + + @property + def state(self): + """ Returns the state. """ + return STATE_LOCKED if self.is_locked else STATE_UNLOCKED + + @property + def unique_id(self): + """ Returns the id of this wink lock """ + return "{}.{}".format(self.__class__, self.wink.deviceId()) + + @property + def name(self): + """ Returns the name of the lock if any. """ + return self.wink.name() + + def update(self): + """ Update the state of the lock. """ + self.wink.updateState() + + @property + def is_locked(self): + """ True if device is locked. """ + return self.wink.state() + + def do_lock(self): + """ Lock the device. """ + self.wink.setState(True) + + def do_unlock(self): + """ Unlock the device. """ + self.wink.setState(False) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 03601f1d958..66ea29ff4dd 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -25,6 +25,7 @@ REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" DISCOVER_SENSORS = "wink.sensors" +DISCOVER_LOCKS = "wink.locks" def setup(hass, config): @@ -41,7 +42,8 @@ def setup(hass, config): for component_name, func_exists, discovery_type in ( ('light', pywink.get_bulbs, DISCOVER_LIGHTS), ('switch', pywink.get_switches, DISCOVER_SWITCHES), - ('sensor', pywink.get_sensors, DISCOVER_SENSORS)): + ('sensor', pywink.get_sensors, DISCOVER_SENSORS), + ('lock', pywink.get_locks, DISCOVER_LOCKS)): if func_exists(): component = get_component(component_name) diff --git a/homeassistant/const.py b/homeassistant/const.py index da1e424718f..5b0b5a5e214 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -53,6 +53,8 @@ STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' STATE_ALARM_PENDING = 'pending' STATE_ALARM_TRIGGERED = 'triggered' +STATE_LOCKED = 'locked' +STATE_UNLOCKED = 'unlocked' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -96,6 +98,9 @@ ATTR_BATTERY_LEVEL = "battery_level" # For devices which support an armed state ATTR_ARMED = "device_armed" +# For devices which support a locked state +ATTR_LOCKED = "locked" + # For sensors that support 'tripping', eg. motion and door sensors ATTR_TRIPPED = "device_tripped" @@ -135,6 +140,9 @@ SERVICE_ALARM_ARM_HOME = "alarm_arm_home" SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" SERVICE_ALARM_TRIGGER = "alarm_trigger" +SERVICE_LOCK = "lock" +SERVICE_UNLOCK = "unlock" + # #### API / REMOTE #### SERVER_PORT = 8123 From fa7391cdf684bb643bba77fbeb79ef0691fa37c0 Mon Sep 17 00:00:00 2001 From: miniconfig Date: Thu, 19 Nov 2015 16:54:55 -0500 Subject: [PATCH 2/4] Changed do_lock and do_unlock methods to lock and unlock. Implemented state method. Fixed locked method for demo interface. Changed LockDevice to extend Entity instead of ToggleEntity --- homeassistant/components/lock/__init__.py | 51 ++++++++--------------- homeassistant/components/lock/demo.py | 11 +++-- homeassistant/components/lock/wink.py | 17 +++----- 3 files changed, 31 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 88768208c1e..613410dfd07 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -12,12 +12,12 @@ import os from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import Entity from homeassistant.const import ( - STATE_LOCKED, SERVICE_LOCK, SERVICE_UNLOCK, ATTR_ENTITY_ID) -from homeassistant.components import ( - group, wink) + STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, + ATTR_ENTITY_ID) +from homeassistant.components import (group, wink) DOMAIN = 'lock' DEPENDENCIES = [] @@ -44,19 +44,19 @@ PROP_TO_ATTR = { _LOGGER = logging.getLogger(__name__) -def is_locked(hass, entity_id=None): +def locked(hass, entity_id=None): """ Returns if the lock is locked based on the statemachine. """ entity_id = entity_id or ENTITY_ID_ALL_LOCKS return hass.states.is_state(entity_id, STATE_LOCKED) -def do_lock(hass, entity_id=None): +def lock(hass, entity_id=None): """ Locks all or specified locks. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.services.call(DOMAIN, SERVICE_LOCK, data) -def do_unlock(hass, entity_id=None): +def unlock(hass, entity_id=None): """ Unlocks all or specified locks. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.services.call(DOMAIN, SERVICE_UNLOCK, data) @@ -73,14 +73,14 @@ def setup(hass, config): """ Handles calls to the lock services. """ target_locks = component.extract_from_service(service) - for lock in target_locks: + for item in target_locks: if service.service == SERVICE_LOCK: - lock.do_lock() + item.lock() else: - lock.do_unlock() + item.unlock() - if lock.should_poll: - lock.update_ha_state(True) + if item.should_poll: + item.update_ha_state(True) descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) @@ -92,7 +92,7 @@ def setup(hass, config): return True -class LockDevice(ToggleEntity): +class LockDevice(Entity): """ Represents a lock within Home Assistant. """ # pylint: disable=no-self-use @@ -102,23 +102,8 @@ class LockDevice(ToggleEntity): return None @property - def device_state_attributes(self): - """ Returns device specific state attributes. """ - return None - - @property - def state_attributes(self): - """ Returns optional state attributes. """ - data = {} - - for prop, attr in PROP_TO_ATTR.items(): - value = getattr(self, prop) - if value: - data[attr] = value - - device_attr = self.device_state_attributes - - if device_attr is not None: - data.update(device_attr) - - return data + def state(self): + is_locked = self.locked + if is_locked is None: + return STATE_UNKNOWN + return STATE_LOCKED if is_locked else STATE_UNLOCKED diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index 76eea0104fb..67e7032849f 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -41,16 +41,19 @@ class DemoLock(LockDevice): return self._icon @property - def is_locked(self): + def locked(self): """ True if device is locked. """ - return self._state + if self._state == STATE_LOCKED: + return True + else: + return False - def do_lock(self, **kwargs): + def lock(self, **kwargs): """ Lock the device. """ self._state = STATE_LOCKED self.update_ha_state() - def do_unlock(self, **kwargs): + def unlock(self, **kwargs): """ Unlock the device. """ self._state = STATE_UNLOCKED self.update_ha_state() diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 059c3a56611..5e463f49a8d 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -8,8 +8,8 @@ https://home-assistant.io/components/lock.wink/ """ import logging -from homeassistant.helpers.entity import Entity -from homeassistant.const import CONF_ACCESS_TOKEN, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.components.lock import LockDevice +from homeassistant.const import CONF_ACCESS_TOKEN REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' '9eb39eaba0717922815e673ad1114c685839d890.zip' @@ -34,17 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) -class WinkLockDevice(Entity): +class WinkLockDevice(LockDevice): """ Represents a Wink lock. """ def __init__(self, wink): self.wink = wink - @property - def state(self): - """ Returns the state. """ - return STATE_LOCKED if self.is_locked else STATE_UNLOCKED - @property def unique_id(self): """ Returns the id of this wink lock """ @@ -60,14 +55,14 @@ class WinkLockDevice(Entity): self.wink.updateState() @property - def is_locked(self): + def locked(self): """ True if device is locked. """ return self.wink.state() - def do_lock(self): + def lock(self): """ Lock the device. """ self.wink.setState(True) - def do_unlock(self): + def unlock(self): """ Unlock the device. """ self.wink.setState(False) From 105dc2847e9c57429afd8b79304f9a7c046b0570 Mon Sep 17 00:00:00 2001 From: miniconfig Date: Fri, 20 Nov 2015 16:34:27 -0500 Subject: [PATCH 3/4] Changed locked method of lock support to "is_locked". Added lock and unlock methods Updated wink components to use the new version of the wink library. --- homeassistant/components/light/wink.py | 4 ++-- homeassistant/components/lock/__init__.py | 18 +++++++++++++----- homeassistant/components/lock/demo.py | 2 +- homeassistant/components/lock/wink.py | 6 +++--- homeassistant/components/sensor/wink.py | 4 ++-- homeassistant/components/switch/wink.py | 4 ++-- homeassistant/components/wink.py | 4 ++-- 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 4fbf87aea2d..eaa703799f7 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -13,8 +13,8 @@ from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 613410dfd07..2cbd3a40872 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -44,7 +44,7 @@ PROP_TO_ATTR = { _LOGGER = logging.getLogger(__name__) -def locked(hass, entity_id=None): +def is_locked(hass, entity_id=None): """ Returns if the lock is locked based on the statemachine. """ entity_id = entity_id or ENTITY_ID_ALL_LOCKS return hass.states.is_state(entity_id, STATE_LOCKED) @@ -97,13 +97,21 @@ class LockDevice(Entity): # pylint: disable=no-self-use @property - def locked(self): + def is_locked(self): """ Is the lock locked or unlocked. """ return None + def lock(self): + """ Locks the lock. """ + raise NotImplementedError() + + def unlock(self): + """ Unlocks the lock. """ + raise NotImplementedError() + @property def state(self): - is_locked = self.locked - if is_locked is None: + locked = self.is_locked + if locked is None: return STATE_UNKNOWN - return STATE_LOCKED if is_locked else STATE_UNLOCKED + return STATE_LOCKED if locked else STATE_UNLOCKED diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index 67e7032849f..ac7bbed3dc2 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -41,7 +41,7 @@ class DemoLock(LockDevice): return self._icon @property - def locked(self): + def is_locked(self): """ True if device is locked. """ if self._state == STATE_LOCKED: return True diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 5e463f49a8d..27f602d65fa 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -12,8 +12,8 @@ from homeassistant.components.lock import LockDevice from homeassistant.const import CONF_ACCESS_TOKEN REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -55,7 +55,7 @@ class WinkLockDevice(LockDevice): self.wink.updateState() @property - def locked(self): + def is_locked(self): """ True if device is locked. """ return self.wink.state() diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 8bfdb9205fa..26fe6538e05 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index b022d8cbf72..f0dc18003c6 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -12,8 +12,8 @@ from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 66ea29ff4dd..bd79210bf75 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -19,8 +19,8 @@ from homeassistant.const import ( DOMAIN = "wink" DEPENDENCIES = [] REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" From f37d0d1c20d4bfa204a16d99f1f00d92bca769d9 Mon Sep 17 00:00:00 2001 From: miniconfig Date: Sat, 21 Nov 2015 09:52:43 -0500 Subject: [PATCH 4/4] Updated wink library version in requirements_all.txt --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index ce6cbfabc96..97d08d97c58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -93,7 +93,7 @@ netdisco==0.5.1 pywemo==0.3.2 # Wink (*.wink) -https://github.com/balloob/python-wink/archive/9eb39eaba0717922815e673ad1114c685839d890.zip#python-wink==0.1.1 +https://github.com/balloob/python-wink/archive/42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip#python-wink==0.2 # Slack notifier (notify.slack) slacker==0.6.8