Dont rely on config flow to monitor homekit_controller c# changes (#76861)

This commit is contained in:
Jc2k 2022-08-20 21:58:59 +01:00 committed by GitHub
parent 8b1713a691
commit eb0828efdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 36 additions and 83 deletions

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType
from .config_flow import normalize_hkid from .config_flow import normalize_hkid
from .connection import HKDevice from .connection import HKDevice
from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS
from .storage import EntityMapStorage, async_get_entity_storage from .storage import EntityMapStorage
from .utils import async_get_controller from .utils import async_get_controller
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -50,8 +50,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up for Homekit devices.""" """Set up for Homekit devices."""
await async_get_entity_storage(hass)
await async_get_controller(hass) await async_get_controller(hass)
hass.data[KNOWN_DEVICES] = {} hass.data[KNOWN_DEVICES] = {}

View File

@ -24,7 +24,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .connection import HKDevice
from .const import DOMAIN, KNOWN_DEVICES from .const import DOMAIN, KNOWN_DEVICES
from .storage import async_get_entity_storage from .storage import async_get_entity_storage
from .utils import async_get_controller from .utils import async_get_controller
@ -253,17 +252,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
category = Categories(int(properties.get("ci", 0))) category = Categories(int(properties.get("ci", 0)))
paired = not status_flags & 0x01 paired = not status_flags & 0x01
# The configuration number increases every time the characteristic map
# needs updating. Some devices use a slightly off-spec name so handle
# both cases.
try:
config_num = int(properties["c#"])
except KeyError:
_LOGGER.warning(
"HomeKit device %s: c# not exposed, in violation of spec", hkid
)
config_num = None
# Set unique-id and error out if it's already configured # Set unique-id and error out if it's already configured
existing_entry = await self.async_set_unique_id( existing_entry = await self.async_set_unique_id(
normalized_hkid, raise_on_progress=False normalized_hkid, raise_on_progress=False
@ -280,12 +268,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
existing_entry, data={**existing_entry.data, **updated_ip_port} existing_entry, data={**existing_entry.data, **updated_ip_port}
) )
conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid]
if config_num and conn.config_num != config_num:
_LOGGER.debug(
"HomeKit info %s: c# incremented, refreshing entities", hkid
)
conn.async_notify_config_changed(config_num)
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
_LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid)

View File

@ -30,7 +30,6 @@ from .const import (
CHARACTERISTIC_PLATFORMS, CHARACTERISTIC_PLATFORMS,
CONTROLLER, CONTROLLER,
DOMAIN, DOMAIN,
ENTITY_MAP,
HOMEKIT_ACCESSORY_DISPATCH, HOMEKIT_ACCESSORY_DISPATCH,
IDENTIFIER_ACCESSORY_ID, IDENTIFIER_ACCESSORY_ID,
IDENTIFIER_LEGACY_ACCESSORY_ID, IDENTIFIER_LEGACY_ACCESSORY_ID,
@ -38,7 +37,6 @@ from .const import (
IDENTIFIER_SERIAL_NUMBER, IDENTIFIER_SERIAL_NUMBER,
) )
from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry
from .storage import EntityMapStorage
RETRY_INTERVAL = 60 # seconds RETRY_INTERVAL = 60 # seconds
MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3
@ -182,14 +180,10 @@ class HKDevice:
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Prepare to use a paired HomeKit device in Home Assistant.""" """Prepare to use a paired HomeKit device in Home Assistant."""
entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP]
pairing = self.pairing pairing = self.pairing
transport = pairing.transport transport = pairing.transport
entry = self.config_entry entry = self.config_entry
if cache := entity_storage.get_map(self.unique_id):
pairing.restore_accessories_state(cache["accessories"], cache["config_num"])
# We need to force an update here to make sure we have # We need to force an update here to make sure we have
# the latest values since the async_update we do in # the latest values since the async_update we do in
# async_process_entity_map will no values to poll yet # async_process_entity_map will no values to poll yet
@ -203,7 +197,7 @@ class HKDevice:
try: try:
await self.pairing.async_populate_accessories_state(force_update=True) await self.pairing.async_populate_accessories_state(force_update=True)
except AccessoryNotFoundError: except AccessoryNotFoundError:
if transport != Transport.BLE or not cache: if transport != Transport.BLE or not pairing.accessories:
# BLE devices may sleep and we can't force a connection # BLE devices may sleep and we can't force a connection
raise raise
@ -217,9 +211,6 @@ class HKDevice:
await self.async_process_entity_map() await self.async_process_entity_map()
if not cache:
# If its missing from the cache, make sure we save it
self.async_save_entity_map()
# If everything is up to date, we can create the entities # If everything is up to date, we can create the entities
# since we know the data is not stale. # since we know the data is not stale.
await self.async_add_new_entities() await self.async_add_new_entities()
@ -438,31 +429,18 @@ class HKDevice:
self.config_entry, self.platforms self.config_entry, self.platforms
) )
def async_notify_config_changed(self, config_num: int) -> None:
"""Notify the pairing of a config change."""
self.pairing.notify_config_changed(config_num)
def process_config_changed(self, config_num: int) -> None: def process_config_changed(self, config_num: int) -> None:
"""Handle a config change notification from the pairing.""" """Handle a config change notification from the pairing."""
self.hass.async_create_task(self.async_update_new_accessories_state()) self.hass.async_create_task(self.async_update_new_accessories_state())
async def async_update_new_accessories_state(self) -> None: async def async_update_new_accessories_state(self) -> None:
"""Process a change in the pairings accessories state.""" """Process a change in the pairings accessories state."""
self.async_save_entity_map()
await self.async_process_entity_map() await self.async_process_entity_map()
if self.watchable_characteristics: if self.watchable_characteristics:
await self.pairing.subscribe(self.watchable_characteristics) await self.pairing.subscribe(self.watchable_characteristics)
await self.async_update() await self.async_update()
await self.async_add_new_entities() await self.async_add_new_entities()
@callback
def async_save_entity_map(self) -> None:
"""Save the entity map."""
entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP]
entity_storage.async_create_or_update_map(
self.unique_id, self.config_num, self.entity_map.serialize()
)
def add_accessory_factory(self, add_entities_cb) -> None: def add_accessory_factory(self, add_entities_cb) -> None:
"""Add a callback to run when discovering new entities for accessories.""" """Add a callback to run when discovering new entities for accessories."""
self.accessory_factories.append(add_entities_cb) self.accessory_factories.append(add_entities_cb)

View File

@ -3,7 +3,7 @@
"name": "HomeKit Controller", "name": "HomeKit Controller",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==1.3.0"], "requirements": ["aiohomekit==1.4.0"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"], "dependencies": ["bluetooth", "zeroconf"],

View File

@ -8,6 +8,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant from homeassistant.core import Event, HomeAssistant
from .const import CONTROLLER from .const import CONTROLLER
from .storage import async_get_entity_storage
def folded_name(name: str) -> str: def folded_name(name: str) -> str:
@ -22,6 +23,8 @@ async def async_get_controller(hass: HomeAssistant) -> Controller:
async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) async_zeroconf_instance = await zeroconf.async_get_async_instance(hass)
char_cache = await async_get_entity_storage(hass)
# In theory another call to async_get_controller could have run while we were # In theory another call to async_get_controller could have run while we were
# trying to get the zeroconf instance. So we check again to make sure we # trying to get the zeroconf instance. So we check again to make sure we
# don't leak a Controller instance here. # don't leak a Controller instance here.
@ -33,6 +36,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller:
controller = Controller( controller = Controller(
async_zeroconf_instance=async_zeroconf_instance, async_zeroconf_instance=async_zeroconf_instance,
bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type]
char_cache=char_cache,
) )
hass.data[CONTROLLER] = controller hass.data[CONTROLLER] = controller

View File

@ -168,7 +168,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==1.3.0 aiohomekit==1.4.0
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http

View File

@ -152,7 +152,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==1.3.0 aiohomekit==1.4.0
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http

View File

@ -11,10 +11,9 @@ from unittest import mock
from aiohomekit.model import Accessories, AccessoriesState, Accessory from aiohomekit.model import Accessories, AccessoriesState, Accessory
from aiohomekit.testing import FakeController, FakePairing from aiohomekit.testing import FakeController, FakePairing
from aiohomekit.zeroconf import HomeKitService
from homeassistant.components import zeroconf
from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.homekit_controller import config_flow
from homeassistant.components.homekit_controller.const import ( from homeassistant.components.homekit_controller.const import (
CONTROLLER, CONTROLLER,
DOMAIN, DOMAIN,
@ -22,6 +21,7 @@ from homeassistant.components.homekit_controller.const import (
IDENTIFIER_ACCESSORY_ID, IDENTIFIER_ACCESSORY_ID,
IDENTIFIER_SERIAL_NUMBER, IDENTIFIER_SERIAL_NUMBER,
) )
from homeassistant.components.homekit_controller.utils import async_get_controller
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, State, callback from homeassistant.core import HomeAssistant, State, 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
@ -175,12 +175,11 @@ async def setup_platform(hass):
config = {"discovery": {}} config = {"discovery": {}}
with mock.patch( with mock.patch(
"homeassistant.components.homekit_controller.utils.Controller" "homeassistant.components.homekit_controller.utils.Controller", FakeController
) as controller: ):
fake_controller = controller.return_value = FakeController()
await async_setup_component(hass, DOMAIN, config) await async_setup_component(hass, DOMAIN, config)
return fake_controller return await async_get_controller(hass)
async def setup_test_accessories(hass, accessories): async def setup_test_accessories(hass, accessories):
@ -228,31 +227,24 @@ async def device_config_changed(hass, accessories):
pairing._accessories_state = AccessoriesState( pairing._accessories_state = AccessoriesState(
accessories_obj, pairing.config_num + 1 accessories_obj, pairing.config_num + 1
) )
pairing._async_description_update(
discovery_info = zeroconf.ZeroconfServiceInfo( HomeKitService(
host="127.0.0.1", name="TestDevice.local",
addresses=["127.0.0.1"], id="00:00:00:00:00:00",
hostname="mock_hostname", model="",
name="TestDevice._hap._tcp.local.", config_num=2,
port=8080, state_num=3,
properties={ feature_flags=0,
"md": "TestDevice", status_flags=0,
"id": "00:00:00:00:00:00", category=1,
"c#": "2", protocol_version="1.0",
"sf": "0", type="_hap._tcp.local.",
}, address="127.0.0.1",
type="mock_type", addresses=["127.0.0.1"],
port=8080,
)
) )
# Config Flow will abort and notify us if the discovery event is of
# interest - in this case c# has incremented
flow = config_flow.HomekitControllerFlowHandler()
flow.hass = hass
flow.context = {}
result = await flow.async_step_zeroconf(discovery_info)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
# Wait for services to reconfigure # Wait for services to reconfigure
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -6,7 +6,7 @@ https://github.com/home-assistant/core/issues/15336
from unittest import mock from unittest import mock
from aiohomekit import AccessoryDisconnectedError from aiohomekit import AccessoryNotFoundError
from aiohomekit.testing import FakePairing from aiohomekit.testing import FakePairing
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -184,9 +184,8 @@ async def test_ecobee3_setup_connection_failure(hass):
# Test that the connection fails during initial setup. # Test that the connection fails during initial setup.
# No entities should be created. # No entities should be created.
list_accessories = "list_accessories_and_characteristics" with mock.patch.object(FakePairing, "async_populate_accessories_state") as laac:
with mock.patch.object(FakePairing, list_accessories) as laac: laac.side_effect = AccessoryNotFoundError("Connection failed")
laac.side_effect = AccessoryDisconnectedError("Connection failed")
# If there is no cached entity map and the accessory connection is # If there is no cached entity map and the accessory connection is
# failing then we have to fail the config entry setup. # failing then we have to fail the config entry setup.

View File

@ -1,7 +1,7 @@
"""Tests for homekit_controller config flow.""" """Tests for homekit_controller config flow."""
import asyncio import asyncio
import unittest.mock import unittest.mock
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, patch
import aiohomekit import aiohomekit
from aiohomekit.exceptions import AuthenticationError from aiohomekit.exceptions import AuthenticationError
@ -524,7 +524,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller):
entry.add_to_hass(hass) entry.add_to_hass(hass)
connection_mock = AsyncMock() connection_mock = AsyncMock()
connection_mock.async_notify_config_changed = MagicMock()
hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock}
device = setup_mock_accessory(controller) device = setup_mock_accessory(controller)
@ -547,7 +546,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller):
assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryIP"] == discovery_info.host
assert entry.data["AccessoryPort"] == discovery_info.port assert entry.data["AccessoryPort"] == discovery_info.port
assert connection_mock.async_notify_config_changed.call_count == 1
@pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS)

View File

@ -119,6 +119,7 @@ async def test_offline_device_raises(hass, controller):
nonlocal is_connected nonlocal is_connected
if not is_connected: if not is_connected:
raise AccessoryNotFoundError("any") raise AccessoryNotFoundError("any")
await super().async_populate_accessories_state(*args, **kwargs)
async def get_characteristics(self, chars, *args, **kwargs): async def get_characteristics(self, chars, *args, **kwargs):
nonlocal is_connected nonlocal is_connected
@ -173,6 +174,7 @@ async def test_ble_device_only_checks_is_available(hass, controller):
nonlocal is_available nonlocal is_available
if not is_available: if not is_available:
raise AccessoryNotFoundError("any") raise AccessoryNotFoundError("any")
await super().async_populate_accessories_state(*args, **kwargs)
async def get_characteristics(self, chars, *args, **kwargs): async def get_characteristics(self, chars, *args, **kwargs):
nonlocal is_available nonlocal is_available