Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 68 additions & 12 deletions homeassistant/components/homekit_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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']

Expand All @@ -37,6 +38,13 @@

_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.
Expand Down Expand Up @@ -89,6 +97,9 @@ 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):
Expand All @@ -101,23 +112,35 @@ def __init__(self, hass, host, port, model, hkid, config_num, config):
# 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]:
Expand All @@ -135,13 +158,39 @@ def accessory_setup(self):
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
import homekit
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:
Expand Down Expand Up @@ -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)
Expand All @@ -201,8 +250,10 @@ def __init__(self, accessory, devinfo):

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
Expand All @@ -222,14 +273,19 @@ def name(self):
"""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

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):
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/helpers/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ def async_call_later(hass, delay, action):
hass, action, dt_util.utcnow() + timedelta(seconds=delay))


call_later = threaded_listener_factory(
async_call_later)


@callback
@bind_hass
def async_track_time_interval(hass, action, interval):
Expand Down
19 changes: 18 additions & 1 deletion tests/helpers/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from homeassistant.const import MATCH_ALL
from homeassistant.helpers.event import (
async_call_later,
call_later,
track_point_in_utc_time,
track_point_in_time,
track_utc_time_change,
Expand Down Expand Up @@ -645,6 +646,22 @@ def test_periodic_task_wrong_input(self):
self.hass.block_till_done()
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
def test_async_call_later(hass):
Expand All @@ -659,7 +676,7 @@ def action(): pass

assert len(mock.mock_calls) == 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_point == now + timedelta(seconds=3)
assert remove is mock()