diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4cd0940adb7..e2847a597eb 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -667,8 +667,8 @@ class HomeKit: if ent_reg_ent := ent_reg.async_get(entity_id): if ( ent_reg_ent.entity_category is not None - and not self._filter.explicitly_included(entity_id) - ): + or ent_reg_ent.hidden_by is not None + ) and not self._filter.explicitly_included(entity_id): continue await self._async_set_device_info_attributes( diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 0863b8f583a..d0c0f73ce07 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -538,16 +538,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) ent_reg = entity_registry.async_get(self.hass) - entity_cat_entities = set() + excluded_entities = set() for entity_id in all_supported_entities: if ent_reg_ent := ent_reg.async_get(entity_id): - if ent_reg_ent.entity_category is not None: - entity_cat_entities.add(entity_id) + if ( + ent_reg_ent.entity_category is not None + or ent_reg_ent.hidden_by is not None + ): + excluded_entities.add(entity_id) # Remove entity category entities since we will exclude them anyways all_supported_entities = { k: v for k, v in all_supported_entities.items() - if k not in entity_cat_entities + if k not in excluded_entities } # Strip out entities that no longer exist to prevent error in the UI default_value = [ diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index b4600c3190e..301040e4f88 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.entity_registry import RegistryEntry, RegistryEntryHider from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component @@ -1403,3 +1403,81 @@ async def test_options_flow_exclude_mode_skips_category_entities( "include_entities": [], }, } + + +@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +async def test_options_flow_exclude_mode_skips_hidden_entities( + port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg +): + """Ensure exclude mode does not offer hidden entities.""" + config_entry = _mock_config_entry_with_options_populated() + await async_init_entry(hass, config_entry) + + hass.states.async_set("media_player.tv", "off") + hass.states.async_set("media_player.sonos", "off") + hass.states.async_set("switch.other", "off") + + sonos_hidden_switch: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "sonos", + "config", + device_id="1234", + hidden_by=RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(sonos_hidden_switch.entity_id, "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "mode": "bridge", + "include_exclude_mode": "exclude", + } + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "domains": ["media_player", "switch"], + "mode": "bridge", + "include_exclude_mode": "exclude", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "exclude" + assert _get_schema_default(result2["data_schema"].schema, "entities") == [] + + # sonos_hidden_switch.entity_id is a hidden entity + # so it should not be selectable since it will always be excluded + with pytest.raises(voluptuous.error.MultipleInvalid): + await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": [sonos_hidden_switch.entity_id]}, + ) + + result4 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": ["media_player.tv", "switch.other"]}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": ["media_player.tv", "switch.other"], + "include_domains": ["media_player", "switch"], + "include_entities": [], + }, + } diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 5c79e764af1..8a8c32d3272 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -49,7 +49,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistantError, State -from homeassistant.helpers import device_registry +from homeassistant.helpers import device_registry, entity_registry as er from homeassistant.helpers.entityfilter import ( CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, @@ -485,6 +485,61 @@ async def test_homekit_entity_glob_filter_with_config_entities( assert hass.states.get("select.keep") in filtered_states +async def test_homekit_entity_glob_filter_with_hidden_entities( + hass, mock_async_zeroconf, entity_reg +): + """Test the entity filter with hidden entities.""" + entry = await async_init_integration(hass) + + from homeassistant.helpers.entity_registry import RegistryEntry + + select_config_entity: RegistryEntry = entity_reg.async_get_or_create( + "select", + "any", + "any", + device_id="1234", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(select_config_entity.entity_id, "off") + + switch_config_entity: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "any", + "any", + device_id="1234", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(switch_config_entity.entity_id, "off") + hass.states.async_set("select.keep", "open") + + hass.states.async_set("cover.excluded_test", "open") + hass.states.async_set("light.included_test", "on") + + entity_filter = generate_filter( + ["select"], + ["switch.test", switch_config_entity.entity_id], + [], + [], + ["*.included_*"], + ["*.excluded_*"], + ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter) + + homekit.bridge = Mock() + homekit.bridge.accessories = {} + + filtered_states = await homekit.async_configure_accessories() + assert ( + hass.states.get(switch_config_entity.entity_id) in filtered_states + ) # explicitly included + assert ( + hass.states.get(select_config_entity.entity_id) not in filtered_states + ) # not explicted included and its a hidden entity + assert hass.states.get("cover.excluded_test") not in filtered_states + assert hass.states.get("light.included_test") in filtered_states + assert hass.states.get("select.keep") in filtered_states + + async def test_homekit_start(hass, hk_driver, mock_async_zeroconf, device_reg): """Test HomeKit start method.""" entry = await async_init_integration(hass)