diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c203d674710..36c7bac9d0a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -193,14 +193,21 @@ def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]: ] -def _async_get_entries_by_name( +def _async_get_imported_entries_indices( current_entries: list[ConfigEntry], -) -> dict[str, ConfigEntry]: - """Return a dict of the entries by name.""" +) -> tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]]: + """Return a dicts of the entries by name and port.""" # For backwards compat, its possible the first bridge is using the default # name. - return {entry.data.get(CONF_NAME, BRIDGE_NAME): entry for entry in current_entries} + entries_by_name: dict[str, ConfigEntry] = {} + entries_by_port: dict[int, ConfigEntry] = {} + for entry in current_entries: + if entry.source != SOURCE_IMPORT: + continue + entries_by_name[entry.data.get(CONF_NAME, BRIDGE_NAME)] = entry + entries_by_port[entry.data.get(CONF_PORT, DEFAULT_PORT)] = entry + return entries_by_name, entries_by_port async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -218,10 +225,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for index, conf in enumerate(config[DOMAIN]): - if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): + if _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ): continue conf[CONF_ENTRY_INDEX] = index @@ -237,8 +248,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_update_config_entry_if_from_yaml( - hass: HomeAssistant, entries_by_name: dict[str, ConfigEntry], conf: ConfigType +def _async_update_config_entry_from_yaml( + hass: HomeAssistant, + entries_by_name: dict[str, ConfigEntry], + entries_by_port: dict[int, ConfigEntry], + conf: ConfigType, ) -> bool: """Update a config entry with the latest yaml. @@ -246,27 +260,24 @@ def _async_update_config_entry_if_from_yaml( Returns False if there is no matching config entry """ - bridge_name = conf[CONF_NAME] - - if ( - bridge_name in entries_by_name - and entries_by_name[bridge_name].source == SOURCE_IMPORT + if not ( + matching_entry := entries_by_name.get(conf.get(CONF_NAME, BRIDGE_NAME)) + or entries_by_port.get(conf.get(CONF_PORT, DEFAULT_PORT)) ): - entry = entries_by_name[bridge_name] - # If they alter the yaml config we import the changes - # since there currently is no practical way to support - # all the options in the UI at this time. - data = conf.copy() - options = {} - for key in CONFIG_OPTIONS: - if key in data: - options[key] = data[key] - del data[key] + return False - hass.config_entries.async_update_entry(entry, data=data, options=options) - return True + # If they alter the yaml config we import the changes + # since there currently is no practical way to support + # all the options in the UI at this time. + data = conf.copy() + options = {} + for key in CONFIG_OPTIONS: + if key in data: + options[key] = data[key] + del data[key] - return False + hass.config_entries.async_update_entry(matching_entry, data=data, options=options) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -451,10 +462,14 @@ def _async_register_events_and_services(hass: HomeAssistant) -> None: return current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for conf in config[DOMAIN]: - _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf) + _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ) reload_tasks = [ hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index be514ce2b6a..dbb63ba690a 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -35,7 +35,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_ZEROCONF from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_DEVICE_ID, @@ -1394,6 +1394,82 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_async_zeroco mock_homekit().async_start.assert_called() +async def test_yaml_can_link_with_default_name(hass, mock_async_zeroconf): + """Test async_setup with imported config linked by default name.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, + "homekit", + {"homekit": {"entity_config": {"camera.back_camera": {"stream_count": 3}}}}, + ) + await hass.async_block_till_done() + + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert entry.options["entity_config"]["camera.back_camera"]["stream_count"] == 3 + + +async def test_yaml_can_link_with_port(hass, mock_async_zeroconf): + """Test async_setup with imported config linked by port.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={"name": "random", "port": 12345}, + options={}, + ) + entry.add_to_hass(hass) + entry2 = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={"name": "random", "port": 12346}, + options={}, + ) + entry2.add_to_hass(hass) + entry3 = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_ZEROCONF, + data={"name": "random", "port": 12347}, + options={}, + ) + entry3.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, + "homekit", + { + "homekit": { + "port": 12345, + "entity_config": {"camera.back_camera": {"stream_count": 3}}, + } + }, + ) + await hass.async_block_till_done() + + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert entry.options["entity_config"]["camera.back_camera"]["stream_count"] == 3 + assert entry2.options == {} + assert entry3.options == {} + + async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_async_zeroconf): """Test HomeKit uses system zeroconf.""" entry = MockConfigEntry(