Prevent HomeKit from offering hidden entities (#69042)

This commit is contained in:
J. Nick Koston 2022-04-01 01:34:44 -10:00 committed by GitHub
parent a81194cdd7
commit 220beefb89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 107 additions and 18 deletions

View File

@ -466,7 +466,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
entity_filter = self.hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
all_supported_entities = _async_get_matching_entities(self.hass, domains)
all_supported_entities = _async_get_matching_entities(
self.hass, domains, include_entity_category=True
)
# In accessory mode we can only have one
default_value = next(
iter(
@ -505,7 +507,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
entity_filter = self.hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
all_supported_entities = _async_get_matching_entities(self.hass, domains)
all_supported_entities = _async_get_matching_entities(
self.hass, domains, include_entity_category=True
)
if not entities:
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
# Strip out entities that no longer exist to prevent error in the UI
@ -559,21 +563,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
all_supported_entities = _async_get_matching_entities(self.hass, domains)
if not entities:
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
ent_reg = entity_registry.async_get(self.hass)
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
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 excluded_entities
}
# Strip out entities that no longer exist to prevent error in the UI
default_value = [
entity_id for entity_id in entities if entity_id in all_supported_entities
@ -652,16 +642,37 @@ async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]:
return dict(sorted(unsorted.items(), key=lambda item: item[1]))
def _exclude_by_entity_registry(
ent_reg: entity_registry.EntityRegistry,
entity_id: str,
include_entity_category: bool,
) -> bool:
"""Filter out hidden entities and ones with entity category (unless specified)."""
return bool(
(entry := ent_reg.async_get(entity_id))
and (
entry.hidden_by is not None
or (not include_entity_category or entry.entity_category is not None)
)
)
def _async_get_matching_entities(
hass: HomeAssistant, domains: list[str] | None = None
hass: HomeAssistant,
domains: list[str] | None = None,
include_entity_category: bool = False,
) -> dict[str, str]:
"""Fetch all entities or entities in the given domains."""
ent_reg = entity_registry.async_get(hass)
return {
state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})"
for state in sorted(
hass.states.async_all(domains and set(domains)),
key=lambda item: item.entity_id,
)
if not _exclude_by_entity_registry(
ent_reg, state.entity_id, include_entity_category
)
}

View File

@ -1481,3 +1481,81 @@ async def test_options_flow_exclude_mode_skips_hidden_entities(
"include_entities": [],
},
}
@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
async def test_options_flow_include_mode_skips_hidden_entities(
port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg
):
"""Ensure include 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": "include",
},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "include"
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": [],
"include_domains": [],
"include_entities": ["media_player.tv", "switch.other"],
},
}