Allow hiding and unhiding group members (#68192)
parent
b5d2c6e43a
commit
1b955970f8
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
"""Constants for the Group integration."""
|
||||
|
||||
CONF_HIDE_MEMBERS = "hide_members"
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue