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