-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Only poll HomeKit connection once for all entities on a single bridge/pairing #25249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
598c33e
f9c3d76
8e3a222
eaea33d
d481be1
4c9f46e
86ed7f1
5984827
3a72c0f
8ee2fa6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
| @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 | ||
|
|
@@ -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 = {} | ||
|
|
||
|
|
@@ -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']) | ||
|
|
@@ -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 | ||
|
|
@@ -125,6 +113,8 @@ async def async_update(self): | |
| # pylint: disable=not-callable | ||
| update_fn(result['value']) | ||
|
|
||
| self.async_schedule_update_ha_state() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have switched to |
||
|
|
||
| @property | ||
| def unique_id(self): | ||
| """Return the ID of this device.""" | ||
|
|
@@ -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): | ||
|
|
||
| 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__) | ||
|
|
@@ -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() | ||
|
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'] | ||
|
|
@@ -98,6 +117,14 @@ async def async_setup(self): | |
|
|
||
| self.add_entities() | ||
|
|
||
| await self.async_update() | ||
|
|
||
| async_track_time_interval( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
|
@@ -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): | ||
|
|
@@ -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): | ||
|
|
@@ -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: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.