Skip to content

Commit

Permalink
Update homekit controller to homekit==0.12.0 (#19549)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jc2k authored and MartinHjelmare committed Dec 24, 2018
1 parent a8797a0 commit e0f50a9
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 104 deletions.
10 changes: 5 additions & 5 deletions homeassistant/components/climate/homekit_controller.py
Expand Up @@ -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']
Expand Down
165 changes: 71 additions & 94 deletions homeassistant/components/homekit_controller/__init__.py
Expand Up @@ -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'
Expand All @@ -36,6 +34,7 @@

KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
CONTROLLER = "{}-controller".format(DOMAIN)

_LOGGER = logging.getLogger(__name__)

Expand All @@ -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
Expand All @@ -85,61 +70,41 @@ 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
self.hkid = hkid
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
Expand All @@ -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()
Expand Down Expand Up @@ -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']:
Expand All @@ -273,20 +217,53 @@ 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."""
raise NotImplementedError

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
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/light/homekit_controller.py
Expand Up @@ -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']
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/switch/homekit_controller.py
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Expand Up @@ -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
Expand Down

0 comments on commit e0f50a9

Please sign in to comment.