Fix hkid matching in homekit_controller when zeroconf value is not upper case (#100641)

This commit is contained in:
J. Nick Koston 2023-09-20 17:37:13 +02:00 committed by GitHub
parent 77001b26de
commit ec5675ff4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 100 additions and 23 deletions

View file

@ -80,12 +80,12 @@ def formatted_category(category: Categories) -> str:
@callback @callback
def find_existing_host( def find_existing_config_entry(
hass: HomeAssistant, serial: str hass: HomeAssistant, upper_case_hkid: str
) -> config_entries.ConfigEntry | None: ) -> config_entries.ConfigEntry | None:
"""Return a set of the configured hosts.""" """Return a set of the configured hosts."""
for entry in hass.config_entries.async_entries(DOMAIN): for entry in hass.config_entries.async_entries(DOMAIN):
if entry.data.get("AccessoryPairingID") == serial: if entry.data.get("AccessoryPairingID") == upper_case_hkid:
return entry return entry
return None return None
@ -114,7 +114,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the homekit_controller flow.""" """Initialize the homekit_controller flow."""
self.model: str | None = None self.model: str | None = None
self.hkid: str | None = None self.hkid: str | None = None # This is always lower case
self.name: str | None = None self.name: str | None = None
self.category: Categories | None = None self.category: Categories | None = None
self.devices: dict[str, AbstractDiscovery] = {} self.devices: dict[str, AbstractDiscovery] = {}
@ -199,11 +199,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self._async_step_pair_show_form() return self._async_step_pair_show_form()
async def _hkid_is_homekit(self, hkid: str) -> bool: @callback
def _hkid_is_homekit(self, hkid: str) -> bool:
"""Determine if the device is a homekit bridge or accessory.""" """Determine if the device is a homekit bridge or accessory."""
dev_reg = dr.async_get(self.hass) dev_reg = dr.async_get(self.hass)
device = dev_reg.async_get_device( device = dev_reg.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, hkid)} connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(hkid))}
) )
if device is None: if device is None:
@ -244,17 +245,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# The hkid is a unique random number that looks like a pairing code. # The hkid is a unique random number that looks like a pairing code.
# It changes if a device is factory reset. # It changes if a device is factory reset.
hkid = properties[zeroconf.ATTR_PROPERTIES_ID] hkid: str = properties[zeroconf.ATTR_PROPERTIES_ID]
normalized_hkid = normalize_hkid(hkid) normalized_hkid = normalize_hkid(hkid)
upper_case_hkid = hkid.upper()
# If this aiohomekit doesn't support this particular device, ignore it.
if not domain_supported(discovery_info.name):
return self.async_abort(reason="ignored_model")
model = properties["md"]
name = domain_to_name(discovery_info.name)
status_flags = int(properties["sf"]) status_flags = int(properties["sf"])
category = Categories(int(properties.get("ci", 0)))
paired = not status_flags & 0x01 paired = not status_flags & 0x01
# Set unique-id and error out if it's already configured # Set unique-id and error out if it's already configured
@ -265,23 +259,29 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"AccessoryIP": discovery_info.host, "AccessoryIP": discovery_info.host,
"AccessoryPort": discovery_info.port, "AccessoryPort": discovery_info.port,
} }
# If the device is already paired and known to us we should monitor c# # If the device is already paired and known to us we should monitor c#
# (config_num) for changes. If it changes, we check for new entities # (config_num) for changes. If it changes, we check for new entities
if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): if paired and upper_case_hkid in self.hass.data.get(KNOWN_DEVICES, {}):
if existing_entry: if existing_entry:
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}
) )
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
_LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) # If this aiohomekit doesn't support this particular device, ignore it.
if not domain_supported(discovery_info.name):
return self.async_abort(reason="ignored_model")
model = properties["md"]
name = domain_to_name(discovery_info.name)
_LOGGER.debug("Discovered device %s (%s - %s)", name, model, upper_case_hkid)
# Device isn't paired with us or anyone else. # Device isn't paired with us or anyone else.
# But we have a 'complete' config entry for it - that is probably # But we have a 'complete' config entry for it - that is probably
# invalid. Remove it automatically. # invalid. Remove it automatically.
existing = find_existing_host(self.hass, hkid) if not paired and (
if not paired and existing: existing := find_existing_config_entry(self.hass, upper_case_hkid)
):
if self.controller is None: if self.controller is None:
await self._async_setup_controller() await self._async_setup_controller()
@ -348,13 +348,13 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# If this is a HomeKit bridge/accessory exported # If this is a HomeKit bridge/accessory exported
# by *this* HA instance ignore it. # by *this* HA instance ignore it.
if await self._hkid_is_homekit(hkid): if self._hkid_is_homekit(hkid):
return self.async_abort(reason="ignored_model") return self.async_abort(reason="ignored_model")
self.name = name self.name = name
self.model = model self.model = model
self.category = category self.category = Categories(int(properties.get("ci", 0)))
self.hkid = hkid self.hkid = normalized_hkid
# We want to show the pairing form - but don't call async_step_pair # We want to show the pairing form - but don't call async_step_pair
# directly as it has side effects (will ask the device to show a # directly as it has side effects (will ask the device to show a

View file

@ -1180,3 +1180,80 @@ async def test_bluetooth_valid_device_discovery_unpaired(
assert result3["data"] == {} assert result3["data"] == {}
assert storage.get_map("00:00:00:00:00:00") is not None assert storage.get_map("00:00:00:00:00:00") is not None
async def test_discovery_updates_ip_when_config_entry_set_up(
hass: HomeAssistant, controller
) -> None:
"""Already configured updates ip when config entry set up."""
entry = MockConfigEntry(
domain="homekit_controller",
data={
"AccessoryIP": "4.4.4.4",
"AccessoryPort": 66,
"AccessoryPairingID": "AA:BB:CC:DD:EE:FF",
},
unique_id="aa:bb:cc:dd:ee:ff",
)
entry.add_to_hass(hass)
connection_mock = AsyncMock()
hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock}
device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
# Set device as already paired
discovery_info.properties["sf"] = 0x00
discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] = "Aa:bB:cC:dD:eE:fF"
# Device is discovered
result = await hass.config_entries.flow.async_init(
"homekit_controller",
context={"source": config_entries.SOURCE_ZEROCONF},
data=discovery_info,
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
await hass.async_block_till_done()
assert entry.data["AccessoryIP"] == discovery_info.host
assert entry.data["AccessoryPort"] == discovery_info.port
async def test_discovery_updates_ip_config_entry_not_set_up(
hass: HomeAssistant, controller
) -> None:
"""Already configured updates ip when the config entry is not set up."""
entry = MockConfigEntry(
domain="homekit_controller",
data={
"AccessoryIP": "4.4.4.4",
"AccessoryPort": 66,
"AccessoryPairingID": "AA:BB:CC:DD:EE:FF",
},
unique_id="aa:bb:cc:dd:ee:ff",
)
entry.add_to_hass(hass)
AsyncMock()
device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
# Set device as already paired
discovery_info.properties["sf"] = 0x00
discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] = "Aa:bB:cC:dD:eE:fF"
# Device is discovered
result = await hass.config_entries.flow.async_init(
"homekit_controller",
context={"source": config_entries.SOURCE_ZEROCONF},
data=discovery_info,
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
await hass.async_block_till_done()
assert entry.data["AccessoryIP"] == discovery_info.host
assert entry.data["AccessoryPort"] == discovery_info.port