diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 06258bcc97a..22c74faf5f0 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -127,6 +127,9 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light') return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'lock': + return TYPES['Lock'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'switch' or state.domain == 'remote' \ or state.domain == 'input_boolean' or state.domain == 'script': _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') @@ -186,8 +189,8 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_lights, type_security_systems, type_sensors, - type_switches, type_thermostats) + type_covers, type_lights, type_locks, type_security_systems, + type_sensors, type_switches, type_thermostats) for state in self._hass.states.all(): self.add_bridge_accessory(state) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 7136852c409..e5a4c80a430 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -27,6 +27,7 @@ MANUFACTURER = 'HomeAssistant' # #### Categories #### CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' CATEGORY_LIGHT = 'LIGHTBULB' +CATEGORY_LOCK = 'DOOR_LOCK' CATEGORY_SENSOR = 'SENSOR' CATEGORY_SWITCH = 'SWITCH' CATEGORY_THERMOSTAT = 'THERMOSTAT' @@ -43,6 +44,7 @@ SERV_HUMIDITY_SENSOR = 'HumiditySensor' # StatusLowBattery, Name SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' SERV_SECURITY_SYSTEM = 'SecuritySystem' @@ -68,6 +70,9 @@ CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LEAK_DETECTED = 'LeakDetected' +CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' +CHAR_LOCK_TARGET_STATE = 'LockTargetState' +CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py new file mode 100644 index 00000000000..9df0c101eff --- /dev/null +++ b/homeassistant/components/homekit/type_locks.py @@ -0,0 +1,77 @@ +"""Class to hold all lock accessories.""" +import logging + +from homeassistant.components.lock import ( + ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) + +from . import TYPES +from .accessories import HomeAccessory, add_preload_service +from .const import ( + CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) + +_LOGGER = logging.getLogger(__name__) + +HASS_TO_HOMEKIT = {STATE_UNLOCKED: 0, + STATE_LOCKED: 1, + # value 2 is Jammed which hass doesn't have a state for + STATE_UNKNOWN: 3} +HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} +STATE_TO_SERVICE = {STATE_LOCKED: 'lock', + STATE_UNLOCKED: 'unlock'} + + +@TYPES.register('Lock') +class Lock(HomeAccessory): + """Generate a Lock accessory for a lock entity. + + The lock entity must support: unlock and lock. + """ + + def __init__(self, hass, entity_id, name, **kwargs): + """Initialize a Lock accessory object.""" + super().__init__(name, entity_id, CATEGORY_LOCK, **kwargs) + + self.hass = hass + self.entity_id = entity_id + + self.flag_target_state = False + + serv_lock_mechanism = add_preload_service(self, SERV_LOCK) + self.char_current_state = serv_lock_mechanism. \ + get_characteristic(CHAR_LOCK_CURRENT_STATE) + self.char_target_state = serv_lock_mechanism. \ + get_characteristic(CHAR_LOCK_TARGET_STATE) + + self.char_current_state.value = HASS_TO_HOMEKIT[STATE_UNKNOWN] + self.char_target_state.value = HASS_TO_HOMEKIT[STATE_LOCKED] + + self.char_target_state.setter_callback = self.set_state + + def set_state(self, value): + """Set lock state to value if call came from HomeKit.""" + _LOGGER.debug("%s: Set state to %d", self.entity_id, value) + self.flag_target_state = True + + hass_value = HOMEKIT_TO_HASS.get(value) + service = STATE_TO_SERVICE[hass_value] + + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call('lock', service, params) + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update lock after state changed.""" + if new_state is None: + return + + hass_state = new_state.state + if hass_state in HASS_TO_HOMEKIT: + current_lock_state = HASS_TO_HOMEKIT[hass_state] + self.char_current_state.set_value(current_lock_state) + _LOGGER.debug('%s: Updated current state to %s (%d)', + self.entity_id, hass_state, current_lock_state) + + # LockTargetState only supports locked and unlocked + if hass_state in (STATE_LOCKED, STATE_UNLOCKED): + if not self.flag_target_state: + self.char_target_state.set_value(current_lock_state) + self.flag_target_state = False diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py new file mode 100644 index 00000000000..d19bcdf3ec5 --- /dev/null +++ b/tests/components/homekit/test_type_locks.py @@ -0,0 +1,77 @@ +"""Test different accessory types: Locks.""" +import unittest + +from homeassistant.core import callback +from homeassistant.components.homekit.type_locks import Lock +from homeassistant.const import ( + STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED, + ATTR_SERVICE, EVENT_CALL_SERVICE) + +from tests.common import get_test_home_assistant + + +class TestHomekitSensors(unittest.TestCase): + """Test class for all accessory types regarding covers.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_lock_unlock(self): + """Test if accessory and HA are updated accordingly.""" + kitchen_lock = 'lock.kitchen_door' + + acc = Lock(self.hass, kitchen_lock, 'Lock', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 6) # DoorLock + + self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_target_state.value, 1) + + self.hass.states.set(kitchen_lock, STATE_LOCKED) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 1) + self.assertEqual(acc.char_target_state.value, 1) + + self.hass.states.set(kitchen_lock, STATE_UNLOCKED) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 0) + self.assertEqual(acc.char_target_state.value, 0) + + self.hass.states.set(kitchen_lock, STATE_UNKNOWN) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_target_state.value, 0) + + # Set from HomeKit + acc.char_target_state.client_update_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'lock') + self.assertEqual(acc.char_target_state.value, 1) + + acc.char_target_state.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'unlock') + self.assertEqual(acc.char_target_state.value, 0) + + self.hass.states.remove(kitchen_lock) + self.hass.block_till_done()