Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Homekit locks support #13625

Merged
merged 2 commits into from
Apr 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -186,8 +189,8 @@ def start(self, *args):

# 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)
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/homekit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_LOCK = 'DOOR_LOCK'
CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT'
Expand All @@ -43,6 +44,7 @@
# 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'
Expand All @@ -68,6 +70,9 @@
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'
Expand Down
77 changes: 77 additions & 0 deletions homeassistant/components/homekit/type_locks.py
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions tests/components/homekit/test_type_locks.py
Original file line number Diff line number Diff line change
@@ -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()