Homekit controller BLE groundwork (#20538)

* Define the characteristics to poll (or subscribe to) up front

* Configure characteristics immediately instead of during first poll

* Do as much cover configuration upfront as possible

* Remove test workaround as no longer needed

* Remove switch code that is already handled by HomeKitEntity

* Remove lock code already handled by HomeKitEntity

* Remove light code already handled by HomeKitEntity

* Remove alarm code already handled by HomeKitEntity

* Remove climate code already handled by HomeKitEntity
This commit is contained in:
Jc2k 2019-01-28 16:21:20 +00:00 committed by Martin Hjelmare
parent 995758b8ac
commit 41c1997b88
9 changed files with 177 additions and 72 deletions

View file

@ -68,6 +68,11 @@ def get_serial(accessory):
return None return None
def escape_characteristic_name(char_name):
"""Escape any dash or dots in a characteristics name."""
return char_name.replace('-', '_').replace('.', '_')
class HKDevice(): class HKDevice():
"""HomeKit device.""" """HomeKit device."""
@ -193,6 +198,57 @@ class HomeKitEntity(Entity):
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid) self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
self._features = 0 self._features = 0
self._chars = {} self._chars = {}
self.setup()
def setup(self):
"""Configure an entity baed on its HomeKit characterstics metadata."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
pairing_data = self._accessory.pairing.pairing_data
get_uuid = CharacteristicsTypes.get_uuid
characteristic_types = [
get_uuid(c) for c in self.get_characteristic_types()
]
self._chars_to_poll = []
self._chars = {}
self._char_names = {}
for accessory in pairing_data.get('accessories', []):
if accessory['aid'] != self._aid:
continue
for service in accessory['services']:
if service['iid'] != self._iid:
continue
for char in service['characteristics']:
uuid = CharacteristicsTypes.get_uuid(char['type'])
if uuid not in characteristic_types:
continue
self._setup_characteristic(char)
def _setup_characteristic(self, char):
"""Configure an entity based on a HomeKit characteristics metadata."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
# Build up a list of (aid, iid) tuples to poll on update()
self._chars_to_poll.append((self._aid, char['iid']))
# Build a map of ctype -> iid
short_name = CharacteristicsTypes.get_short(char['type'])
self._chars[short_name] = char['iid']
self._char_names[char['iid']] = short_name
# Callback to allow entity to configure itself based on this
# characteristics metadata (valid values, value ranges, features, etc)
setup_fn_name = escape_characteristic_name(short_name)
setup_fn = getattr(self, '_setup_{}'.format(setup_fn_name), None)
if not setup_fn:
return
# pylint: disable=E1102
setup_fn(char)
def update(self): def update(self):
"""Obtain a HomeKit device's state.""" """Obtain a HomeKit device's state."""
@ -228,6 +284,10 @@ class HomeKitEntity(Entity):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._accessory.pairing is not None return self._accessory.pairing is not None
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
raise NotImplementedError
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise a HomeKit device state with Home Assistant.""" """Synchronise a HomeKit device state with Home Assistant."""
raise NotImplementedError raise NotImplementedError

View file

@ -54,6 +54,16 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
self._state = None self._state = None
self._battery_level = None self._battery_level = None
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
return [
CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT,
CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET,
CharacteristicsTypes.BATTERY_LEVEL,
]
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise the Alarm Control Panel state with Home Assistant.""" """Synchronise the Alarm Control Panel state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
@ -63,14 +73,8 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
ctype = characteristic['type'] ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype) ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "security-system-state.current": if ctype == "security-system-state.current":
self._chars['security-system-state.current'] = \
characteristic['iid']
self._state = CURRENT_STATE_MAP[characteristic['value']] self._state = CURRENT_STATE_MAP[characteristic['value']]
elif ctype == "security-system-state.target":
self._chars['security-system-state.target'] = \
characteristic['iid']
elif ctype == "battery-level": elif ctype == "battery-level":
self._chars['battery-level'] = characteristic['iid']
self._battery_level = characteristic['value'] self._battery_level = characteristic['value']
@property @property

View file

@ -49,6 +49,29 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
self._current_temp = None self._current_temp = None
self._target_temp = None self._target_temp = None
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
return [
CharacteristicsTypes.HEATING_COOLING_CURRENT,
CharacteristicsTypes.HEATING_COOLING_TARGET,
CharacteristicsTypes.TEMPERATURE_CURRENT,
CharacteristicsTypes.TEMPERATURE_TARGET,
]
def _setup_heating_cooling_target(self, characteristic):
self._features |= SUPPORT_OPERATION_MODE
valid_values = characteristic.get(
'valid-values', DEFAULT_VALID_MODES)
self._valid_modes = [
MODE_HOMEKIT_TO_HASS.get(mode) for mode in valid_values
]
def _setup_temperature_target(self, characteristic):
self._features |= SUPPORT_TARGET_TEMPERATURE
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise device state with Home Assistant.""" """Synchronise device state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
@ -60,20 +83,11 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
self._state = MODE_HOMEKIT_TO_HASS.get( self._state = MODE_HOMEKIT_TO_HASS.get(
characteristic['value']) characteristic['value'])
if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET: if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET:
self._chars['target_mode'] = characteristic['iid']
self._features |= SUPPORT_OPERATION_MODE
self._current_mode = MODE_HOMEKIT_TO_HASS.get( self._current_mode = MODE_HOMEKIT_TO_HASS.get(
characteristic['value']) characteristic['value'])
valid_values = characteristic.get(
'valid-values', DEFAULT_VALID_MODES)
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
mode) for mode in valid_values]
elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT: elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT:
self._current_temp = characteristic['value'] self._current_temp = characteristic['value']
elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET: elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET:
self._chars['target_temp'] = characteristic['iid']
self._features |= SUPPORT_TARGET_TEMPERATURE
self._target_temp = characteristic['value'] self._target_temp = characteristic['value']
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
@ -81,14 +95,14 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
characteristics = [{'aid': self._aid, characteristics = [{'aid': self._aid,
'iid': self._chars['target_temp'], 'iid': self._chars['temperature.target'],
'value': temp}] 'value': temp}]
self.put_characteristics(characteristics) self.put_characteristics(characteristics)
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
characteristics = [{'aid': self._aid, characteristics = [{'aid': self._aid,
'iid': self._chars['target_mode'], 'iid': self._chars['heating-cooling.target'],
'value': MODE_HASS_TO_HOMEKIT[operation_mode]}] 'value': MODE_HASS_TO_HOMEKIT[operation_mode]}]
self.put_characteristics(characteristics) self.put_characteristics(characteristics)

View file

@ -62,7 +62,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
def __init__(self, accessory, discovery_info): def __init__(self, accessory, discovery_info):
"""Initialise the Cover.""" """Initialise the Cover."""
super().__init__(accessory, discovery_info) super().__init__(accessory, discovery_info)
self._name = None
self._state = None self._state = None
self._obstruction_detected = None self._obstruction_detected = None
self.lock_state = None self.lock_state = None
@ -72,6 +71,20 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
"""Define this cover as a garage door.""" """Define this cover as a garage door."""
return 'garage' return 'garage'
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
return [
CharacteristicsTypes.DOOR_STATE_CURRENT,
CharacteristicsTypes.DOOR_STATE_TARGET,
CharacteristicsTypes.OBSTRUCTION_DETECTED,
CharacteristicsTypes.NAME,
]
def _setup_name(self, char):
self._name = char['value']
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise the Cover state with Home Assistant.""" """Synchronise the Cover state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
@ -81,18 +94,9 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
ctype = characteristic['type'] ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype) ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "door-state.current": if ctype == "door-state.current":
self._chars['door-state.current'] = \
characteristic['iid']
self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']] self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']]
elif ctype == "door-state.target":
self._chars['door-state.target'] = \
characteristic['iid']
elif ctype == "obstruction-detected": elif ctype == "obstruction-detected":
self._chars['obstruction-detected'] = characteristic['iid']
self._obstruction_detected = characteristic['value'] self._obstruction_detected = characteristic['value']
elif ctype == "name":
self._chars['name'] = characteristic['iid']
self._name = characteristic['value']
@property @property
def available(self): def available(self):
@ -151,7 +155,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
def __init__(self, accessory, discovery_info): def __init__(self, accessory, discovery_info):
"""Initialise the Cover.""" """Initialise the Cover."""
super().__init__(accessory, discovery_info) super().__init__(accessory, discovery_info)
self._name = None
self._state = None self._state = None
self._position = None self._position = None
self._tilt_position = None self._tilt_position = None
@ -164,6 +167,26 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._state is not None return self._state is not None
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
return [
CharacteristicsTypes.POSITION_STATE,
CharacteristicsTypes.POSITION_CURRENT,
CharacteristicsTypes.POSITION_TARGET,
CharacteristicsTypes.POSITION_HOLD,
CharacteristicsTypes.VERTICAL_TILT_CURRENT,
CharacteristicsTypes.VERTICAL_TILT_TARGET,
CharacteristicsTypes.HORIZONTAL_TILT_CURRENT,
CharacteristicsTypes.HORIZONTAL_TILT_TARGET,
CharacteristicsTypes.OBSTRUCTION_DETECTED,
CharacteristicsTypes.NAME,
]
def _setup_name(self, char):
self._name = char['value']
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise the Cover state with Home Assistant.""" """Synchronise the Cover state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
@ -173,43 +196,22 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
ctype = characteristic['type'] ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype) ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "position.state": if ctype == "position.state":
self._chars['position.state'] = \
characteristic['iid']
if 'value' in characteristic: if 'value' in characteristic:
self._state = \ self._state = \
CURRENT_WINDOW_STATE_MAP[characteristic['value']] CURRENT_WINDOW_STATE_MAP[characteristic['value']]
elif ctype == "position.current": elif ctype == "position.current":
self._chars['position.current'] = \
characteristic['iid']
self._position = characteristic['value'] self._position = characteristic['value']
elif ctype == "position.target":
self._chars['position.target'] = \
characteristic['iid']
elif ctype == "position.hold": elif ctype == "position.hold":
self._chars['position.hold'] = characteristic['iid']
if 'value' in characteristic: if 'value' in characteristic:
self._hold = characteristic['value'] self._hold = characteristic['value']
elif ctype == "vertical-tilt.current": elif ctype == "vertical-tilt.current":
self._chars['vertical-tilt.current'] = characteristic['iid']
if characteristic['value'] is not None: if characteristic['value'] is not None:
self._tilt_position = characteristic['value'] self._tilt_position = characteristic['value']
elif ctype == "horizontal-tilt.current": elif ctype == "horizontal-tilt.current":
self._chars['horizontal-tilt.current'] = characteristic['iid']
if characteristic['value'] is not None: if characteristic['value'] is not None:
self._tilt_position = characteristic['value'] self._tilt_position = characteristic['value']
elif ctype == "vertical-tilt.target":
self._chars['vertical-tilt.target'] = \
characteristic['iid']
elif ctype == "horizontal-tilt.target":
self._chars['horizontal-tilt.target'] = \
characteristic['iid']
elif ctype == "obstruction-detected": elif ctype == "obstruction-detected":
self._chars['obstruction-detected'] = characteristic['iid']
self._obstruction_detected = characteristic['value'] self._obstruction_detected = characteristic['value']
elif ctype == "name":
self._chars['name'] = characteristic['iid']
if 'value' in characteristic:
self._name = characteristic['value']
@property @property
def supported_features(self): def supported_features(self):

View file

@ -36,6 +36,30 @@ class HomeKitLight(HomeKitEntity, Light):
self._hue = None self._hue = None
self._saturation = None self._saturation = None
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
return [
CharacteristicsTypes.ON,
CharacteristicsTypes.BRIGHTNESS,
CharacteristicsTypes.COLOR_TEMPERATURE,
CharacteristicsTypes.HUE,
CharacteristicsTypes.SATURATION,
]
def _setup_brightness(self, char):
self._features |= SUPPORT_BRIGHTNESS
def _setup_color_temperature(self, char):
self._features |= SUPPORT_COLOR_TEMP
def _setup_hue(self, char):
self._features |= SUPPORT_COLOR
def _setup_saturation(self, char):
self._features |= SUPPORT_COLOR
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise light state with Home Assistant.""" """Synchronise light state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
@ -45,23 +69,14 @@ class HomeKitLight(HomeKitEntity, Light):
ctype = characteristic['type'] ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype) ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "on": if ctype == "on":
self._chars['on'] = characteristic['iid']
self._on = characteristic['value'] self._on = characteristic['value']
elif ctype == 'brightness': elif ctype == 'brightness':
self._chars['brightness'] = characteristic['iid']
self._features |= SUPPORT_BRIGHTNESS
self._brightness = characteristic['value'] self._brightness = characteristic['value']
elif ctype == 'color-temperature': elif ctype == 'color-temperature':
self._chars['color-temperature'] = characteristic['iid']
self._features |= SUPPORT_COLOR_TEMP
self._color_temperature = characteristic['value'] self._color_temperature = characteristic['value']
elif ctype == "hue": elif ctype == "hue":
self._chars['hue'] = characteristic['iid']
self._features |= SUPPORT_COLOR
self._hue = characteristic['value'] self._hue = characteristic['value']
elif ctype == "saturation": elif ctype == "saturation":
self._chars['saturation'] = characteristic['iid']
self._features |= SUPPORT_COLOR
self._saturation = characteristic['value'] self._saturation = characteristic['value']
@property @property

View file

@ -51,6 +51,16 @@ class HomeKitLock(HomeKitEntity, LockDevice):
self._name = discovery_info['model'] self._name = discovery_info['model']
self._battery_level = None self._battery_level = None
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
return [
CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE,
CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE,
CharacteristicsTypes.BATTERY_LEVEL,
]
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise the Lock state with Home Assistant.""" """Synchronise the Lock state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
@ -60,14 +70,8 @@ class HomeKitLock(HomeKitEntity, LockDevice):
ctype = characteristic['type'] ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype) ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "lock-mechanism.current-state": if ctype == "lock-mechanism.current-state":
self._chars['lock-mechanism.current-state'] = \
characteristic['iid']
self._state = CURRENT_STATE_MAP[characteristic['value']] self._state = CURRENT_STATE_MAP[characteristic['value']]
elif ctype == "lock-mechanism.target-state":
self._chars['lock-mechanism.target-state'] = \
characteristic['iid']
elif ctype == "battery-level": elif ctype == "battery-level":
self._chars['battery-level'] = characteristic['iid']
self._battery_level = characteristic['value'] self._battery_level = characteristic['value']
@property @property

View file

@ -33,6 +33,15 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
self._on = None self._on = None
self._outlet_in_use = None self._outlet_in_use = None
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
return [
CharacteristicsTypes.ON,
CharacteristicsTypes.OUTLET_IN_USE,
]
def update_characteristics(self, characteristics): def update_characteristics(self, characteristics):
"""Synchronise the switch state with Home Assistant.""" """Synchronise the switch state with Home Assistant."""
# pylint: disable=import-error # pylint: disable=import-error
@ -42,10 +51,8 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
ctype = characteristic['type'] ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype) ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "on": if ctype == "on":
self._chars['on'] = characteristic['iid']
self._on = characteristic['value'] self._on = characteristic['value']
elif ctype == "outlet-in-use": elif ctype == "outlet-in-use":
self._chars['outlet-in-use'] = characteristic['iid']
self._outlet_in_use = characteristic['value'] self._outlet_in_use = characteristic['value']
@property @property

View file

@ -8,7 +8,7 @@ from homekit.model.characteristics import (
from homekit.model import Accessory, get_id from homekit.model import Accessory, get_id
from homeassistant.components.homekit_controller import ( from homeassistant.components.homekit_controller import (
DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT, HomeKitEntity) DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT)
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, fire_service_discovered from tests.common import async_fire_time_changed, fire_service_discovered
@ -168,8 +168,7 @@ async def setup_test_component(hass, services):
} }
} }
with mock.patch.object(HomeKitEntity, 'name', 'testdevice'): fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info)
fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) await hass.async_block_till_done()
await hass.async_block_till_done()
return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory) return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory)

View file

@ -39,7 +39,7 @@ def create_window_covering_service():
obstruction.value = False obstruction.value = False
name = service.add_characteristic('name') name = service.add_characteristic('name')
name.value = "Window Cover 1" name.value = "testdevice"
return service return service
@ -166,7 +166,7 @@ def create_garage_door_opener_service():
obstruction.value = False obstruction.value = False
name = service.add_characteristic('name') name = service.add_characteristic('name')
name.value = "Garage Door Opener 1" name.value = "testdevice"
return service return service