1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 04:50:05 +00:00

Only poll HomeKit connection once for all entities on a single bridge/pairing (#25249)

* Stub for polling from a central location

* Allow connection to know the entity objects attached to it

* Move polling logic to connection

* Don't poll if no characteristics selected

* Loosen coupling between entity and HKDevice

* Disable track_time_interval when removing entry

* Revert self.entities changes

* Use @callback for async_state_changed

* Split out unload and remove and add a test

* Test that entity is gone and fix docstring
This commit is contained in:
Jc2k
2019-07-22 17:22:44 +01:00
committed by Paulus Schoutsen
parent 58f946e452
commit 8c69fd91ff
8 changed files with 266 additions and 62 deletions

View File

@@ -1,6 +1,7 @@
"""Support for Homekit device discovery."""
import logging
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
@@ -27,7 +28,6 @@ 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']
@@ -35,6 +35,39 @@ class HomeKitEntity(Entity):
self._chars = {}
self.setup()
self._signals = []
async def async_added_to_hass(self):
"""Entity added to hass."""
self._signals.append(
self.hass.helpers.dispatcher.async_dispatcher_connect(
self._accessory.signal_state_updated,
self.async_state_changed,
)
)
self._accessory.add_pollable_characteristics(
self.pollable_characteristics,
)
async def async_will_remove_from_hass(self):
"""Prepare to be removed from hass."""
self._accessory.remove_pollable_characteristics(
self._aid,
)
for signal_remove in self._signals:
signal_remove()
self._signals.clear()
@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 +80,7 @@ class HomeKitEntity(Entity):
get_uuid(c) for c in self.get_characteristic_types()
]
self._chars_to_poll = []
self.pollable_characteristics = []
self._chars = {}
self._char_names = {}
@@ -75,7 +108,7 @@ class HomeKitEntity(Entity):
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 +124,11 @@ class HomeKitEntity(Entity):
# 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():
@callback
def async_state_changed(self):
"""Collect new data from bridge and update the entity state in hass."""
accessory_state = self._accessory.current_state.get(self._aid, {})
for iid, result in accessory_state.items():
if 'value' not in result:
continue
# Callback to update the entity with this characteristic value
@@ -125,6 +139,8 @@ class HomeKitEntity(Entity):
# pylint: disable=not-callable
update_fn(result['value'])
self.async_write_ha_state()
@property
def unique_id(self):
"""Return the ID of this device."""
@@ -139,7 +155,7 @@ class HomeKitEntity(Entity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
return self._accessory.available
@property
def device_info(self):
@@ -211,6 +227,17 @@ async def async_setup(hass, config):
return True
async def async_unload_entry(hass, entry):
"""Disconnect from HomeKit devices before unloading entry."""
hkid = entry.data['AccessoryPairingID']
if hkid in hass.data[KNOWN_DEVICES]:
connection = hass.data[KNOWN_DEVICES][hkid]
await connection.async_unload()
return True
async def async_remove_entry(hass, entry):
"""Cleanup caches before removing config entry."""
hkid = entry.data['AccessoryPairingID']

View File

@@ -1,10 +1,14 @@
"""Helpers for managing a pairing with a HomeKit accessory or bridge."""
import asyncio
import datetime
import logging
from .const import HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP
from homeassistant.helpers.event import async_track_time_interval
from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP
DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60)
RETRY_INTERVAL = 60 # seconds
_LOGGER = logging.getLogger(__name__)
@@ -81,11 +85,54 @@ class HKDevice():
# allow one entity to use pairing at once.
self.pairing_lock = asyncio.Lock()
self.available = True
self.signal_state_updated = '_'.join((
DOMAIN,
self.unique_id,
'state_updated',
))
# Current values of all characteristics homekit_controller is tracking.
# Key is a (accessory_id, characteristic_id) tuple.
self.current_state = {}
self.pollable_characteristics = []
# If this is set polling is active and can be disabled by calling
# this method.
self._polling_interval_remover = None
def add_pollable_characteristics(self, characteristics):
"""Add (aid, iid) pairs that we need to poll."""
self.pollable_characteristics.extend(characteristics)
def remove_pollable_characteristics(self, accessory_id):
"""Remove all pollable characteristics by accessory id."""
self.pollable_characteristics = [
char for char in self.pollable_characteristics
if char[0] != accessory_id
]
def async_set_unavailable(self):
"""Mark state of all entities on this connection as unavailable."""
self.available = False
self.hass.helpers.dispatcher.async_dispatcher_send(
self.signal_state_updated,
)
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):
self._polling_interval_remover = 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,8 +145,34 @@ class HKDevice():
self.add_entities()
await self.async_update()
self._polling_interval_remover = async_track_time_interval(
self.hass,
self.async_update,
DEFAULT_SCAN_INTERVAL
)
return True
async def async_unload(self):
"""Stop interacting with device and prepare for removal from hass."""
if self._polling_interval_remover:
self._polling_interval_remover()
unloads = []
for platform in self.platforms:
unloads.append(
self.hass.config_entries.async_forward_entry_unload(
self.config_entry,
platform
)
)
results = await asyncio.gather(*unloads)
return False not in results
async def async_refresh_entity_map(self, config_num):
"""Handle setup of a HomeKit accessory."""
# pylint: disable=import-error
@@ -132,6 +205,8 @@ class HKDevice():
# Register and add new entities that are available
self.add_entities()
await self.async_update()
return True
def add_listener(self, add_entities_cb):
@@ -184,6 +259,47 @@ class HKDevice():
)
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)
if not self.pollable_characteristics:
_LOGGER.debug(
"HomeKit connection not polling any characteristics."
)
return
_LOGGER.debug("Starting HomeKit controller update")
try:
new_values_dict = await self.get_characteristics(
self.pollable_characteristics,
)
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 (aid, cid), value in new_values_dict.items():
accessory = self.current_state.setdefault(aid, {})
accessory[cid] = value
self.hass.helpers.dispatcher.async_dispatcher_send(
self.signal_state_updated,
)
_LOGGER.debug("Finished HomeKit controller update")
async def get_characteristics(self, *args, **kwargs):
"""Read latest state from homekit accessory."""
async with self.pairing_lock: