Add Homekit locks support (#13625)

* homekit: Add locks support
* Improved upgradeability
This commit is contained in:
Phil Kates 2018-04-09 07:23:49 -07:00 committed by cdce8p
parent 73de749411
commit c61611d2b4
4 changed files with 164 additions and 2 deletions

View file

@ -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)

View file

@ -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'

View file

@ -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

View file

@ -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()