Skip to content
Snippets Groups Projects
Commit 41c1997b authored by Jc2k's avatar Jc2k Committed by Martin Hjelmare
Browse files

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
parent 995758b8
No related branches found
No related tags found
No related merge requests found
......@@ -68,6 +68,11 @@ def get_serial(accessory):
return None
def escape_characteristic_name(char_name):
"""Escape any dash or dots in a characteristics name."""
return char_name.replace('-', '_').replace('.', '_')
class HKDevice():
"""HomeKit device."""
......@@ -193,6 +198,57 @@ class HomeKitEntity(Entity):
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
self._features = 0
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):
"""Obtain a HomeKit device's state."""
......@@ -228,6 +284,10 @@ class HomeKitEntity(Entity):
"""Return True if entity is available."""
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):
"""Synchronise a HomeKit device state with Home Assistant."""
raise NotImplementedError
......
......@@ -54,6 +54,16 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
self._state = 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):
"""Synchronise the Alarm Control Panel state with Home Assistant."""
# pylint: disable=import-error
......@@ -63,14 +73,8 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "security-system-state.current":
self._chars['security-system-state.current'] = \
characteristic['iid']
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":
self._chars['battery-level'] = characteristic['iid']
self._battery_level = characteristic['value']
@property
......
......@@ -49,6 +49,29 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
self._current_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):
"""Synchronise device state with Home Assistant."""
# pylint: disable=import-error
......@@ -60,20 +83,11 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
self._state = MODE_HOMEKIT_TO_HASS.get(
characteristic['value'])
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(
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:
self._current_temp = characteristic['value']
elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET:
self._chars['target_temp'] = characteristic['iid']
self._features |= SUPPORT_TARGET_TEMPERATURE
self._target_temp = characteristic['value']
def set_temperature(self, **kwargs):
......@@ -81,14 +95,14 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
temp = kwargs.get(ATTR_TEMPERATURE)
characteristics = [{'aid': self._aid,
'iid': self._chars['target_temp'],
'iid': self._chars['temperature.target'],
'value': temp}]
self.put_characteristics(characteristics)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
characteristics = [{'aid': self._aid,
'iid': self._chars['target_mode'],
'iid': self._chars['heating-cooling.target'],
'value': MODE_HASS_TO_HOMEKIT[operation_mode]}]
self.put_characteristics(characteristics)
......
......@@ -62,7 +62,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
def __init__(self, accessory, discovery_info):
"""Initialise the Cover."""
super().__init__(accessory, discovery_info)
self._name = None
self._state = None
self._obstruction_detected = None
self.lock_state = None
......@@ -72,6 +71,20 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
"""Define this cover as a garage door."""
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):
"""Synchronise the Cover state with Home Assistant."""
# pylint: disable=import-error
......@@ -81,18 +94,9 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "door-state.current":
self._chars['door-state.current'] = \
characteristic['iid']
self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']]
elif ctype == "door-state.target":
self._chars['door-state.target'] = \
characteristic['iid']
elif ctype == "obstruction-detected":
self._chars['obstruction-detected'] = characteristic['iid']
self._obstruction_detected = characteristic['value']
elif ctype == "name":
self._chars['name'] = characteristic['iid']
self._name = characteristic['value']
@property
def available(self):
......@@ -151,7 +155,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
def __init__(self, accessory, discovery_info):
"""Initialise the Cover."""
super().__init__(accessory, discovery_info)
self._name = None
self._state = None
self._position = None
self._tilt_position = None
......@@ -164,6 +167,26 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
"""Return True if entity is available."""
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):
"""Synchronise the Cover state with Home Assistant."""
# pylint: disable=import-error
......@@ -173,43 +196,22 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "position.state":
self._chars['position.state'] = \
characteristic['iid']
if 'value' in characteristic:
self._state = \
CURRENT_WINDOW_STATE_MAP[characteristic['value']]
elif ctype == "position.current":
self._chars['position.current'] = \
characteristic['iid']
self._position = characteristic['value']
elif ctype == "position.target":
self._chars['position.target'] = \
characteristic['iid']
elif ctype == "position.hold":
self._chars['position.hold'] = characteristic['iid']
if 'value' in characteristic:
self._hold = characteristic['value']
elif ctype == "vertical-tilt.current":
self._chars['vertical-tilt.current'] = characteristic['iid']
if characteristic['value'] is not None:
self._tilt_position = characteristic['value']
elif ctype == "horizontal-tilt.current":
self._chars['horizontal-tilt.current'] = characteristic['iid']
if characteristic['value'] is not None:
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":
self._chars['obstruction-detected'] = characteristic['iid']
self._obstruction_detected = characteristic['value']
elif ctype == "name":
self._chars['name'] = characteristic['iid']
if 'value' in characteristic:
self._name = characteristic['value']
@property
def supported_features(self):
......
......@@ -36,6 +36,30 @@ class HomeKitLight(HomeKitEntity, Light):
self._hue = 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):
"""Synchronise light state with Home Assistant."""
# pylint: disable=import-error
......@@ -45,23 +69,14 @@ class HomeKitLight(HomeKitEntity, Light):
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "on":
self._chars['on'] = characteristic['iid']
self._on = characteristic['value']
elif ctype == 'brightness':
self._chars['brightness'] = characteristic['iid']
self._features |= SUPPORT_BRIGHTNESS
self._brightness = characteristic['value']
elif ctype == 'color-temperature':
self._chars['color-temperature'] = characteristic['iid']
self._features |= SUPPORT_COLOR_TEMP
self._color_temperature = characteristic['value']
elif ctype == "hue":
self._chars['hue'] = characteristic['iid']
self._features |= SUPPORT_COLOR
self._hue = characteristic['value']
elif ctype == "saturation":
self._chars['saturation'] = characteristic['iid']
self._features |= SUPPORT_COLOR
self._saturation = characteristic['value']
@property
......
......@@ -51,6 +51,16 @@ class HomeKitLock(HomeKitEntity, LockDevice):
self._name = discovery_info['model']
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):
"""Synchronise the Lock state with Home Assistant."""
# pylint: disable=import-error
......@@ -60,14 +70,8 @@ class HomeKitLock(HomeKitEntity, LockDevice):
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "lock-mechanism.current-state":
self._chars['lock-mechanism.current-state'] = \
characteristic['iid']
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":
self._chars['battery-level'] = characteristic['iid']
self._battery_level = characteristic['value']
@property
......
......@@ -33,6 +33,15 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
self._on = 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):
"""Synchronise the switch state with Home Assistant."""
# pylint: disable=import-error
......@@ -42,10 +51,8 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "on":
self._chars['on'] = characteristic['iid']
self._on = characteristic['value']
elif ctype == "outlet-in-use":
self._chars['outlet-in-use'] = characteristic['iid']
self._outlet_in_use = characteristic['value']
@property
......
......@@ -8,7 +8,7 @@ from homekit.model.characteristics import (
from homekit.model import Accessory, get_id
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
import homeassistant.util.dt as dt_util
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)
await hass.async_block_till_done()
fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info)
await hass.async_block_till_done()
return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory)
......@@ -39,7 +39,7 @@ def create_window_covering_service():
obstruction.value = False
name = service.add_characteristic('name')
name.value = "Window Cover 1"
name.value = "testdevice"
return service
......@@ -166,7 +166,7 @@ def create_garage_door_opener_service():
obstruction.value = False
name = service.add_characteristic('name')
name.value = "Garage Door Opener 1"
name.value = "testdevice"
return service
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment