diff --git a/homeassistant/components/climate/homekit_controller.py b/homeassistant/components/climate/homekit_controller.py index f720fb602773..e703bfe182d5 100644 --- a/homeassistant/components/climate/homekit_controller.py +++ b/homeassistant/components/climate/homekit_controller.py @@ -50,23 +50,23 @@ def __init__(self, *args): def update_characteristics(self, characteristics): """Synchronise device state with Home Assistant.""" # pylint: disable=import-error - from homekit import CharacteristicsTypes as ctypes + from homekit.models.characteristics import CharacteristicsTypes for characteristic in characteristics: ctype = characteristic['type'] - if ctype == ctypes.HEATING_COOLING_CURRENT: + if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT: self._state = MODE_HOMEKIT_TO_HASS.get( characteristic['value']) - if ctype == ctypes.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( characteristic['value']) self._valid_modes = [MODE_HOMEKIT_TO_HASS.get( mode) for mode in characteristic['valid-values']] - elif ctype == ctypes.TEMPERATURE_CURRENT: + elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT: self._current_temp = characteristic['value'] - elif ctype == ctypes.TEMPERATURE_TARGET: + elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET: self._chars['target_temp'] = characteristic['iid'] self._features |= SUPPORT_TARGET_TEMPERATURE self._target_temp = characteristic['value'] diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 45b3442ba9f2..689a8e9d91a4 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -4,18 +4,16 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/homekit_controller/ """ -import http import json import logging import os -import uuid 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 -REQUIREMENTS = ['homekit==0.10'] +REQUIREMENTS = ['homekit==0.12.0'] DOMAIN = 'homekit_controller' HOMEKIT_DIR = '.homekit' @@ -36,6 +34,7 @@ KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) KNOWN_DEVICES = "{}-devices".format(DOMAIN) +CONTROLLER = "{}-controller".format(DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -47,32 +46,18 @@ class HomeKitConnectionError(ConnectionError): """Raised when unable to connect to target device.""" -def homekit_http_send(self, message_body=None, encode_chunked=False): - r"""Send the currently buffered request and clear the buffer. - - Appends an extra \r\n to the buffer. - A message_body may be specified, to be appended to the request. - """ - # pylint: disable=protected-access - self._buffer.extend((b"", b"")) - msg = b"\r\n".join(self._buffer) - del self._buffer[:] - - if message_body is not None: - msg = msg + message_body - - self.send(msg) - - def get_serial(accessory): """Obtain the serial number of a HomeKit device.""" - import homekit # pylint: disable=import-error + # pylint: disable=import-error + from homekit.model.services import ServicesTypes + from homekit.model.characteristics import CharacteristicsTypes + for service in accessory['services']: - if homekit.ServicesTypes.get_short(service['type']) != \ + if ServicesTypes.get_short(service['type']) != \ 'accessory-information': continue for characteristic in service['characteristics']: - ctype = homekit.CharacteristicsTypes.get_short( + ctype = CharacteristicsTypes.get_short( characteristic['type']) if ctype != 'serial-number': continue @@ -85,10 +70,10 @@ class HKDevice(): def __init__(self, hass, host, port, model, hkid, config_num, config): """Initialise a generic HomeKit device.""" - import homekit # pylint: disable=import-error - _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 @@ -96,50 +81,30 @@ def __init__(self, hass, host, port, model, hkid, config_num, config): self.config_num = config_num self.config = config self.configurator = hass.components.configurator - self.conn = None - self.securecon = None self._connection_warning_logged = False - data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR) - if not os.path.isdir(data_dir): - os.mkdir(data_dir) - - self.pairing_file = os.path.join(data_dir, 'hk-{}'.format(hkid)) - self.pairing_data = homekit.load_pairing(self.pairing_file) - - # Monkey patch httpclient for increased compatibility - # pylint: disable=protected-access - http.client.HTTPConnection._send_output = homekit_http_send + self.pairing = self.controller.pairings.get(hkid) - if self.pairing_data is not None: + if self.pairing is not None: self.accessory_setup() else: self.configure() - def connect(self): - """Open the connection to the HomeKit device.""" - # pylint: disable=import-error - import homekit - - self.conn = http.client.HTTPConnection( - self.host, port=self.port, timeout=REQUEST_TIMEOUT) - if self.pairing_data is not None: - controllerkey, accessorykey = \ - homekit.get_session_keys(self.conn, self.pairing_data) - self.securecon = homekit.SecureHttp( - self.conn.sock, accessorykey, controllerkey) - def accessory_setup(self): """Handle setup of a HomeKit accessory.""" - import homekit # pylint: disable=import-error + # pylint: disable=import-error + from homekit.model.services import ServicesTypes + + self.pairing.pairing_data['AccessoryIP'] = self.host + self.pairing.pairing_data['AccessoryPort'] = self.port try: - data = self.get_json('/accessories') + data = self.pairing.list_accessories_and_characteristics() except HomeKitConnectionError: call_later( self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup()) return - for accessory in data['accessories']: + for accessory in data: serial = get_serial(accessory) if serial in self.hass.data[KNOWN_ACCESSORIES]: continue @@ -149,67 +114,45 @@ def accessory_setup(self): service_info = {'serial': serial, 'aid': aid, 'iid': service['iid']} - devtype = homekit.ServicesTypes.get_short(service['type']) + devtype = ServicesTypes.get_short(service['type']) _LOGGER.debug("Found %s", 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 get_json(self, target): - """Get JSON data from the device.""" - try: - if self.conn is None: - self.connect() - response = self.securecon.get(target) - data = json.loads(response.read().decode()) - - # After a successful connection, clear the warning logged status - self._connection_warning_logged = False - - return data - except (ConnectionError, OSError, json.JSONDecodeError) as ex: - # Mark connection as failed - if not self._connection_warning_logged: - _LOGGER.warning("Failed to connect to homekit device", - exc_info=ex) - self._connection_warning_logged = True - else: - _LOGGER.debug("Failed to connect to homekit device", - exc_info=ex) - self.conn = None - self.securecon = None - raise HomeKitConnectionError() from ex - def device_config_callback(self, callback_data): """Handle initial pairing.""" import homekit # pylint: disable=import-error - pairing_id = str(uuid.uuid4()) code = callback_data.get('code').strip() try: - self.connect() - self.pairing_data = homekit.perform_pair_setup(self.conn, code, - pairing_id) - except homekit.exception.UnavailableError: + 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.exception.AuthenticationError: + 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.exception.UnknownError: + 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 - if self.pairing_data is not None: - homekit.save_pairing(self.pairing_file, self.pairing_data) + 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.json' + ) + self.controller.save_data(pairing_file) _configurator = self.hass.data[DOMAIN+self.hkid] self.configurator.request_done(_configurator) self.accessory_setup() @@ -248,10 +191,11 @@ def __init__(self, accessory, devinfo): def update(self): """Obtain a HomeKit device's state.""" try: - data = self._accessory.get_json('/accessories') + pairing = self._accessory.pairing + data = pairing.list_accessories_and_characteristics() except HomeKitConnectionError: return - for accessory in data['accessories']: + for accessory in data: if accessory['aid'] != self._aid: continue for service in accessory['services']: @@ -273,7 +217,7 @@ def name(self): @property def available(self) -> bool: """Return True if entity is available.""" - return self._accessory.conn is not None + return self._accessory.pairing is not None def update_characteristics(self, characteristics): """Synchronise a HomeKit device state with Home Assistant.""" @@ -281,12 +225,45 @@ def update_characteristics(self, characteristics): def put_characteristics(self, characteristics): """Control a HomeKit device state from Home Assistant.""" - body = json.dumps({'characteristics': characteristics}) - self._accessory.securecon.put('/characteristics', body) + chars = [] + for row in characteristics: + chars.append(( + row['aid'], + row['iid'], + row['value'], + )) + + self._accessory.pairing.put_characteristics(chars) def setup(hass, config): """Set up for Homekit devices.""" + # pylint: disable=import-error + import homekit + from homekit.controller import Pairing + + hass.data[CONTROLLER] = controller = homekit.Controller() + + data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR) + if not os.path.isdir(data_dir): + os.mkdir(data_dir) + + pairing_file = os.path.join(data_dir, 'pairings.json') + if os.path.exists(pairing_file): + controller.load_data(pairing_file) + + # Migrate any existing pairings to the new internal homekit_python format + for device in os.listdir(data_dir): + if not device.startswith('hk-'): + continue + alias = device[3:] + if alias in controller.pairings: + continue + with open(os.path.join(data_dir, device)) as pairing_data_fp: + pairing_data = json.load(pairing_data_fp) + controller.pairings[alias] = Pairing(pairing_data) + controller.save_data(pairing_file) + def discovery_dispatch(service, discovery_info): """Dispatcher for Homekit discovery events.""" # model, id diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py index b874bc49f0e5..7c8119f6e89d 100644 --- a/homeassistant/components/light/homekit_controller.py +++ b/homeassistant/components/light/homekit_controller.py @@ -38,11 +38,12 @@ def __init__(self, *args): def update_characteristics(self, characteristics): """Synchronise light state with Home Assistant.""" - import homekit # pylint: disable=import-error + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes for characteristic in characteristics: ctype = characteristic['type'] - ctype = homekit.CharacteristicsTypes.get_short(ctype) + ctype = CharacteristicsTypes.get_short(ctype) if ctype == "on": self._chars['on'] = characteristic['iid'] self._on = characteristic['value'] diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py index 6333375b560e..51a71163bade 100644 --- a/homeassistant/components/switch/homekit_controller.py +++ b/homeassistant/components/switch/homekit_controller.py @@ -35,11 +35,12 @@ def __init__(self, *args): def update_characteristics(self, characteristics): """Synchronise the switch state with Home Assistant.""" - import homekit # pylint: disable=import-error + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes for characteristic in characteristics: ctype = characteristic['type'] - ctype = homekit.CharacteristicsTypes.get_short(ctype) + ctype = CharacteristicsTypes.get_short(ctype) if ctype == "on": self._chars['on'] = characteristic['iid'] self._on = characteristic['value'] diff --git a/requirements_all.txt b/requirements_all.txt index 7bb0a0490d2a..2c389ede713a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -511,7 +511,7 @@ home-assistant-frontend==20181219.0 homeassistant-pyozw==0.1.1 # homeassistant.components.homekit_controller -# homekit==0.10 +# homekit==0.12.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8