Update homekit controller to homekit==0.12.0 (#19549)
parent
a8797a08c6
commit
e0f50a9e54
|
@ -50,23 +50,23 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
|
||||||
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
|
||||||
from homekit import CharacteristicsTypes as ctypes
|
from homekit.models.characteristics import CharacteristicsTypes
|
||||||
|
|
||||||
for characteristic in characteristics:
|
for characteristic in characteristics:
|
||||||
ctype = characteristic['type']
|
ctype = characteristic['type']
|
||||||
if ctype == ctypes.HEATING_COOLING_CURRENT:
|
if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT:
|
||||||
self._state = MODE_HOMEKIT_TO_HASS.get(
|
self._state = MODE_HOMEKIT_TO_HASS.get(
|
||||||
characteristic['value'])
|
characteristic['value'])
|
||||||
if ctype == ctypes.HEATING_COOLING_TARGET:
|
if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET:
|
||||||
self._chars['target_mode'] = characteristic['iid']
|
self._chars['target_mode'] = characteristic['iid']
|
||||||
self._features |= SUPPORT_OPERATION_MODE
|
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'])
|
||||||
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
|
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
|
||||||
mode) for mode in characteristic['valid-values']]
|
mode) for mode in characteristic['valid-values']]
|
||||||
elif ctype == ctypes.TEMPERATURE_CURRENT:
|
elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT:
|
||||||
self._current_temp = characteristic['value']
|
self._current_temp = characteristic['value']
|
||||||
elif ctype == ctypes.TEMPERATURE_TARGET:
|
elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET:
|
||||||
self._chars['target_temp'] = characteristic['iid']
|
self._chars['target_temp'] = characteristic['iid']
|
||||||
self._features |= SUPPORT_TARGET_TEMPERATURE
|
self._features |= SUPPORT_TARGET_TEMPERATURE
|
||||||
self._target_temp = characteristic['value']
|
self._target_temp = characteristic['value']
|
||||||
|
|
|
@ -4,18 +4,16 @@ Support for Homekit device discovery.
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/homekit_controller/
|
https://home-assistant.io/components/homekit_controller/
|
||||||
"""
|
"""
|
||||||
import http
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import uuid
|
|
||||||
|
|
||||||
from homeassistant.components.discovery import SERVICE_HOMEKIT
|
from homeassistant.components.discovery import SERVICE_HOMEKIT
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import call_later
|
from homeassistant.helpers.event import call_later
|
||||||
|
|
||||||
REQUIREMENTS = ['homekit==0.10']
|
REQUIREMENTS = ['homekit==0.12.0']
|
||||||
|
|
||||||
DOMAIN = 'homekit_controller'
|
DOMAIN = 'homekit_controller'
|
||||||
HOMEKIT_DIR = '.homekit'
|
HOMEKIT_DIR = '.homekit'
|
||||||
|
@ -36,6 +34,7 @@ HOMEKIT_IGNORE = [
|
||||||
|
|
||||||
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
|
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
|
||||||
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
|
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
|
||||||
|
CONTROLLER = "{}-controller".format(DOMAIN)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -47,32 +46,18 @@ class HomeKitConnectionError(ConnectionError):
|
||||||
"""Raised when unable to connect to target device."""
|
"""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):
|
def get_serial(accessory):
|
||||||
"""Obtain the serial number of a HomeKit device."""
|
"""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']:
|
for service in accessory['services']:
|
||||||
if homekit.ServicesTypes.get_short(service['type']) != \
|
if ServicesTypes.get_short(service['type']) != \
|
||||||
'accessory-information':
|
'accessory-information':
|
||||||
continue
|
continue
|
||||||
for characteristic in service['characteristics']:
|
for characteristic in service['characteristics']:
|
||||||
ctype = homekit.CharacteristicsTypes.get_short(
|
ctype = CharacteristicsTypes.get_short(
|
||||||
characteristic['type'])
|
characteristic['type'])
|
||||||
if ctype != 'serial-number':
|
if ctype != 'serial-number':
|
||||||
continue
|
continue
|
||||||
|
@ -85,10 +70,10 @@ class HKDevice():
|
||||||
|
|
||||||
def __init__(self, hass, host, port, model, hkid, config_num, config):
|
def __init__(self, hass, host, port, model, hkid, config_num, config):
|
||||||
"""Initialise a generic HomeKit device."""
|
"""Initialise a generic HomeKit device."""
|
||||||
import homekit # pylint: disable=import-error
|
|
||||||
|
|
||||||
_LOGGER.info("Setting up Homekit device %s", model)
|
_LOGGER.info("Setting up Homekit device %s", model)
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
self.controller = hass.data[CONTROLLER]
|
||||||
|
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.model = model
|
self.model = model
|
||||||
|
@ -96,50 +81,30 @@ class HKDevice():
|
||||||
self.config_num = config_num
|
self.config_num = config_num
|
||||||
self.config = config
|
self.config = config
|
||||||
self.configurator = hass.components.configurator
|
self.configurator = hass.components.configurator
|
||||||
self.conn = None
|
|
||||||
self.securecon = None
|
|
||||||
self._connection_warning_logged = False
|
self._connection_warning_logged = False
|
||||||
|
|
||||||
data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR)
|
self.pairing = self.controller.pairings.get(hkid)
|
||||||
if not os.path.isdir(data_dir):
|
|
||||||
os.mkdir(data_dir)
|
|
||||||
|
|
||||||
self.pairing_file = os.path.join(data_dir, 'hk-{}'.format(hkid))
|
if self.pairing is not None:
|
||||||
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
|
|
||||||
|
|
||||||
if self.pairing_data is not None:
|
|
||||||
self.accessory_setup()
|
self.accessory_setup()
|
||||||
else:
|
else:
|
||||||
self.configure()
|
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):
|
def accessory_setup(self):
|
||||||
"""Handle setup of a HomeKit accessory."""
|
"""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:
|
try:
|
||||||
data = self.get_json('/accessories')
|
data = self.pairing.list_accessories_and_characteristics()
|
||||||
except HomeKitConnectionError:
|
except HomeKitConnectionError:
|
||||||
call_later(
|
call_later(
|
||||||
self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup())
|
self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup())
|
||||||
return
|
return
|
||||||
for accessory in data['accessories']:
|
for accessory in data:
|
||||||
serial = get_serial(accessory)
|
serial = get_serial(accessory)
|
||||||
if serial in self.hass.data[KNOWN_ACCESSORIES]:
|
if serial in self.hass.data[KNOWN_ACCESSORIES]:
|
||||||
continue
|
continue
|
||||||
|
@ -149,67 +114,45 @@ class HKDevice():
|
||||||
service_info = {'serial': serial,
|
service_info = {'serial': serial,
|
||||||
'aid': aid,
|
'aid': aid,
|
||||||
'iid': service['iid']}
|
'iid': service['iid']}
|
||||||
devtype = homekit.ServicesTypes.get_short(service['type'])
|
devtype = ServicesTypes.get_short(service['type'])
|
||||||
_LOGGER.debug("Found %s", devtype)
|
_LOGGER.debug("Found %s", devtype)
|
||||||
component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None)
|
component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None)
|
||||||
if component is not None:
|
if component is not None:
|
||||||
discovery.load_platform(self.hass, component, DOMAIN,
|
discovery.load_platform(self.hass, component, DOMAIN,
|
||||||
service_info, self.config)
|
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):
|
def device_config_callback(self, callback_data):
|
||||||
"""Handle initial pairing."""
|
"""Handle initial pairing."""
|
||||||
import homekit # pylint: disable=import-error
|
import homekit # pylint: disable=import-error
|
||||||
pairing_id = str(uuid.uuid4())
|
|
||||||
code = callback_data.get('code').strip()
|
code = callback_data.get('code').strip()
|
||||||
try:
|
try:
|
||||||
self.connect()
|
self.controller.perform_pairing(self.hkid, self.hkid, code)
|
||||||
self.pairing_data = homekit.perform_pair_setup(self.conn, code,
|
except homekit.UnavailableError:
|
||||||
pairing_id)
|
|
||||||
except homekit.exception.UnavailableError:
|
|
||||||
error_msg = "This accessory is already paired to another device. \
|
error_msg = "This accessory is already paired to another device. \
|
||||||
Please reset the accessory and try again."
|
Please reset the accessory and try again."
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
_configurator = self.hass.data[DOMAIN+self.hkid]
|
||||||
self.configurator.notify_errors(_configurator, error_msg)
|
self.configurator.notify_errors(_configurator, error_msg)
|
||||||
return
|
return
|
||||||
except homekit.exception.AuthenticationError:
|
except homekit.AuthenticationError:
|
||||||
error_msg = "Incorrect HomeKit code for {}. Please check it and \
|
error_msg = "Incorrect HomeKit code for {}. Please check it and \
|
||||||
try again.".format(self.model)
|
try again.".format(self.model)
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
_configurator = self.hass.data[DOMAIN+self.hkid]
|
||||||
self.configurator.notify_errors(_configurator, error_msg)
|
self.configurator.notify_errors(_configurator, error_msg)
|
||||||
return
|
return
|
||||||
except homekit.exception.UnknownError:
|
except homekit.UnknownError:
|
||||||
error_msg = "Received an unknown error. Please file a bug."
|
error_msg = "Received an unknown error. Please file a bug."
|
||||||
_configurator = self.hass.data[DOMAIN+self.hkid]
|
_configurator = self.hass.data[DOMAIN+self.hkid]
|
||||||
self.configurator.notify_errors(_configurator, error_msg)
|
self.configurator.notify_errors(_configurator, error_msg)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if self.pairing_data is not None:
|
self.pairing = self.controller.pairings.get(self.hkid)
|
||||||
homekit.save_pairing(self.pairing_file, self.pairing_data)
|
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]
|
_configurator = self.hass.data[DOMAIN+self.hkid]
|
||||||
self.configurator.request_done(_configurator)
|
self.configurator.request_done(_configurator)
|
||||||
self.accessory_setup()
|
self.accessory_setup()
|
||||||
|
@ -248,10 +191,11 @@ class HomeKitEntity(Entity):
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Obtain a HomeKit device's state."""
|
"""Obtain a HomeKit device's state."""
|
||||||
try:
|
try:
|
||||||
data = self._accessory.get_json('/accessories')
|
pairing = self._accessory.pairing
|
||||||
|
data = pairing.list_accessories_and_characteristics()
|
||||||
except HomeKitConnectionError:
|
except HomeKitConnectionError:
|
||||||
return
|
return
|
||||||
for accessory in data['accessories']:
|
for accessory in data:
|
||||||
if accessory['aid'] != self._aid:
|
if accessory['aid'] != self._aid:
|
||||||
continue
|
continue
|
||||||
for service in accessory['services']:
|
for service in accessory['services']:
|
||||||
|
@ -273,7 +217,7 @@ class HomeKitEntity(Entity):
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""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):
|
def update_characteristics(self, characteristics):
|
||||||
"""Synchronise a HomeKit device state with Home Assistant."""
|
"""Synchronise a HomeKit device state with Home Assistant."""
|
||||||
|
@ -281,12 +225,45 @@ class HomeKitEntity(Entity):
|
||||||
|
|
||||||
def put_characteristics(self, characteristics):
|
def put_characteristics(self, characteristics):
|
||||||
"""Control a HomeKit device state from Home Assistant."""
|
"""Control a HomeKit device state from Home Assistant."""
|
||||||
body = json.dumps({'characteristics': characteristics})
|
chars = []
|
||||||
self._accessory.securecon.put('/characteristics', body)
|
for row in characteristics:
|
||||||
|
chars.append((
|
||||||
|
row['aid'],
|
||||||
|
row['iid'],
|
||||||
|
row['value'],
|
||||||
|
))
|
||||||
|
|
||||||
|
self._accessory.pairing.put_characteristics(chars)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up for Homekit devices."""
|
"""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):
|
def discovery_dispatch(service, discovery_info):
|
||||||
"""Dispatcher for Homekit discovery events."""
|
"""Dispatcher for Homekit discovery events."""
|
||||||
# model, id
|
# model, id
|
||||||
|
|
|
@ -38,11 +38,12 @@ class HomeKitLight(HomeKitEntity, Light):
|
||||||
|
|
||||||
def update_characteristics(self, characteristics):
|
def update_characteristics(self, characteristics):
|
||||||
"""Synchronise light state with Home Assistant."""
|
"""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:
|
for characteristic in characteristics:
|
||||||
ctype = characteristic['type']
|
ctype = characteristic['type']
|
||||||
ctype = homekit.CharacteristicsTypes.get_short(ctype)
|
ctype = CharacteristicsTypes.get_short(ctype)
|
||||||
if ctype == "on":
|
if ctype == "on":
|
||||||
self._chars['on'] = characteristic['iid']
|
self._chars['on'] = characteristic['iid']
|
||||||
self._on = characteristic['value']
|
self._on = characteristic['value']
|
||||||
|
|
|
@ -35,11 +35,12 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
||||||
|
|
||||||
def update_characteristics(self, characteristics):
|
def update_characteristics(self, characteristics):
|
||||||
"""Synchronise the switch state with Home Assistant."""
|
"""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:
|
for characteristic in characteristics:
|
||||||
ctype = characteristic['type']
|
ctype = characteristic['type']
|
||||||
ctype = homekit.CharacteristicsTypes.get_short(ctype)
|
ctype = CharacteristicsTypes.get_short(ctype)
|
||||||
if ctype == "on":
|
if ctype == "on":
|
||||||
self._chars['on'] = characteristic['iid']
|
self._chars['on'] = characteristic['iid']
|
||||||
self._on = characteristic['value']
|
self._on = characteristic['value']
|
||||||
|
|
|
@ -511,7 +511,7 @@ home-assistant-frontend==20181219.0
|
||||||
homeassistant-pyozw==0.1.1
|
homeassistant-pyozw==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.homekit_controller
|
# homeassistant.components.homekit_controller
|
||||||
# homekit==0.10
|
# homekit==0.12.0
|
||||||
|
|
||||||
# homeassistant.components.homematicip_cloud
|
# homeassistant.components.homematicip_cloud
|
||||||
homematicip==0.9.8
|
homematicip==0.9.8
|
||||||
|
|
Loading…
Reference in New Issue