From 8bf5e57b7fdacae3e0b31e16c38154ae2728d7b4 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 28 Mar 2019 03:01:10 +0000 Subject: [PATCH] Move HKDevice into connection (#22430) --- .../components/homekit_controller/__init__.py | 161 +----------------- .../homekit_controller/connection.py | 161 ++++++++++++++++++ .../components/homekit_controller/const.py | 3 + tests/components/homekit_controller/common.py | 5 +- 4 files changed, 170 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index cf349854f52..c777d2944c1 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,24 +1,20 @@ """Support for Homekit device discovery.""" -import asyncio import logging import os from homeassistant.components.discovery import SERVICE_HOMEKIT from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import call_later from .config_flow import load_old_pairings -from .connection import get_accessory_information +from .connection import get_accessory_information HKDevice from .const import ( - CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, KNOWN_DEVICES + CONTROLLER, HOMEKIT_DIR, KNOWN_DEVICES, PAIRING_FILE ) - +from .const import DOMAIN # noqa: pylint: disable=unused-import REQUIREMENTS = ['homekit[IP]==0.13.0'] -HOMEKIT_DIR = '.homekit' - HOMEKIT_IGNORE = [ 'BSB002', 'Home Assistant Bridge', @@ -27,163 +23,12 @@ HOMEKIT_IGNORE = [ _LOGGER = logging.getLogger(__name__) -RETRY_INTERVAL = 60 # seconds - -PAIRING_FILE = "pairing.json" - def escape_characteristic_name(char_name): """Escape any dash or dots in a characteristics name.""" return char_name.replace('-', '_').replace('.', '_') -class HKDevice(): - """HomeKit device.""" - - def __init__(self, hass, host, port, model, hkid, config_num, config): - """Initialise a generic HomeKit device.""" - _LOGGER.info("Setting up Homekit device %s", model) - self.hass = hass - self.controller = hass.data[CONTROLLER] - - self.host = host - self.port = port - self.model = model - self.hkid = hkid - self.config_num = config_num - self.config = config - self.configurator = hass.components.configurator - self._connection_warning_logged = False - - # This just tracks aid/iid pairs so we know if a HK service has been - # mapped to a HA entity. - self.entities = [] - - self.pairing_lock = asyncio.Lock(loop=hass.loop) - - self.pairing = self.controller.pairings.get(hkid) - - if self.pairing is not None: - self.accessory_setup() - else: - self.configure() - - def accessory_setup(self): - """Handle setup of a HomeKit accessory.""" - # pylint: disable=import-error - from homekit.model.services import ServicesTypes - from homekit.exceptions import AccessoryDisconnectedError - - self.pairing.pairing_data['AccessoryIP'] = self.host - self.pairing.pairing_data['AccessoryPort'] = self.port - - try: - data = self.pairing.list_accessories_and_characteristics() - except AccessoryDisconnectedError: - call_later( - self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup()) - return - for accessory in data: - aid = accessory['aid'] - for service in accessory['services']: - iid = service['iid'] - if (aid, iid) in self.entities: - # Don't add the same entity again - continue - - devtype = ServicesTypes.get_short(service['type']) - _LOGGER.debug("Found %s", devtype) - service_info = {'serial': self.hkid, - 'aid': aid, - 'iid': service['iid'], - 'model': self.model, - 'device-type': devtype} - component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None) - if component is not None: - discovery.load_platform(self.hass, component, DOMAIN, - service_info, self.config) - self.entities.append((aid, iid)) - - def device_config_callback(self, callback_data): - """Handle initial pairing.""" - import homekit # pylint: disable=import-error - code = callback_data.get('code').strip() - try: - self.controller.perform_pairing(self.hkid, self.hkid, code) - except homekit.UnavailableError: - error_msg = "This accessory is already paired to another device. \ - Please reset the accessory and try again." - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) - return - except homekit.AuthenticationError: - error_msg = "Incorrect HomeKit code for {}. Please check it and \ - try again.".format(self.model) - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) - return - except homekit.UnknownError: - error_msg = "Received an unknown error. Please file a bug." - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) - raise - - self.pairing = self.controller.pairings.get(self.hkid) - if self.pairing is not None: - pairing_file = os.path.join( - self.hass.config.path(), - HOMEKIT_DIR, - PAIRING_FILE, - ) - self.controller.save_data(pairing_file) - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.request_done(_configurator) - self.accessory_setup() - else: - error_msg = "Unable to pair, please try again" - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) - - def configure(self): - """Obtain the pairing code for a HomeKit device.""" - description = "Please enter the HomeKit code for your {}".format( - self.model) - self.hass.data[DOMAIN+self.hkid] = \ - self.configurator.request_config(self.model, - self.device_config_callback, - description=description, - submit_caption="submit", - fields=[{'id': 'code', - 'name': 'HomeKit code', - 'type': 'string'}]) - - async def get_characteristics(self, *args, **kwargs): - """Read latest state from homekit accessory.""" - async with self.pairing_lock: - chars = await self.hass.async_add_executor_job( - self.pairing.get_characteristics, - *args, - **kwargs, - ) - return chars - - async def put_characteristics(self, characteristics): - """Control a HomeKit device state from Home Assistant.""" - chars = [] - for row in characteristics: - chars.append(( - row['aid'], - row['iid'], - row['value'], - )) - - async with self.pairing_lock: - await self.hass.async_add_executor_job( - self.pairing.put_characteristics, - chars - ) - - class HomeKitEntity(Entity): """Representation of a Home Assistant HomeKit device.""" diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 5550846120b..d875b91eb2c 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -1,4 +1,19 @@ """Helpers for managing a pairing with a HomeKit accessory or bridge.""" +import asyncio +import logging +import os + +from homeassistant.helpers import discovery +from homeassistant.helpers.event import call_later + +from .const import ( + CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, PAIRING_FILE, HOMEKIT_DIR +) + + +RETRY_INTERVAL = 60 # seconds + +_LOGGER = logging.getLogger(__name__) def get_accessory_information(accessory): @@ -33,3 +48,149 @@ def get_accessory_name(accessory_info): if field in accessory_info: return accessory_info[field] return None + + +class HKDevice(): + """HomeKit device.""" + + def __init__(self, hass, host, port, model, hkid, config_num, config): + """Initialise a generic HomeKit device.""" + _LOGGER.info("Setting up Homekit device %s", model) + self.hass = hass + self.controller = hass.data[CONTROLLER] + + self.host = host + self.port = port + self.model = model + self.hkid = hkid + self.config_num = config_num + self.config = config + self.configurator = hass.components.configurator + self._connection_warning_logged = False + + # This just tracks aid/iid pairs so we know if a HK service has been + # mapped to a HA entity. + self.entities = [] + + self.pairing_lock = asyncio.Lock(loop=hass.loop) + + self.pairing = self.controller.pairings.get(hkid) + + if self.pairing is not None: + self.accessory_setup() + else: + self.configure() + + def accessory_setup(self): + """Handle setup of a HomeKit accessory.""" + # pylint: disable=import-error + from homekit.model.services import ServicesTypes + from homekit.exceptions import AccessoryDisconnectedError + + self.pairing.pairing_data['AccessoryIP'] = self.host + self.pairing.pairing_data['AccessoryPort'] = self.port + + try: + data = self.pairing.list_accessories_and_characteristics() + except AccessoryDisconnectedError: + call_later( + self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup()) + return + for accessory in data: + aid = accessory['aid'] + for service in accessory['services']: + iid = service['iid'] + if (aid, iid) in self.entities: + # Don't add the same entity again + continue + + devtype = ServicesTypes.get_short(service['type']) + _LOGGER.debug("Found %s", devtype) + service_info = {'serial': self.hkid, + 'aid': aid, + 'iid': service['iid'], + 'model': self.model, + 'device-type': devtype} + component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None) + if component is not None: + discovery.load_platform(self.hass, component, DOMAIN, + service_info, self.config) + + def device_config_callback(self, callback_data): + """Handle initial pairing.""" + import homekit # pylint: disable=import-error + code = callback_data.get('code').strip() + try: + self.controller.perform_pairing(self.hkid, self.hkid, code) + except homekit.UnavailableError: + error_msg = "This accessory is already paired to another device. \ + Please reset the accessory and try again." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.AuthenticationError: + error_msg = "Incorrect HomeKit code for {}. Please check it and \ + try again.".format(self.model) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.UnknownError: + error_msg = "Received an unknown error. Please file a bug." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + raise + + self.pairing = self.controller.pairings.get(self.hkid) + if self.pairing is not None: + pairing_file = os.path.join( + self.hass.config.path(), + HOMEKIT_DIR, + PAIRING_FILE, + ) + self.controller.save_data(pairing_file) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.request_done(_configurator) + self.accessory_setup() + else: + error_msg = "Unable to pair, please try again" + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + + def configure(self): + """Obtain the pairing code for a HomeKit device.""" + description = "Please enter the HomeKit code for your {}".format( + self.model) + self.hass.data[DOMAIN+self.hkid] = \ + self.configurator.request_config(self.model, + self.device_config_callback, + description=description, + submit_caption="submit", + fields=[{'id': 'code', + 'name': 'HomeKit code', + 'type': 'string'}]) + + async def get_characteristics(self, *args, **kwargs): + """Read latest state from homekit accessory.""" + async with self.pairing_lock: + chars = await self.hass.async_add_executor_job( + self.pairing.get_characteristics, + *args, + **kwargs, + ) + return chars + + async def put_characteristics(self, characteristics): + """Control a HomeKit device state from Home Assistant.""" + chars = [] + for row in characteristics: + chars.append(( + row['aid'], + row['iid'], + row['value'], + )) + + async with self.pairing_lock: + await self.hass.async_add_executor_job( + self.pairing.put_characteristics, + chars + ) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 90a105b0ad9..de9663f1202 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -4,6 +4,9 @@ DOMAIN = 'homekit_controller' KNOWN_DEVICES = "{}-devices".format(DOMAIN) CONTROLLER = "{}-controller".format(DOMAIN) +HOMEKIT_DIR = '.homekit' +PAIRING_FILE = 'pairing.json' + # Mapping from Homekit type to component. HOMEKIT_ACCESSORY_DISPATCH = { 'lightbulb': 'light', diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 2c60dc168d0..2d659d42dfb 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -8,8 +8,9 @@ from homekit.model.characteristics import ( AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes) from homekit.model import Accessory, get_id from homekit.exceptions import AccessoryNotFoundError -from homeassistant.components.homekit_controller import ( - DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT) +from homeassistant.components.homekit_controller import SERVICE_HOMEKIT +from homeassistant.components.homekit_controller.const import ( + DOMAIN, HOMEKIT_ACCESSORY_DISPATCH) 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