Skip to content
46 changes: 18 additions & 28 deletions homeassistant/components/homekit_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,23 @@ class HomeKitEntity(Entity):

def __init__(self, accessory, devinfo):
"""Initialise a generic HomeKit device."""
self._available = True
self._accessory = accessory
self._aid = devinfo['aid']
self._iid = devinfo['iid']
self._features = 0
self._chars = {}
self.setup()

accessory.entities[(self._aid, self._iid)] = self
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated

@property
def should_poll(self) -> bool:
"""Return False.

Data update is triggered from HKDevice.
"""
return False

def setup(self):
"""Configure an entity baed on its HomeKit characterstics metadata."""
# pylint: disable=import-error
Expand All @@ -47,7 +56,7 @@ def setup(self):
get_uuid(c) for c in self.get_characteristic_types()
]

self._chars_to_poll = []
self.pollable_characteristics = []
self._chars = {}
self._char_names = {}

Expand Down Expand Up @@ -75,7 +84,7 @@ def _setup_characteristic(self, char):
from homekit.model.characteristics import CharacteristicsTypes

# Build up a list of (aid, iid) tuples to poll on update()
self._chars_to_poll.append((self._aid, char['iid']))
self.pollable_characteristics.append((self._aid, char['iid']))

# Build a map of ctype -> iid
short_name = CharacteristicsTypes.get_short(char['type'])
Expand All @@ -91,30 +100,9 @@ def _setup_characteristic(self, char):
# pylint: disable=not-callable
setup_fn(char)

async def async_update(self):
"""Obtain a HomeKit device's state."""
# pylint: disable=import-error
from homekit.exceptions import (
AccessoryDisconnectedError, AccessoryNotFoundError,
EncryptionError)

try:
new_values_dict = await self._accessory.get_characteristics(
self._chars_to_poll
)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.
self._available = False
return
except (AccessoryDisconnectedError, EncryptionError):
# Temporary connection failure. Device is still available but our
# connection was dropped.
return

self._available = True

for (_, iid), result in new_values_dict.items():
def handle_new_values(self, new_values):
"""Update state from one of more characteristics fetch from device."""
for (_, iid), result in new_values.items():
if 'value' not in result:
continue
# Callback to update the entity with this characteristic value
Expand All @@ -125,6 +113,8 @@ async def async_update(self):
# pylint: disable=not-callable
update_fn(result['value'])

self.async_schedule_update_ha_state()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this called from async? because you're calling it from a sync method?

If it's called from async and you don't need any work done in async_update, you could also use self.async_write_ha_state()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's non blocking code thats called from an async method elsewhere. Should i call it async_handle_new_values to make thise clear?

I will keep this in mind as I implement @MartinHjelmare suggestions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have switched to async_write_ha_state and its now called from an explicitly async method.


@property
def unique_id(self):
"""Return the ID of this device."""
Expand All @@ -139,7 +129,7 @@ def name(self):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
return self._accessory.available

@property
def device_info(self):
Expand Down
79 changes: 76 additions & 3 deletions homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""Helpers for managing a pairing with a HomeKit accessory or bridge."""
import asyncio
import datetime
import logging

from homeassistant.helpers.event import async_track_time_interval

from .const import HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP


DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60)
RETRY_INTERVAL = 60 # seconds

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -75,17 +79,32 @@ def __init__(self, hass, config_entry, pairing_data):

# This just tracks aid/iid pairs so we know if a HK service has been
# mapped to a HA entity.
self.entities = []
self.entities = {}

# There are multiple entities sharing a single connection - only
# allow one entity to use pairing at once.
self.pairing_lock = asyncio.Lock()

self.available = True

def async_set_unavailable(self):
"""Mark state of all entities on this connection as unavailable."""
self.available = False
for entity in self.entities.values():
entity.async_schedule_update_ha_state()
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated

async def async_setup(self):
"""Prepare to use a paired HomeKit device in homeassistant."""
cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id)
if not cache:
return await self.async_refresh_entity_map(self.config_num)
if await self.async_refresh_entity_map(self.config_num):
async_track_time_interval(
self.hass,
self.async_update,
DEFAULT_SCAN_INTERVAL
)
return True
return False

self.accessories = cache['accessories']
self.config_num = cache['config_num']
Expand All @@ -98,6 +117,14 @@ async def async_setup(self):

self.add_entities()

await self.async_update()

async_track_time_interval(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that you don't track any of the unsub functions for tracking time interval. It is needed to support removing the config entry (but I haven't looked if HomeKit supports that)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It currently doesn’t support it, but I’ll keep that in mind when I add support

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polling is now turned off when an config entry is unloaded.

self.hass,
self.async_update,
DEFAULT_SCAN_INTERVAL
)

return True

async def async_refresh_entity_map(self, config_num):
Expand Down Expand Up @@ -132,6 +159,8 @@ async def async_refresh_entity_map(self, config_num):
# Register and add new entities that are available
self.add_entities()

await self.async_update()

return True

def add_listener(self, add_entities_cb):
Expand Down Expand Up @@ -159,7 +188,6 @@ def _add_new_entities(self, callbacks):

for listener in callbacks:
if listener(aid, service):
self.entities.append((aid, iid))
break

def async_load_platforms(self):
Expand All @@ -184,6 +212,51 @@ def async_load_platforms(self):
)
self.platforms.add(platform)

async def async_update(self, now=None):
"""Poll state of all entities attached to this bridge/accessory."""
# pylint: disable=import-error
from homekit.exceptions import (
AccessoryDisconnectedError, AccessoryNotFoundError,
EncryptionError)

_LOGGER.debug("Starting HomeKit controller update")

chars_to_poll = {}
for entity in self.entities.values():
for char in entity.pollable_characteristics:
chars_to_poll[char] = entity

if not chars_to_poll:
return

try:
new_values_dict = await self.get_characteristics(
list(chars_to_poll.keys())
)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.
self.async_set_unavailable()
return
except (AccessoryDisconnectedError, EncryptionError):
# Temporary connection failure. Device is still available but our
# connection was dropped.
return

self.available = True

for entity in self.entities.values():
new_values = {}
for key, value in new_values_dict.items():
if key not in chars_to_poll:
_LOGGER.debug("Unsolicited characteristic value: %s", key)
continue
if chars_to_poll[key] == entity:
new_values[key] = value
entity.handle_new_values(new_values)

_LOGGER.debug("Finished HomeKit controller update")

async def get_characteristics(self, *args, **kwargs):
"""Read latest state from homekit accessory."""
async with self.pairing_lock:
Expand Down