From a9cc2d23221a58a3bdd59d1a21ae291369a3f152 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Mar 2022 13:05:04 +0100 Subject: [PATCH] Add config flow for cover, fan, light and media_player groups (#67660) * Add options flow support to HelperConfigFlowHandler * Add config flow for cover, fan, light and media_player groups * Update according to review comments * Update translation strings * Update translation strings * Copy schema before adding suggested values --- homeassistant/components/group/__init__.py | 28 +++ homeassistant/components/group/config_flow.py | 81 ++++++++ homeassistant/components/group/cover.py | 19 +- homeassistant/components/group/fan.py | 17 +- homeassistant/components/group/light.py | 19 +- homeassistant/components/group/manifest.json | 7 +- .../components/group/media_player.py | 19 +- homeassistant/components/group/strings.json | 62 ++++++ homeassistant/generated/config_flows.py | 1 + .../helpers/helper_config_entry_flow.py | 118 ++++++++++- tests/components/group/test_config_flow.py | 191 ++++++++++++++++++ 11 files changed, 546 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/group/config_flow.py create mode 100644 tests/components/group/test_config_flow.py diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 8e595d75db6..e5dbb3c3630 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -11,6 +11,7 @@ from typing import Any, Union, cast import voluptuous as vol from homeassistant import core as ha +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -58,6 +59,14 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" +PLATFORMS_CONFIG_ENTRY = [ + Platform.BINARY_SENSOR, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.MEDIA_PLAYER, +] + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, @@ -218,6 +227,25 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: return groups +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + hass.config_entries.async_setup_platforms(entry, (entry.options["group_type"],)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (entry.options["group_type"],) + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all groups found defined in the configuration.""" if DOMAIN not in hass.data: diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py new file mode 100644 index 00000000000..82de2056da5 --- /dev/null +++ b/homeassistant/components/group/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for Group integration.""" +from __future__ import annotations + +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITIES +from homeassistant.helpers import helper_config_entry_flow, selector + +from . import DOMAIN + + +def basic_group_options_schema(domain: str) -> vol.Schema: + """Generate options schema.""" + return vol.Schema( + { + vol.Required(CONF_ENTITIES): selector.selector( + {"entity": {"domain": domain, "multiple": True}} + ), + } + ) + + +def basic_group_config_schema(domain: str) -> vol.Schema: + """Generate config schema.""" + return vol.Schema({vol.Required("name"): selector.selector({"text": {}})}).extend( + basic_group_options_schema(domain).schema + ) + + +STEPS = { + "init": vol.Schema( + { + vol.Required("group_type"): selector.selector( + { + "select": { + "options": [ + "cover", + "fan", + "light", + "media_player", + ] + } + } + ) + } + ), + "cover": basic_group_config_schema("cover"), + "fan": basic_group_config_schema("fan"), + "light": basic_group_config_schema("light"), + "media_player": basic_group_config_schema("media_player"), + "cover_options": basic_group_options_schema("cover"), + "fan_options": basic_group_options_schema("fan"), + "light_options": basic_group_options_schema("light"), + "media_player_options": basic_group_options_schema("media_player"), +} + + +class GroupConfigFlowHandler( + helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN +): + """Handle a config or options flow for Switch Light.""" + + steps = STEPS + + def async_config_entry_title(self, user_input: dict[str, Any]) -> str: + """Return config entry title.""" + return cast(str, user_input["name"]) if "name" in user_input else "" + + @staticmethod + def async_initial_options_step(config_entry: ConfigEntry) -> str: + """Return initial options step.""" + return f"{config_entry.options['group_type']}_options" + + def async_next_step(self, step_id: str, user_input: dict[str, Any]) -> str | None: + """Return next step_id.""" + if step_id == "init": + return cast(str, user_input["group_type"]) + return None diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index a4c550b8119..851079ad5e4 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -22,6 +22,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -43,7 +44,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -85,6 +86,22 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [CoverGroup(config_entry.entry_id, config_entry.title, entity_id)] + ) + + class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 7920e0f5d20..f26b1bf57fb 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -25,6 +25,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -35,7 +36,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -78,6 +79,20 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entity_id)]) + + class FanGroup(GroupEntity, FanEntity): """Representation of a FanGroup.""" diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index ea74136b204..c51baf0ff66 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -36,6 +36,7 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -48,7 +49,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -92,6 +93,22 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [LightGroup(config_entry.entry_id, config_entry.title, entity_id)] + ) + + FORWARDED_ATTRIBUTES = frozenset( { ATTR_BRIGHTNESS, diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 6d8fd446c27..9c97f318da3 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -2,7 +2,10 @@ "domain": "group", "name": "Group", "documentation": "https://www.home-assistant.io/integrations/group", - "codeowners": ["@home-assistant/core"], + "codeowners": [ + "@home-assistant/core" + ], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "config_flow": true } diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 509c0cb4083..e1c044fd3e3 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -32,6 +32,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_STEP, MediaPlayerEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -55,7 +56,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType @@ -96,6 +97,22 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [MediaGroup(config_entry.entry_id, config_entry.title, entity_id)] + ) + + class MediaGroup(MediaPlayerEntity): """Representation of a Media Group.""" diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index e29407bf932..8f0531ac7e4 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -1,5 +1,67 @@ { "title": "Group", + "config": { + "step": { + "init": { + "description": "Select group type", + "data": { + "group_type": "Group type" + } + }, + "cover": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "cover_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + }, + "fan": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "fan_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + }, + "light": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "light_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + }, + "media_player": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "media_player_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + } + } + }, "state": { "_": { "off": "[%key:common::state::off%]", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2ee6e235e91..379adad2b93 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -127,6 +127,7 @@ FLOWS = [ "google_travel_time", "gpslogger", "gree", + "group", "growatt_server", "guardian", "habitica", diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 82d10868d01..c632ad60eae 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -2,13 +2,20 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Awaitable, Callable +import copy +import types from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import ( + RESULT_TYPE_CREATE_ENTRY, + FlowResult, + UnknownHandler, +) class HelperCommonFlowHandler: @@ -16,19 +23,18 @@ class HelperCommonFlowHandler: def __init__( self, - handler: HelperConfigFlowHandler, + handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, config_entry: config_entries.ConfigEntry | None, ) -> None: """Initialize a common handler.""" self._handler = handler self._options = dict(config_entry.options) if config_entry is not None else {} - async def async_step(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step( + self, step_id: str, _user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a step.""" errors = None - step_id = ( - self._handler.cur_step["step_id"] if self._handler.cur_step else "init" - ) if _user_input is not None: errors = {} try: @@ -38,19 +44,28 @@ class HelperCommonFlowHandler: except vol.Invalid as exc: errors["base"] = str(exc) else: + self._options.update(user_input) if ( next_step_id := self._handler.async_next_step(step_id, user_input) ) is None: title = self._handler.async_config_entry_title(user_input) return self._handler.async_create_entry( - title=title, data=user_input + title=title, data=self._options ) return self._handler.async_show_form( step_id=next_step_id, data_schema=self._handler.steps[next_step_id] ) + schema = dict(self._handler.steps[step_id].schema) + for key in list(schema): + if key in self._options and isinstance(key, vol.Marker): + new_key = copy.copy(key) + new_key.description = {"suggested_value": self._options[key]} + val = schema.pop(key) + schema[new_key] = val + return self._handler.async_show_form( - step_id=step_id, data_schema=self._handler.steps[step_id], errors=errors + step_id=step_id, data_schema=vol.Schema(schema), errors=errors ) @@ -66,6 +81,29 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) + @callback + def _async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + if ( + cls.async_initial_options_step + is HelperConfigFlowHandler.async_initial_options_step + ): + raise UnknownHandler + + return HelperOptionsFlowHandler( + config_entry, + cls.steps, + cls.async_config_entry_title, + cls.async_initial_options_step, + cls.async_next_step, + cls.async_validate_input, + ) + + # Create an async_get_options_flow method + cls.async_get_options_flow = _async_get_options_flow # type: ignore[assignment] + # Create flow step methods for each step defined in the flow schema for step in cls.steps: setattr(cls, f"async_step_{step}", cls.async_step) @@ -73,6 +111,17 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Initialize config flow.""" self._common_handler = HelperCommonFlowHandler(self, None) + @classmethod + @callback + def async_supports_options_flow( + cls, config_entry: config_entries.ConfigEntry + ) -> bool: + """Return options flow support for this handler.""" + return ( + cls.async_initial_options_step + is not HelperConfigFlowHandler.async_initial_options_step + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -81,7 +130,8 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): async def async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: """Handle a step.""" - result = await self._common_handler.async_step(user_input) + step_id = self.cur_step["step_id"] if self.cur_step else "init" + result = await self._common_handler.async_step(step_id, user_input) if result["type"] == RESULT_TYPE_CREATE_ENTRY: result["options"] = result["data"] result["data"] = {} @@ -97,9 +147,57 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Return next step_id, or None to finish the flow.""" return None + @staticmethod + @callback + def async_initial_options_step( + config_entry: config_entries.ConfigEntry, + ) -> str: + """Return initial step_id of options flow.""" + raise UnknownHandler + # pylint: disable-next=no-self-use async def async_validate_input( self, hass: HomeAssistant, step_id: str, user_input: dict[str, Any] ) -> dict[str, Any]: """Validate user input.""" return user_input + + +class HelperOptionsFlowHandler(config_entries.OptionsFlow): + """Handle an options flow for helper integrations.""" + + def __init__( + self, + config_entry: config_entries.ConfigEntry, + steps: dict[str, vol.Schema], + config_entry_title: Callable[[Any, dict[str, Any]], str], + initial_step: Callable[[config_entries.ConfigEntry], str], + next_step: Callable[[Any, str, dict[str, Any]], str | None], + validate: Callable[ + [Any, HomeAssistant, str, dict[str, Any]], Awaitable[dict[str, Any]] + ], + ) -> None: + """Initialize options flow.""" + self._common_handler = HelperCommonFlowHandler(self, config_entry) + self._config_entry = config_entry + self._initial_step = initial_step(config_entry) + self.async_config_entry_title = types.MethodType(config_entry_title, self) + self.async_next_step = types.MethodType(next_step, self) + self.async_validate_input = types.MethodType(validate, self) + self.steps = steps + for step in self.steps: + if step == "init": + continue + setattr(self, f"async_step_{step}", self.async_step) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + return await self.async_step(user_input) + + async def async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle a step.""" + # pylint: disable-next=unsubscriptable-object # self.cur_step is a dict + step_id = self.cur_step["step_id"] if self.cur_step else self._initial_step + return await self._common_handler.async_step(step_id, user_input) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py new file mode 100644 index 00000000000..cc97ff8c95f --- /dev/null +++ b/tests/components/group/test_config_flow.py @@ -0,0 +1,191 @@ +"""Test the Switch config flow.""" +from unittest.mock import patch + +import pytest + +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 + + +@pytest.mark.parametrize( + "group_type,group_state,member_state,member_attributes", + ( + ("cover", "open", "open", {}), + ("fan", "on", "on", {}), + ("light", "on", "on", {}), + ("media_player", "on", "on", {}), + ), +) +async def test_config_flow( + hass: HomeAssistant, group_type, group_state, member_state, member_attributes +) -> None: + """Test the config flow.""" + members = [f"{group_type}.one", f"{group_type}.two"] + for member in members: + hass.states.async_set(member, member_state, member_attributes) + + 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 + + with patch( + "homeassistant.components.group.async_setup_entry", wraps=async_setup_entry + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "Living Room", + "entities": members, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Living Room" + assert result["data"] == {} + assert result["options"] == { + "group_type": group_type, + "entities": members, + "name": "Living Room", + } + assert len(mock_setup_entry.mock_calls) == 1 + + 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, + } + + state = hass.states.get(f"{group_type}.living_room") + assert state.state == group_state + assert state.attributes["entity_id"] == members + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize( + "group_type,member_state", + (("cover", "open"), ("fan", "on"), ("light", "on"), ("media_player", "on")), +) +async def test_options(hass: HomeAssistant, group_type, member_state) -> None: + """Test reconfiguring.""" + members1 = [f"{group_type}.one", f"{group_type}.two"] + members2 = [f"{group_type}.four", f"{group_type}.five"] + + for member in members1: + hass.states.async_set(member, member_state, {}) + for member in members2: + hass.states.async_set(member, member_state, {}) + + 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 + + assert get_suggested(result["data_schema"].schema, "group_type") 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 + + assert get_suggested(result["data_schema"].schema, "entities") is None + assert get_suggested(result["data_schema"].schema, "name") is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "Bed Room", + "entities": members1, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "group_type": group_type, + "entities": members1, + "name": "Bed Room", + } + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == f"{group_type}_options" + assert get_suggested(result["data_schema"].schema, "entities") == members1 + assert "name" not in result["data_schema"].schema + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members2, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "group_type": group_type, + "entities": members2, + "name": "Bed Room", + } + assert config_entry.data == {} + assert config_entry.options == { + "group_type": group_type, + "entities": members2, + "name": "Bed Room", + } + assert config_entry.title == "Bed Room" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members2 + + # Check we don't get suggestions from another entry + 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 + assert get_suggested(result["data_schema"].schema, "group_type") 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 + + assert get_suggested(result["data_schema"].schema, "entities") is None + assert get_suggested(result["data_schema"].schema, "name") is None