Await startup in homekit controller (#75021)
This commit is contained in:
parent
a3c1926da5
commit
a31dde9cb4
7 changed files with 133 additions and 30 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue