Allow hiding and unhiding group members (#68192)

This commit is contained in:
Erik Montnemery 2022-03-22 12:14:34 +01:00 committed by GitHub
parent b5d2c6e43a
commit 1b955970f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 359 additions and 14 deletions

View File

@ -28,8 +28,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id
from homeassistant.helpers import start
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import config_validation as cv, entity_registry as er, start
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event
@ -40,6 +39,8 @@ from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from .const import CONF_HIDE_MEMBERS
# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
DOMAIN = "group"
@ -238,6 +239,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove a config entry."""
# Unhide the group members
registry = er.async_get(hass)
if not entry.options[CONF_HIDE_MEMBERS]:
return
for member in entry.options[CONF_ENTITIES]:
if not (entity_id := er.async_resolve_entity_id(registry, member)):
continue
if (entity_entry := registry.async_get(entity_id)) is None:
continue
if entity_entry.hidden_by != er.RegistryEntryHider.INTEGRATION:
continue
registry.async_update_entity(entity_id, hidden_by=None)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up all groups found defined in the configuration."""
if DOMAIN not in hass.data:

View File

@ -7,8 +7,8 @@ from typing import Any, cast
import voluptuous as vol
from homeassistant.const import CONF_ENTITIES
from homeassistant.core import callback
from homeassistant.helpers import selector
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er, selector
from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler,
HelperFlowStep,
@ -16,6 +16,7 @@ from homeassistant.helpers.helper_config_entry_flow import (
from . import DOMAIN
from .binary_sensor import CONF_ALL
from .const import CONF_HIDE_MEMBERS
def basic_group_options_schema(domain: str) -> vol.Schema:
@ -25,6 +26,9 @@ def basic_group_options_schema(domain: str) -> vol.Schema:
vol.Required(CONF_ENTITIES): selector.selector(
{"entity": {"domain": domain, "multiple": True}}
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector(
{"boolean": {}}
),
}
)
@ -98,6 +102,39 @@ class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
@callback
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return cast(str, options["name"]) if "name" in options else ""
@callback
def async_config_flow_finished(self, options: Mapping[str, Any]) -> None:
"""Hide the group members if requested."""
if options[CONF_HIDE_MEMBERS]:
_async_hide_members(
self.hass, options[CONF_ENTITIES], er.RegistryEntryHider.INTEGRATION
)
@callback
@staticmethod
def async_options_flow_finished(
hass: HomeAssistant, options: Mapping[str, Any]
) -> None:
"""Hide or unhide the group members as requested."""
hidden_by = (
er.RegistryEntryHider.INTEGRATION if options[CONF_HIDE_MEMBERS] else None
)
_async_hide_members(hass, options[CONF_ENTITIES], hidden_by)
def _async_hide_members(
hass: HomeAssistant, members: list[str], hidden_by: er.RegistryEntryHider | None
) -> None:
"""Hide or unhide group members."""
registry = er.async_get(hass)
for member in members:
if not (entity_id := er.async_resolve_entity_id(registry, member)):
continue
if entity_id not in registry.entities:
continue
registry.async_update_entity(entity_id, hidden_by=hidden_by)

View File

@ -0,0 +1,3 @@
"""Constants for the Group integration."""
CONF_HIDE_MEMBERS = "hide_members"

View File

@ -961,6 +961,22 @@ def async_validate_entity_id(registry: EntityRegistry, entity_id_or_uuid: str) -
return entry.entity_id
@callback
def async_resolve_entity_id(
registry: EntityRegistry, entity_id_or_uuid: str
) -> str | None:
"""Validate and resolve an entity id or UUID to an entity id.
Returns None if the entity or UUID is invalid, or if the UUID is not
associated with an entity registry item.
"""
if valid_entity_id(entity_id_or_uuid):
return entity_id_or_uuid
if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None:
return None
return entry.entity_id
@callback
def async_validate_entity_ids(
registry: EntityRegistry, entity_ids_or_uuids: list[str]

View File

@ -137,7 +137,9 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow):
if cls.options_flow is None:
raise UnknownHandler
return HelperOptionsFlowHandler(config_entry, cls.options_flow)
return HelperOptionsFlowHandler(
config_entry, cls.options_flow, cls.async_options_flow_finished
)
# Create an async_get_options_flow method
cls.async_get_options_flow = _async_get_options_flow # type: ignore[assignment]
@ -167,6 +169,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow):
# pylint: disable-next=no-self-use
@abstractmethod
@callback
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title.
@ -174,6 +177,25 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow):
input from the config flow steps.
"""
@callback
def async_config_flow_finished(self, options: Mapping[str, Any]) -> None:
"""Take necessary actions after the config flow is finished, if needed.
The options parameter contains config entry options, which is the union of user
input from the config flow steps.
"""
@callback
@staticmethod
def async_options_flow_finished(
hass: HomeAssistant, options: Mapping[str, Any]
) -> None:
"""Take necessary actions after the options flow is finished, if needed.
The options parameter contains config entry options, which is the union of stored
options and user input from the options flow steps.
"""
@callback
def async_create_entry( # pylint: disable=arguments-differ
self,
@ -181,6 +203,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow):
**kwargs: Any,
) -> FlowResult:
"""Finish config flow and create a config entry."""
self.async_config_flow_finished(data)
return super().async_create_entry(
data={}, options=data, title=self.async_config_entry_title(data), **kwargs
)
@ -193,10 +216,12 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow):
self,
config_entry: config_entries.ConfigEntry,
options_flow: dict[str, vol.Schema],
async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None],
) -> None:
"""Initialize options flow."""
self._common_handler = HelperCommonFlowHandler(self, options_flow, config_entry)
self._config_entry = config_entry
self._async_options_flow_finished = async_options_flow_finished
for step in options_flow:
setattr(self, f"async_step_{step}", self._async_step)
@ -210,10 +235,12 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow):
@callback
def async_create_entry( # pylint: disable=arguments-differ
self,
data: Mapping[str, Any],
**kwargs: Any,
) -> FlowResult:
"""Finish config flow and create a config entry."""
return super().async_create_entry(title="", **kwargs)
self._async_options_flow_finished(self.hass, data)
return super().async_create_entry(title="", data=data, **kwargs)
@callback

View File

@ -7,6 +7,7 @@ from homeassistant import config_entries
from homeassistant.components.group import DOMAIN, async_setup_entry
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
@ -68,8 +69,9 @@ async def test_config_flow(
assert result["title"] == "Living Room"
assert result["data"] == {}
assert result["options"] == {
"group_type": group_type,
"entities": members,
"group_type": group_type,
"hide_members": False,
"name": "Living Room",
**extra_options,
}
@ -78,9 +80,10 @@ async def test_config_flow(
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
assert config_entry.data == {}
assert config_entry.options == {
"group_type": group_type,
"name": "Living Room",
"entities": members,
"group_type": group_type,
"hide_members": False,
"name": "Living Room",
**extra_options,
}
@ -91,6 +94,69 @@ async def test_config_flow(
assert state.attributes[key] == extra_attrs[key]
@pytest.mark.parametrize(
"hide_members,hidden_by", ((False, None), (True, "integration"))
)
@pytest.mark.parametrize(
"group_type,extra_input",
(
("binary_sensor", {"all": False}),
("cover", {}),
("fan", {}),
("light", {}),
("media_player", {}),
),
)
async def test_config_flow_hides_members(
hass: HomeAssistant, group_type, extra_input, hide_members, hidden_by
) -> None:
"""Test the config flow hides members if requested."""
fake_uuid = "a266a680b608c32770e6c45bfe6b8411"
registry = er.async_get(hass)
entry = registry.async_get_or_create(
group_type, "test", "unique", suggested_object_id="one"
)
assert entry.entity_id == f"{group_type}.one"
assert entry.hidden_by is None
entry = registry.async_get_or_create(
group_type, "test", "unique3", suggested_object_id="three"
)
assert entry.entity_id == f"{group_type}.three"
assert entry.hidden_by is None
members = [f"{group_type}.one", f"{group_type}.two", fake_uuid, entry.id]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"group_type": group_type},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == group_type
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"name": "Living Room",
"entities": members,
"hide_members": hide_members,
**extra_input,
},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by
assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by
def get_suggested(schema, key):
"""Get suggested value for key in voluptuous schema."""
for k in schema.keys():
@ -124,7 +190,7 @@ async def test_options(
for member in members2:
hass.states.async_set(member, member_state, {})
switch_as_x_config_entry = MockConfigEntry(
group_config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
@ -135,9 +201,9 @@ async def test_options(
},
title="Bed Room",
)
switch_as_x_config_entry.add_to_hass(hass)
group_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id)
assert await hass.config_entries.async_setup(group_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(f"{group_type}.bed_room")
@ -159,15 +225,17 @@ async def test_options(
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
"group_type": group_type,
"entities": members2,
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
**extra_options,
}
assert config_entry.data == {}
assert config_entry.options == {
"group_type": group_type,
"entities": members2,
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
**extra_options,
}
@ -196,3 +264,83 @@ async def test_options(
assert get_suggested(result["data_schema"].schema, "entities") is None
assert get_suggested(result["data_schema"].schema, "name") is None
@pytest.mark.parametrize(
"hide_members,hidden_by_initial,hidden_by",
((False, "integration", None), (True, None, "integration")),
)
@pytest.mark.parametrize(
"group_type,extra_input",
(
("binary_sensor", {"all": False}),
("cover", {}),
("fan", {}),
("light", {}),
("media_player", {}),
),
)
async def test_options_flow_hides_members(
hass: HomeAssistant,
group_type,
extra_input,
hide_members,
hidden_by_initial,
hidden_by,
) -> None:
"""Test the options flow hides or unhides members if requested."""
fake_uuid = "a266a680b608c32770e6c45bfe6b8411"
registry = er.async_get(hass)
entry = registry.async_get_or_create(
group_type,
"test",
"unique1",
suggested_object_id="one",
hidden_by=hidden_by_initial,
)
assert entry.entity_id == f"{group_type}.one"
entry = registry.async_get_or_create(
group_type,
"test",
"unique3",
suggested_object_id="three",
hidden_by=hidden_by_initial,
)
assert entry.entity_id == f"{group_type}.three"
members = [f"{group_type}.one", f"{group_type}.two", fake_uuid, entry.id]
group_config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
"entities": members,
"group_type": group_type,
"hide_members": False,
"name": "Bed Room",
**extra_input,
},
title="Bed Room",
)
group_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(group_config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(group_config_entry.entry_id)
assert result["type"] == RESULT_TYPE_FORM
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"entities": members,
"hide_members": hide_members,
},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by
assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by

View File

@ -1416,3 +1416,97 @@ async def test_setup_and_remove_config_entry(
# Check the state and entity registry entry are removed
assert hass.states.get(f"{group_type}.bed_room") is None
assert registry.async_get(f"{group_type}.bed_room") is None
@pytest.mark.parametrize(
"hide_members,hidden_by_initial,hidden_by",
(
(False, "integration", "integration"),
(False, None, None),
(False, "user", "user"),
(True, "integration", None),
(True, None, None),
(True, "user", "user"),
),
)
@pytest.mark.parametrize(
"group_type,extra_options",
(
("binary_sensor", {"all": False}),
("cover", {}),
("fan", {}),
("light", {}),
("media_player", {}),
),
)
async def test_unhide_members_on_remove(
hass: HomeAssistant,
group_type: str,
extra_options: dict[str, Any],
hide_members: bool,
hidden_by_initial: str,
hidden_by: str,
) -> None:
"""Test removing a config entry."""
registry = er.async_get(hass)
registry = er.async_get(hass)
entry1 = registry.async_get_or_create(
group_type,
"test",
"unique1",
suggested_object_id="one",
hidden_by=hidden_by_initial,
)
assert entry1.entity_id == f"{group_type}.one"
entry3 = registry.async_get_or_create(
group_type,
"test",
"unique3",
suggested_object_id="three",
hidden_by=hidden_by_initial,
)
assert entry3.entity_id == f"{group_type}.three"
entry4 = registry.async_get_or_create(
group_type,
"test",
"unique4",
suggested_object_id="four",
)
assert entry4.entity_id == f"{group_type}.four"
members = [f"{group_type}.one", f"{group_type}.two", entry3.id, entry4.id]
# Setup the config entry
group_config_entry = MockConfigEntry(
data={},
domain=group.DOMAIN,
options={
"entities": members,
"group_type": group_type,
"hide_members": hide_members,
"name": "Bed Room",
**extra_options,
},
title="Bed Room",
)
group_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(group_config_entry.entry_id)
await hass.async_block_till_done()
# Check the state is present
assert hass.states.get(f"{group_type}.bed_room")
# Remove one entity registry entry, to make sure this does not trip up config entry
# removal
registry.async_remove(entry4.entity_id)
# Remove the config entry
assert await hass.config_entries.async_remove(group_config_entry.entry_id)
await hass.async_block_till_done()
# Check the group members are unhidden
assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by
assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by