Await startup in homekit controller (#75021)

This commit is contained in:
J. Nick Koston 2022-07-14 14:44:27 +02:00 committed by GitHub
parent a3c1926da5
commit a31dde9cb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 30 deletions

View file

@ -227,6 +227,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not await conn.async_setup():
del hass.data[KNOWN_DEVICES][conn.unique_id]
if (connection := getattr(conn.pairing, "connection")) and hasattr(
connection, "host"
):
raise ConfigEntryNotReady(
f"Cannot connect to {connection.host}:{connection.port}"
)
raise ConfigEntryNotReady
return True

View file

@ -25,7 +25,7 @@ class HomeKitCamera(AccessoryEntity, Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a jpeg with the current camera snapshot."""
return await self._accessory.pairing.image(
return await self._accessory.pairing.image( # type: ignore[attr-defined]
self._aid,
width or 640,
height or 480,

View file

@ -17,6 +17,7 @@ from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers import device_registry as dr
from .connection import HKDevice
from .const import DOMAIN, KNOWN_DEVICES
from .utils import async_get_controller
@ -240,17 +241,19 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry(
existing_entry, data={**existing_entry.data, **updated_ip_port}
)
conn = self.hass.data[KNOWN_DEVICES][hkid]
conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid]
# When we rediscover the device, let aiohomekit know
# that the device is available and we should not wait
# to retry connecting any longer. reconnect_soon
# will do nothing if the device is already connected
await conn.pairing.reconnect_soon()
if conn.config_num != config_num:
if config_num and conn.config_num != config_num:
_LOGGER.debug(
"HomeKit info %s: c# incremented, refreshing entities", hkid
)
self.hass.async_create_task(conn.async_refresh_entity_map(config_num))
self.hass.async_create_task(
conn.async_refresh_entity_map_and_entities(config_num)
)
return self.async_abort(reason="already_configured")
_LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid)

View file

@ -7,6 +7,7 @@ import datetime
import logging
from typing import Any
from aiohomekit import Controller
from aiohomekit.exceptions import (
AccessoryDisconnectedError,
AccessoryNotFoundError,
@ -35,6 +36,7 @@ from .const import (
IDENTIFIER_SERIAL_NUMBER,
)
from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry
from .storage import EntityMapStorage
DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60)
RETRY_INTERVAL = 60 # seconds
@ -70,11 +72,13 @@ class HKDevice:
# don't want to mutate a dict owned by a config entry.
self.pairing_data = pairing_data.copy()
self.pairing = hass.data[CONTROLLER].load_pairing(
connection: Controller = hass.data[CONTROLLER]
self.pairing = connection.load_pairing(
self.pairing_data["AccessoryPairingID"], self.pairing_data
)
self.accessories = None
self.accessories: list[Any] | None = None
self.config_num = 0
self.entity_map = Accessories()
@ -167,26 +171,23 @@ class HKDevice:
async def async_setup(self) -> bool:
"""Prepare to use a paired HomeKit device in Home Assistant."""
cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id)
if not cache:
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
entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP]
if cache := entity_storage.get_map(self.unique_id):
self.accessories = cache["accessories"]
self.config_num = cache["config_num"]
self.entity_map = Accessories.from_list(self.accessories)
elif not await self.async_refresh_entity_map(self.config_num):
return False
await self.async_process_entity_map()
if not self.pairing.is_connected:
return False
# If everything is up to date, we can create the entities
# since we know the data is not stale.
self.add_entities()
self._polling_interval_remover = async_track_time_interval(
self.hass, self.async_update, DEFAULT_SCAN_INTERVAL
)
self.hass.async_create_task(self.async_process_entity_map())
return True
def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo:
@ -361,7 +362,7 @@ class HKDevice:
# is especially important for BLE, as the Pairing instance relies on the entity map
# to map aid/iid to GATT characteristics. So push it to there as well.
self.pairing.pairing_data["accessories"] = self.accessories
self.pairing.pairing_data["accessories"] = self.accessories # type: ignore[attr-defined]
self.async_detect_workarounds()
@ -375,8 +376,6 @@ class HKDevice:
# Load any triggers for this config entry
await async_setup_triggers_for_entry(self.hass, self.config_entry)
self.add_entities()
if self.watchable_characteristics:
await self.pairing.subscribe(self.watchable_characteristics)
if not self.pairing.is_connected:
@ -395,6 +394,12 @@ class HKDevice:
self.config_entry, self.platforms
)
async def async_refresh_entity_map_and_entities(self, config_num: int) -> None:
"""Refresh the entity map and entities for this pairing."""
await self.async_refresh_entity_map(config_num)
await self.async_process_entity_map()
self.add_entities()
async def async_refresh_entity_map(self, config_num: int) -> bool:
"""Handle setup of a HomeKit accessory."""
try:
@ -404,15 +409,17 @@ class HKDevice:
# later when Bonjour spots c# is still not up to date.
return False
assert self.accessories is not None
self.entity_map = Accessories.from_list(self.accessories)
self.hass.data[ENTITY_MAP].async_create_or_update_map(
entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP]
entity_storage.async_create_or_update_map(
self.unique_id, config_num, self.accessories
)
self.config_num = config_num
self.hass.async_create_task(self.async_process_entity_map())
return True
def add_accessory_factory(self, add_entities_cb) -> None:

View file

@ -186,6 +186,13 @@ async def setup_platform(hass):
async def setup_test_accessories(hass, accessories):
"""Load a fake homekit device based on captured JSON profile."""
fake_controller = await setup_platform(hass)
return await setup_test_accessories_with_controller(
hass, accessories, fake_controller
)
async def setup_test_accessories_with_controller(hass, accessories, fake_controller):
"""Load a fake homekit device based on captured JSON profile."""
pairing_id = "00:00:00:00:00:00"

View file

@ -515,7 +515,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller):
assert entry.data["AccessoryIP"] == discovery_info.host
assert entry.data["AccessoryPort"] == discovery_info.port
assert connection_mock.async_refresh_entity_map.await_count == 1
assert connection_mock.async_refresh_entity_map_and_entities.await_count == 1
@pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS)

View file

@ -1,19 +1,26 @@
"""Tests for homekit_controller init."""
from datetime import timedelta
from unittest.mock import patch
from aiohomekit import exceptions
from aiohomekit.model import Accessory
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.testing import FakeController, FakeDiscovery, FakePairing
from homeassistant.components.homekit_controller.const import ENTITY_MAP
from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from .common import Helper, remove_device
from .common import Helper, remove_device, setup_test_accessories_with_controller
from tests.common import async_fire_time_changed
from tests.components.homekit_controller.common import setup_test_component
ALIVE_DEVICE_NAME = "testdevice"
@ -89,3 +96,76 @@ async def test_device_remove_devices(hass, hass_ws_client):
await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id)
is True
)
async def test_offline_device_raises(hass):
"""Test an offline device raises ConfigEntryNotReady."""
is_connected = False
class OfflineFakePairing(FakePairing):
"""Fake pairing that always returns False for is_connected."""
@property
def is_connected(self):
nonlocal is_connected
return is_connected
class OfflineFakeDiscovery(FakeDiscovery):
"""Fake discovery that returns an offline pairing."""
async def start_pairing(self, alias: str):
if self.description.id in self.controller.pairings:
raise exceptions.AlreadyPairedError(
f"{self.description.id} already paired"
)
async def finish_pairing(pairing_code):
if pairing_code != self.pairing_code:
raise exceptions.AuthenticationError("M4")
pairing_data = {}
pairing_data["AccessoryIP"] = self.info["address"]
pairing_data["AccessoryPort"] = self.info["port"]
pairing_data["Connection"] = "IP"
obj = self.controller.pairings[alias] = OfflineFakePairing(
self.controller, pairing_data, self.accessories
)
return obj
return finish_pairing
class OfflineFakeController(FakeController):
"""Fake controller that always returns a discovery with a pairing that always returns False for is_connected."""
def add_device(self, accessories):
device_id = "00:00:00:00:00:00"
discovery = self.discoveries[device_id] = OfflineFakeDiscovery(
self,
device_id,
accessories=accessories,
)
return discovery
with patch(
"homeassistant.components.homekit_controller.utils.Controller"
) as controller:
fake_controller = controller.return_value = OfflineFakeController()
await async_setup_component(hass, DOMAIN, {})
accessory = Accessory.create_with_info(
"TestDevice", "example.com", "Test", "0001", "0.1"
)
create_alive_service(accessory)
config_entry, _ = await setup_test_accessories_with_controller(
hass, [accessory], fake_controller
)
assert config_entry.state == ConfigEntryState.SETUP_RETRY
is_connected = True
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.LOADED