Poll HomeKit Controller locks for state after lock operation (#82058)

This commit is contained in:
J. Nick Koston 2022-11-14 10:54:28 -06:00 committed by GitHub
parent bbda122c99
commit 1ded3ac51e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 39 additions and 2 deletions

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from datetime import timedelta from datetime import datetime, timedelta
import logging import logging
from types import MappingProxyType from types import MappingProxyType
from typing import Any from typing import Any
@ -22,6 +22,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STARTED from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
@ -29,6 +30,7 @@ from homeassistant.helpers.event import async_track_time_interval
from .const import ( from .const import (
CHARACTERISTIC_PLATFORMS, CHARACTERISTIC_PLATFORMS,
CONTROLLER, CONTROLLER,
DEBOUNCE_COOLDOWN,
DOMAIN, DOMAIN,
HOMEKIT_ACCESSORY_DISPATCH, HOMEKIT_ACCESSORY_DISPATCH,
IDENTIFIER_ACCESSORY_ID, IDENTIFIER_ACCESSORY_ID,
@ -41,6 +43,8 @@ from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry
RETRY_INTERVAL = 60 # seconds RETRY_INTERVAL = 60 # seconds
MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3
BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -127,6 +131,14 @@ class HKDevice:
self.watchable_characteristics: list[tuple[int, int]] = [] self.watchable_characteristics: list[tuple[int, int]] = []
self._debounced_update = Debouncer(
hass,
_LOGGER,
cooldown=DEBOUNCE_COOLDOWN,
immediate=False,
function=self.async_update,
)
@property @property
def entity_map(self) -> Accessories: def entity_map(self) -> Accessories:
"""Return the accessories from the pairing.""" """Return the accessories from the pairing."""
@ -240,8 +252,11 @@ class HKDevice:
self.async_set_available_state(self.pairing.is_available) self.async_set_available_state(self.pairing.is_available)
# We use async_request_update to avoid multiple updates
# at the same time which would generate a spurious warning
# in the log about concurrent polling.
self._polling_interval_remover = async_track_time_interval( self._polling_interval_remover = async_track_time_interval(
self.hass, self.async_update, self.pairing.poll_interval self.hass, self.async_request_update, self.pairing.poll_interval
) )
if transport == Transport.BLE: if transport == Transport.BLE:
@ -631,6 +646,10 @@ class HKDevice:
"""Update the available state of the device.""" """Update the available state of the device."""
self.async_set_available_state(self.pairing.is_available) self.async_set_available_state(self.pairing.is_available)
async def async_request_update(self, now: datetime | None = None) -> None:
"""Request an debounced update from the accessory."""
await self._debounced_update.async_call()
async def async_update(self, now=None): async def async_update(self, now=None):
"""Poll state of all entities attached to this bridge/accessory.""" """Poll state of all entities attached to this bridge/accessory."""
if not self.pollable_characteristics: if not self.pollable_characteristics:

View File

@ -107,3 +107,10 @@ STARTUP_EXCEPTIONS = (
EncryptionError, EncryptionError,
AccessoryDisconnectedError, AccessoryDisconnectedError,
) )
# 10 seconds was chosen because its soon enough
# for most state changes to happen but not too
# long that the BLE connection is dropped. It
# also happens to be the same value used by
# the update coordinator.
DEBOUNCE_COOLDOWN = 10 # seconds

View File

@ -175,6 +175,10 @@ class HomeKitEntity(Entity):
"""Define the homekit characteristics the entity cares about.""" """Define the homekit characteristics the entity cares about."""
raise NotImplementedError raise NotImplementedError
async def async_update(self) -> None:
"""Update the entity."""
await self._accessory.async_request_update()
class AccessoryEntity(HomeKitEntity): class AccessoryEntity(HomeKitEntity):
"""A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic."""

View File

@ -124,6 +124,10 @@ class HomeKitLock(HomeKitEntity, LockEntity):
await self.async_put_characteristics( await self.async_put_characteristics(
{CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: TARGET_STATE_MAP[state]} {CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: TARGET_STATE_MAP[state]}
) )
# Some locks need to be polled to update the current state
# after a target state change.
# https://github.com/home-assistant/core/issues/81887
await self._accessory.async_request_update()
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:

View File

@ -21,6 +21,7 @@ from aiohomekit.zeroconf import HomeKitService
from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.homekit_controller.const import ( from homeassistant.components.homekit_controller.const import (
CONTROLLER, CONTROLLER,
DEBOUNCE_COOLDOWN,
DOMAIN, DOMAIN,
HOMEKIT_ACCESSORY_DISPATCH, HOMEKIT_ACCESSORY_DISPATCH,
IDENTIFIER_ACCESSORY_ID, IDENTIFIER_ACCESSORY_ID,
@ -146,6 +147,7 @@ class Helper:
# If they are enabled, then HA will pick up the changes next time # If they are enabled, then HA will pick up the changes next time
# we yield control # we yield control
await time_changed(self.hass, 60) await time_changed(self.hass, 60)
await time_changed(self.hass, DEBOUNCE_COOLDOWN)
await self.hass.async_block_till_done() await self.hass.async_block_till_done()
@ -165,6 +167,7 @@ class Helper:
async def poll_and_get_state(self) -> State: async def poll_and_get_state(self) -> State:
"""Trigger a time based poll and return the current entity state.""" """Trigger a time based poll and return the current entity state."""
await time_changed(self.hass, 60) await time_changed(self.hass, 60)
await time_changed(self.hass, DEBOUNCE_COOLDOWN)
state = self.hass.states.get(self.entity_id) state = self.hass.states.get(self.entity_id)
assert state is not None assert state is not None