Homekit controller reconnect (#17060)

* Add threaded call_later helper

* Reconnect to device when connection fails

* Consolidate connection logs and warn on first
This commit is contained in:
Adam Mills 2018-10-04 03:25:05 -04:00 committed by Paulus Schoutsen
parent 6a0c9a718e
commit 3abdf217bb
3 changed files with 90 additions and 13 deletions

View file

@ -13,6 +13,7 @@ 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']
@ -37,6 +38,13 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN)
_LOGGER = logging.getLogger(__name__)
REQUEST_TIMEOUT = 5 # seconds
RETRY_INTERVAL = 60 # seconds
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.
@ -89,6 +97,9 @@ class HKDevice():
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):
@ -101,23 +112,35 @@ class HKDevice():
# pylint: disable=protected-access
http.client.HTTPConnection._send_output = homekit_http_send
self.conn = http.client.HTTPConnection(self.host, port=self.port)
if self.pairing_data 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."""
# pylint: disable=import-error
import homekit
self.controllerkey, self.accessorykey = \
homekit.get_session_keys(self.conn, self.pairing_data)
self.securecon = homekit.SecureHttp(self.conn.sock,
self.accessorykey,
self.controllerkey)
response = self.securecon.get('/accessories')
data = json.loads(response.read().decode())
try:
data = self.get_json('/accessories')
except HomeKitConnectionError:
call_later(
self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup())
return
for accessory in data['accessories']:
serial = get_serial(accessory)
if serial in self.hass.data[KNOWN_ACCESSORIES]:
@ -135,6 +158,31 @@ class HKDevice():
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."""
# pylint: disable=import-error
@ -142,6 +190,7 @@ class HKDevice():
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:
@ -192,7 +241,7 @@ class HomeKitEntity(Entity):
def __init__(self, accessory, devinfo):
"""Initialise a generic HomeKit device."""
self._name = accessory.model
self._securecon = accessory.securecon
self._accessory = accessory
self._aid = devinfo['aid']
self._iid = devinfo['iid']
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
@ -201,8 +250,10 @@ class HomeKitEntity(Entity):
def update(self):
"""Obtain a HomeKit device's state."""
response = self._securecon.get('/accessories')
data = json.loads(response.read().decode())
try:
data = self._accessory.get_json('/accessories')
except HomeKitConnectionError:
return
for accessory in data['accessories']:
if accessory['aid'] != self._aid:
continue
@ -222,6 +273,11 @@ class HomeKitEntity(Entity):
"""Return the name of the device if any."""
return self._name
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._accessory.conn is not None
def update_characteristics(self, characteristics):
"""Synchronise a HomeKit device state with Home Assistant."""
raise NotImplementedError
@ -229,7 +285,7 @@ class HomeKitEntity(Entity):
def put_characteristics(self, characteristics):
"""Control a HomeKit device state from Home Assistant."""
body = json.dumps({'characteristics': characteristics})
self._securecon.put('/characteristics', body)
self._accessory.securecon.put('/characteristics', body)
def setup(hass, config):