mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Dont rely on config flow to monitor homekit_controller c# changes (#76861)
This commit is contained in:
parent
8b1713a691
commit
eb0828efdb
@ -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] = {}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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"],
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user