mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
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:
parent
6a0c9a718e
commit
3abdf217bb
@ -13,6 +13,7 @@ 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
|
||||||
|
|
||||||
REQUIREMENTS = ['homekit==0.10']
|
REQUIREMENTS = ['homekit==0.10']
|
||||||
|
|
||||||
@ -37,6 +38,13 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN)
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
def homekit_http_send(self, message_body=None, encode_chunked=False):
|
||||||
r"""Send the currently buffered request and clear the buffer.
|
r"""Send the currently buffered request and clear the buffer.
|
||||||
@ -89,6 +97,9 @@ 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
|
||||||
|
|
||||||
data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR)
|
data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR)
|
||||||
if not os.path.isdir(data_dir):
|
if not os.path.isdir(data_dir):
|
||||||
@ -101,23 +112,35 @@ class HKDevice():
|
|||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
http.client.HTTPConnection._send_output = homekit_http_send
|
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:
|
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."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import homekit
|
import homekit
|
||||||
self.controllerkey, self.accessorykey = \
|
|
||||||
homekit.get_session_keys(self.conn, self.pairing_data)
|
try:
|
||||||
self.securecon = homekit.SecureHttp(self.conn.sock,
|
data = self.get_json('/accessories')
|
||||||
self.accessorykey,
|
except HomeKitConnectionError:
|
||||||
self.controllerkey)
|
call_later(
|
||||||
response = self.securecon.get('/accessories')
|
self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup())
|
||||||
data = json.loads(response.read().decode())
|
return
|
||||||
for accessory in data['accessories']:
|
for accessory in data['accessories']:
|
||||||
serial = get_serial(accessory)
|
serial = get_serial(accessory)
|
||||||
if serial in self.hass.data[KNOWN_ACCESSORIES]:
|
if serial in self.hass.data[KNOWN_ACCESSORIES]:
|
||||||
@ -135,6 +158,31 @@ class HKDevice():
|
|||||||
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."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
@ -142,6 +190,7 @@ class HKDevice():
|
|||||||
pairing_id = str(uuid.uuid4())
|
pairing_id = str(uuid.uuid4())
|
||||||
code = callback_data.get('code').strip()
|
code = callback_data.get('code').strip()
|
||||||
try:
|
try:
|
||||||
|
self.connect()
|
||||||
self.pairing_data = homekit.perform_pair_setup(self.conn, code,
|
self.pairing_data = homekit.perform_pair_setup(self.conn, code,
|
||||||
pairing_id)
|
pairing_id)
|
||||||
except homekit.exception.UnavailableError:
|
except homekit.exception.UnavailableError:
|
||||||
@ -192,7 +241,7 @@ class HomeKitEntity(Entity):
|
|||||||
def __init__(self, accessory, devinfo):
|
def __init__(self, accessory, devinfo):
|
||||||
"""Initialise a generic HomeKit device."""
|
"""Initialise a generic HomeKit device."""
|
||||||
self._name = accessory.model
|
self._name = accessory.model
|
||||||
self._securecon = accessory.securecon
|
self._accessory = accessory
|
||||||
self._aid = devinfo['aid']
|
self._aid = devinfo['aid']
|
||||||
self._iid = devinfo['iid']
|
self._iid = devinfo['iid']
|
||||||
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
|
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
|
||||||
@ -201,8 +250,10 @@ class HomeKitEntity(Entity):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Obtain a HomeKit device's state."""
|
"""Obtain a HomeKit device's state."""
|
||||||
response = self._securecon.get('/accessories')
|
try:
|
||||||
data = json.loads(response.read().decode())
|
data = self._accessory.get_json('/accessories')
|
||||||
|
except HomeKitConnectionError:
|
||||||
|
return
|
||||||
for accessory in data['accessories']:
|
for accessory in data['accessories']:
|
||||||
if accessory['aid'] != self._aid:
|
if accessory['aid'] != self._aid:
|
||||||
continue
|
continue
|
||||||
@ -222,6 +273,11 @@ class HomeKitEntity(Entity):
|
|||||||
"""Return the name of the device if any."""
|
"""Return the name of the device if any."""
|
||||||
return self._name
|
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):
|
def update_characteristics(self, characteristics):
|
||||||
"""Synchronise a HomeKit device state with Home Assistant."""
|
"""Synchronise a HomeKit device state with Home Assistant."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -229,7 +285,7 @@ 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})
|
body = json.dumps({'characteristics': characteristics})
|
||||||
self._securecon.put('/characteristics', body)
|
self._accessory.securecon.put('/characteristics', body)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
@ -227,6 +227,10 @@ def async_call_later(hass, delay, action):
|
|||||||
hass, action, dt_util.utcnow() + timedelta(seconds=delay))
|
hass, action, dt_util.utcnow() + timedelta(seconds=delay))
|
||||||
|
|
||||||
|
|
||||||
|
call_later = threaded_listener_factory(
|
||||||
|
async_call_later)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def async_track_time_interval(hass, action, interval):
|
def async_track_time_interval(hass, action, interval):
|
||||||
|
@ -13,6 +13,7 @@ import homeassistant.core as ha
|
|||||||
from homeassistant.const import MATCH_ALL
|
from homeassistant.const import MATCH_ALL
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_call_later,
|
async_call_later,
|
||||||
|
call_later,
|
||||||
track_point_in_utc_time,
|
track_point_in_utc_time,
|
||||||
track_point_in_time,
|
track_point_in_time,
|
||||||
track_utc_time_change,
|
track_utc_time_change,
|
||||||
@ -645,6 +646,22 @@ class TestEventHelpers(unittest.TestCase):
|
|||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
self.assertEqual(0, len(specific_runs))
|
self.assertEqual(0, len(specific_runs))
|
||||||
|
|
||||||
|
def test_call_later(self):
|
||||||
|
"""Test calling an action later."""
|
||||||
|
def action(): pass
|
||||||
|
now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC)
|
||||||
|
|
||||||
|
with patch('homeassistant.helpers.event'
|
||||||
|
'.async_track_point_in_utc_time') as mock, \
|
||||||
|
patch('homeassistant.util.dt.utcnow', return_value=now):
|
||||||
|
call_later(self.hass, 3, action)
|
||||||
|
|
||||||
|
assert len(mock.mock_calls) == 1
|
||||||
|
p_hass, p_action, p_point = mock.mock_calls[0][1]
|
||||||
|
assert p_hass is self.hass
|
||||||
|
assert p_action is action
|
||||||
|
assert p_point == now + timedelta(seconds=3)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_async_call_later(hass):
|
def test_async_call_later(hass):
|
||||||
@ -659,7 +676,7 @@ def test_async_call_later(hass):
|
|||||||
|
|
||||||
assert len(mock.mock_calls) == 1
|
assert len(mock.mock_calls) == 1
|
||||||
p_hass, p_action, p_point = mock.mock_calls[0][1]
|
p_hass, p_action, p_point = mock.mock_calls[0][1]
|
||||||
assert hass is hass
|
assert p_hass is hass
|
||||||
assert p_action is action
|
assert p_action is action
|
||||||
assert p_point == now + timedelta(seconds=3)
|
assert p_point == now + timedelta(seconds=3)
|
||||||
assert remove is mock()
|
assert remove is mock()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user