diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 5794819995d..e007f967438 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -326,6 +326,9 @@ class SubentryManagerFlowIndexView( """Return context.""" context = super().get_context(data) context["source"] = config_entries.SOURCE_USER + if subentry_id := data.get("subentry_id"): + context["source"] = config_entries.SOURCE_RECONFIGURE + context["subentry_id"] = subentry_id return context diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 320083386c1..c6979e68b90 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -339,7 +339,14 @@ class ConfigSubentryDataWithId(ConfigSubentryData): subentry_id: str -class SubentryFlowResult(FlowResult[FlowContext, tuple[str, str]], total=False): +class SubentryFlowContext(FlowContext, total=False): + """Typed context dict for config flow.""" + + entry_id: str + subentry_id: str + + +class SubentryFlowResult(FlowResult[SubentryFlowContext, tuple[str, str]], total=False): """Typed result dict for subentry flow.""" unique_id: str | None @@ -389,7 +396,7 @@ class ConfigEntry(Generic[_DataT]): supports_remove_device: bool | None _supports_options: bool | None _supports_reconfigure: bool | None - _supported_subentries: tuple[str, ...] | None + _supported_subentry_flows: dict[str, dict[str, bool]] | None update_listeners: list[UpdateListenerType] _async_cancel_retry_setup: Callable[[], Any] | None _on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None @@ -500,7 +507,7 @@ class ConfigEntry(Generic[_DataT]): _setter(self, "_supports_reconfigure", None) # Supports subentries - _setter(self, "_supported_subentries", None) + _setter(self, "_supported_subentry_flows", None) # Listeners to call on update _setter(self, "update_listeners", []) @@ -575,16 +582,26 @@ class ConfigEntry(Generic[_DataT]): return self._supports_reconfigure or False @property - def supported_subentries(self) -> tuple[str, ...]: + def supported_subentry_flows(self) -> dict[str, dict[str, bool]]: """Return supported subentries.""" - if self._supported_subentries is None and ( + if self._supported_subentry_flows is None and ( handler := HANDLERS.get(self.domain) ): # work out sub entries supported by the handler + supported_flows = handler.async_get_supported_subentry_flows(self) object.__setattr__( - self, "_supported_subentries", handler.async_supported_subentries(self) + self, + "_supported_subentry_flows", + { + subentry_flow_type: { + "supports_reconfigure": hasattr( + subentry_flow_handler, "async_step_reconfigure" + ) + } + for subentry_flow_type, subentry_flow_handler in supported_flows.items() + }, ) - return self._supported_subentries or () + return self._supported_subentry_flows or {} def clear_state_cache(self) -> None: """Clear cached properties that are included in as_json_fragment.""" @@ -605,7 +622,7 @@ class ConfigEntry(Generic[_DataT]): "supports_remove_device": self.supports_remove_device or False, "supports_unload": self.supports_unload or False, "supports_reconfigure": self.supports_reconfigure, - "supported_subentries": self.supported_subentries, + "supported_subentry_flows": self.supported_subentry_flows, "pref_disable_new_entities": self.pref_disable_new_entities, "pref_disable_polling": self.pref_disable_polling, "disabled_by": self.disabled_by, @@ -2378,6 +2395,11 @@ class ConfigEntries: _setter(entry, "modified_at", utcnow()) + self._async_save_and_notify(entry) + return True + + @callback + def _async_save_and_notify(self, entry: ConfigEntry) -> None: for listener in entry.update_listeners: self.hass.async_create_task( listener(self.hass, entry), @@ -2388,7 +2410,6 @@ class ConfigEntries: entry.clear_state_cache() entry.clear_storage_cache() self._async_dispatch(ConfigEntryChange.UPDATED, entry) - return True @callback def async_add_subentry(self, entry: ConfigEntry, subentry: ConfigSubentry) -> bool: @@ -2417,6 +2438,54 @@ class ConfigEntries: ent_reg.async_clear_config_subentry(entry.entry_id, subentry_id) return result + @callback + def async_update_subentry( + self, + entry: ConfigEntry, + subentry: ConfigSubentry, + *, + data: Mapping[str, Any] | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, + unique_id: str | None | UndefinedType = UNDEFINED, + ) -> bool: + """Update a config entry. + + If the entry was changed, the update_listeners are + fired and this function returns True + + If the entry was not changed, the update_listeners are + not fired and this function returns False + """ + if entry.entry_id not in self._entries: + raise UnknownEntry(entry.entry_id) + if subentry.subentry_id not in entry.subentries: + raise UnknownSubEntry(subentry.subentry_id) + + self.hass.verify_event_loop_thread("hass.config_entries.async_update_subentry") + changed = False + _setter = object.__setattr__ + + if unique_id is not UNDEFINED and subentry.unique_id != unique_id: + self._raise_if_subentry_unique_id_exists(entry, unique_id) + changed = True + _setter(subentry, "unique_id", unique_id) + + if title is not UNDEFINED and subentry.title != title: + changed = True + _setter(subentry, "title", title) + + if data is not UNDEFINED and subentry.data != data: + changed = True + _setter(subentry, "data", MappingProxyType(data)) + + if not changed: + return False + + _setter(entry, "modified_at", utcnow()) + + self._async_save_and_notify(entry) + return True + def _raise_if_subentry_unique_id_exists( self, entry: ConfigEntry, unique_id: str | None ) -> None: @@ -2763,19 +2832,13 @@ class ConfigFlow(ConfigEntryBaseFlow): """Return options flow support for this handler.""" return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow - @staticmethod - @callback - def async_get_subentry_flow( - config_entry: ConfigEntry, subentry_type: str - ) -> ConfigSubentryFlow: - """Get the subentry flow for this handler.""" - raise NotImplementedError - @classmethod @callback - def async_supported_subentries(cls, config_entry: ConfigEntry) -> tuple[str, ...]: + def async_get_supported_subentry_flows( + cls, config_entry: ConfigEntry + ) -> dict[str, type[ConfigSubentryFlow]]: """Return subentries supported by this handler.""" - return () + return {} @callback def _async_abort_entries_match( @@ -3231,7 +3294,9 @@ class _ConfigSubFlowManager: class ConfigSubentryFlowManager( - data_entry_flow.FlowManager[FlowContext, SubentryFlowResult, tuple[str, str]], + data_entry_flow.FlowManager[ + SubentryFlowContext, SubentryFlowResult, tuple[str, str] + ], _ConfigSubFlowManager, ): """Manage all the config subentry flows that are in progress.""" @@ -3255,18 +3320,19 @@ class ConfigSubentryFlowManager( entry_id, subentry_type = handler_key entry = self._async_get_config_entry(entry_id) handler = await _async_get_flow_handler(self.hass, entry.domain, {}) - if subentry_type not in handler.async_supported_subentries(entry): + subentry_flows = handler.async_get_supported_subentry_flows(entry) + if subentry_type not in subentry_flows: raise data_entry_flow.UnknownHandler( f"Config entry '{entry.domain}' does not support subentry '{subentry_type}'" ) - subentry_flow = handler.async_get_subentry_flow(entry, subentry_type) + subentry_flow = subentry_flows[subentry_type]() subentry_flow.init_step = context["source"] return subentry_flow async def async_finish_flow( self, flow: data_entry_flow.FlowHandler[ - FlowContext, SubentryFlowResult, tuple[str, str] + SubentryFlowContext, SubentryFlowResult, tuple[str, str] ], result: SubentryFlowResult, ) -> SubentryFlowResult: @@ -3302,7 +3368,9 @@ class ConfigSubentryFlowManager( class ConfigSubentryFlow( - data_entry_flow.FlowHandler[FlowContext, SubentryFlowResult, tuple[str, str]] + data_entry_flow.FlowHandler[ + SubentryFlowContext, SubentryFlowResult, tuple[str, str] + ] ): """Base class for config subentry flows.""" @@ -3320,6 +3388,9 @@ class ConfigSubentryFlow( unique_id: str | None = None, ) -> SubentryFlowResult: """Finish config flow and create a config entry.""" + if self.source != SOURCE_USER: + raise ValueError(f"Source is {self.source}, expected {SOURCE_USER}") + result = super().async_create_entry( title=title, data=data, @@ -3331,6 +3402,70 @@ class ConfigSubentryFlow( return result + @callback + def async_update_and_abort( + self, + entry: ConfigEntry, + subentry: ConfigSubentry, + *, + unique_id: str | None | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, + data: Mapping[str, Any] | UndefinedType = UNDEFINED, + data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED, + ) -> SubentryFlowResult: + """Update config subentry and finish subentry flow. + + :param data: replace the subentry data with new data + :param data_updates: add items from data_updates to subentry data - existing + keys are overridden + :param title: replace the title of the subentry + :param unique_id: replace the unique_id of the subentry + """ + if data_updates is not UNDEFINED: + if data is not UNDEFINED: + raise ValueError("Cannot set both data and data_updates") + data = entry.data | data_updates + self.hass.config_entries.async_update_subentry( + entry=entry, + subentry=subentry, + unique_id=unique_id, + title=title, + data=data, + ) + return self.async_abort(reason="reconfigure_successful") + + @property + def _reconfigure_entry_id(self) -> str: + """Return reconfigure entry id.""" + if self.source != SOURCE_RECONFIGURE: + raise ValueError(f"Source is {self.source}, expected {SOURCE_RECONFIGURE}") + return self.handler[0] + + @callback + def _get_reconfigure_entry(self) -> ConfigEntry: + """Return the reconfigure config entry linked to the current context.""" + return self.hass.config_entries.async_get_known_entry( + self._reconfigure_entry_id + ) + + @property + def _reconfigure_subentry_id(self) -> str: + """Return reconfigure subentry id.""" + if self.source != SOURCE_RECONFIGURE: + raise ValueError(f"Source is {self.source}, expected {SOURCE_RECONFIGURE}") + return self.context["subentry_id"] + + @callback + def _get_reconfigure_subentry(self) -> ConfigSubentry: + """Return the reconfigure config subentry linked to the current context.""" + entry = self.hass.config_entries.async_get_known_entry( + self._reconfigure_entry_id + ) + subentry_id = self._reconfigure_subentry_id + if subentry_id not in entry.subentries: + raise UnknownEntry + return entry.subentries[subentry_id] + class OptionsFlowManager( data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult], diff --git a/tests/common.py b/tests/common.py index d2b0dff8faa..c0cfd454d10 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1090,6 +1090,28 @@ class MockConfigEntry(config_entries.ConfigEntry): }, ) + async def start_subentry_reconfigure_flow( + self, + hass: HomeAssistant, + subentry_flow_type: str, + subentry_id: str, + *, + show_advanced_options: bool = False, + ) -> ConfigFlowResult: + """Start a subnetry reconfiguration flow.""" + if self.entry_id not in hass.config_entries._entries: + raise ValueError( + "Config entry must be added to hass to start reconfiguration flow" + ) + return await hass.config_entries.subentries.async_init( + (self.entry_id, subentry_flow_type), + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "subentry_id": subentry_id, + "show_advanced_options": show_advanced_options, + }, + ) + async def start_reauth_flow( hass: HomeAssistant, diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 0a1ffbe87b3..06e19990c62 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -143,7 +143,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "reason": None, "source": "bla", "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": True, "supports_reconfigure": False, "supports_remove_device": False, @@ -163,7 +163,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "reason": "Unsupported API", "source": "bla2", "state": core_ce.ConfigEntryState.SETUP_ERROR.value, - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -183,7 +183,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "reason": None, "source": "bla3", "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -203,7 +203,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "reason": None, "source": "bla4", "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -223,7 +223,7 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "reason": None, "source": "bla5", "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -587,7 +587,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None: "reason": None, "source": core_ce.SOURCE_USER, "state": core_ce.ConfigEntryState.LOADED.value, - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -673,7 +673,7 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None: "reason": None, "source": core_ce.SOURCE_USER, "state": core_ce.ConfigEntryState.LOADED.value, - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1108,28 +1108,25 @@ async def test_subentry_flow(hass: HomeAssistant, client) -> None: """Test we can start a subentry flow.""" class TestFlow(core_ce.ConfigFlow): - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - class SubentryFlowHandler(core_ce.ConfigSubentryFlow): - async def async_step_init(self, user_input=None): - raise NotImplementedError + class SubentryFlowHandler(core_ce.ConfigSubentryFlow): + async def async_step_init(self, user_input=None): + raise NotImplementedError - async def async_step_user(self, user_input=None): - schema = OrderedDict() - schema[vol.Required("enabled")] = bool - return self.async_show_form( - step_id="user", - data_schema=schema, - description_placeholders={"enabled": "Set to true to be true"}, - ) - - return SubentryFlowHandler() + async def async_step_user(self, user_input=None): + schema = OrderedDict() + schema[vol.Required("enabled")] = bool + return self.async_show_form( + step_id="user", + data_schema=schema, + description_placeholders={"enabled": "Set to true to be true"}, + ) @classmethod @callback - def async_supported_subentries(cls, config_entry): - return ("test",) + def async_get_supported_subentry_flows( + cls, config_entry: core_ce.ConfigEntry + ) -> dict[str, type[core_ce.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} mock_integration(hass, MockModule("test")) mock_platform(hass, "test.config_flow", None) @@ -1160,6 +1157,69 @@ async def test_subentry_flow(hass: HomeAssistant, client) -> None: } +async def test_subentry_reconfigure_flow(hass: HomeAssistant, client) -> None: + """Test we can start a subentry reconfigure flow.""" + + class TestFlow(core_ce.ConfigFlow): + class SubentryFlowHandler(core_ce.ConfigSubentryFlow): + async def async_step_init(self, user_input=None): + raise NotImplementedError + + async def async_step_user(self, user_input=None): + raise NotImplementedError + + async def async_step_reconfigure(self, user_input=None): + schema = OrderedDict() + schema[vol.Required("enabled")] = bool + return self.async_show_form( + step_id="reconfigure", + data_schema=schema, + description_placeholders={"enabled": "Set to true to be true"}, + ) + + @classmethod + @callback + def async_get_supported_subentry_flows( + cls, config_entry: core_ce.ConfigEntry + ) -> dict[str, type[core_ce.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + mock_integration(hass, MockModule("test")) + mock_platform(hass, "test.config_flow", None) + MockConfigEntry( + domain="test", + entry_id="test1", + source="bla", + subentries_data=[ + core_ce.ConfigSubentryData( + data={}, subentry_id="mock_id", title="Title", unique_id=None + ) + ], + ).add_to_hass(hass) + entry = hass.config_entries.async_entries()[0] + + with patch.dict(HANDLERS, {"test": TestFlow}): + url = "/api/config/config_entries/subentries/flow" + resp = await client.post( + url, json={"handler": [entry.entry_id, "test"], "subentry_id": "mock_id"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + data.pop("flow_id") + assert data == { + "type": "form", + "handler": ["test1", "test"], + "step_id": "reconfigure", + "data_schema": [{"name": "enabled", "required": True, "type": "boolean"}], + "description_placeholders": {"enabled": "Set to true to be true"}, + "errors": None, + "last_step": None, + "preview": None, + } + + @pytest.mark.parametrize( ("endpoint", "method"), [ @@ -1174,25 +1234,22 @@ async def test_subentry_flow_unauth( """Test unauthorized on subentry flow.""" class TestFlow(core_ce.ConfigFlow): - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - class SubentryFlowHandler(core_ce.ConfigSubentryFlow): - async def async_step_init(self, user_input=None): - schema = OrderedDict() - schema[vol.Required("enabled")] = bool - return self.async_show_form( - step_id="user", - data_schema=schema, - description_placeholders={"enabled": "Set to true to be true"}, - ) - - return SubentryFlowHandler() + class SubentryFlowHandler(core_ce.ConfigSubentryFlow): + async def async_step_init(self, user_input=None): + schema = OrderedDict() + schema[vol.Required("enabled")] = bool + return self.async_show_form( + step_id="user", + data_schema=schema, + description_placeholders={"enabled": "Set to true to be true"}, + ) @classmethod @callback - def async_supported_subentries(cls, config_entry): - return ("test",) + def async_get_supported_subentry_flows( + cls, config_entry: core_ce.ConfigEntry + ) -> dict[str, type[core_ce.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} mock_integration(hass, MockModule("test")) mock_platform(hass, "test.config_flow", None) @@ -1219,29 +1276,26 @@ async def test_two_step_subentry_flow(hass: HomeAssistant, client) -> None: mock_platform(hass, "test.config_flow", None) class TestFlow(core_ce.ConfigFlow): - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - class SubentryFlowHandler(core_ce.ConfigSubentryFlow): - async def async_step_user(self, user_input=None): - return await self.async_step_finish() + class SubentryFlowHandler(core_ce.ConfigSubentryFlow): + async def async_step_user(self, user_input=None): + return await self.async_step_finish() - async def async_step_finish(self, user_input=None): - if user_input: - return self.async_create_entry( - title="Mock title", data=user_input, unique_id="test" - ) - - return self.async_show_form( - step_id="finish", data_schema=vol.Schema({"enabled": bool}) + async def async_step_finish(self, user_input=None): + if user_input: + return self.async_create_entry( + title="Mock title", data=user_input, unique_id="test" ) - return SubentryFlowHandler() + return self.async_show_form( + step_id="finish", data_schema=vol.Schema({"enabled": bool}) + ) @classmethod @callback - def async_supported_subentries(cls, config_entry): - return ("test",) + def async_get_supported_subentry_flows( + cls, config_entry: core_ce.ConfigEntry + ) -> dict[str, type[core_ce.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} MockConfigEntry( domain="test", @@ -1300,33 +1354,28 @@ async def test_subentry_flow_with_invalid_data(hass: HomeAssistant, client) -> N mock_platform(hass, "test.config_flow", None) class TestFlow(core_ce.ConfigFlow): - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - class SubentryFlowHandler(core_ce.ConfigSubentryFlow): - async def async_step_user(self, user_input=None): - return self.async_show_form( - step_id="finish", - data_schema=vol.Schema( - { - vol.Required( - "choices", default=["invalid", "valid"] - ): cv.multi_select({"valid": "Valid"}) - } - ), - ) + class SubentryFlowHandler(core_ce.ConfigSubentryFlow): + async def async_step_user(self, user_input=None): + return self.async_show_form( + step_id="finish", + data_schema=vol.Schema( + { + vol.Required( + "choices", default=["invalid", "valid"] + ): cv.multi_select({"valid": "Valid"}) + } + ), + ) - async def async_step_finish(self, user_input=None): - return self.async_create_entry( - title="Enable disable", data=user_input - ) - - return SubentryFlowHandler() + async def async_step_finish(self, user_input=None): + return self.async_create_entry(title="Enable disable", data=user_input) @classmethod @callback - def async_supported_subentries(cls, config_entry): - return ("test",) + def async_get_supported_subentry_flows( + cls, config_entry: core_ce.ConfigEntry + ) -> dict[str, type[core_ce.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} MockConfigEntry( domain="test", @@ -1409,7 +1458,7 @@ async def test_get_single( "reason": None, "source": "user", "state": "loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1771,7 +1820,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1792,7 +1841,7 @@ async def test_get_matching_entries_ws( "reason": "Unsupported API", "source": "bla2", "state": "setup_error", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1813,7 +1862,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1834,7 +1883,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla4", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1855,7 +1904,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla5", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1887,7 +1936,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1918,7 +1967,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla4", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1939,7 +1988,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla5", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1970,7 +2019,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -1991,7 +2040,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2028,7 +2077,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2049,7 +2098,7 @@ async def test_get_matching_entries_ws( "reason": "Unsupported API", "source": "bla2", "state": "setup_error", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2070,7 +2119,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2091,7 +2140,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla4", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2112,7 +2161,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla5", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2221,7 +2270,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2245,7 +2294,7 @@ async def test_subscribe_entries_ws( "reason": "Unsupported API", "source": "bla2", "state": "setup_error", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2269,7 +2318,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2299,7 +2348,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2330,7 +2379,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2360,7 +2409,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2452,7 +2501,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2476,7 +2525,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla3", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2508,7 +2557,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2536,7 +2585,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla3", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2568,7 +2617,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, @@ -2598,7 +2647,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", - "supported_subentries": [], + "supported_subentry_flows": {}, "supports_options": False, "supports_reconfigure": False, "supports_remove_device": False, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1ad152e8e42..e5a0b13d03d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1959,7 +1959,7 @@ async def test_create_entry_subentries( entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 - assert entries[0].supported_subentries == () + assert entries[0].supported_subentry_flows == {} assert entries[0].data == {"example": "data"} assert len(entries[0].subentries) == 1 subentry_id = list(entries[0].subentries)[0] @@ -1984,22 +1984,15 @@ async def test_entry_subentry( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(data_entry_flow.FlowHandler): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_flows( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2029,7 +2022,9 @@ async def test_entry_subentry( unique_id="test", ) } - assert entry.supported_subentries == ("test",) + assert entry.supported_subentry_flows == { + "test": {"supports_reconfigure": False} + } async def test_entry_subentry_non_string( @@ -2044,22 +2039,15 @@ async def test_entry_subentry_non_string( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(data_entry_flow.FlowHandler): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_flows( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2093,22 +2081,15 @@ async def test_entry_subentry_no_context( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(data_entry_flow.FlowHandler): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_flows( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow), pytest.raises(KeyError): await manager.subentries.async_create_flow( @@ -2146,22 +2127,15 @@ async def test_entry_subentry_duplicate( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(data_entry_flow.FlowHandler): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_flows( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2194,22 +2168,15 @@ async def test_entry_subentry_abort( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(data_entry_flow.FlowHandler): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_flows( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2248,22 +2215,15 @@ async def test_entry_subentry_deleted_config_entry( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(data_entry_flow.FlowHandler): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_flows( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2286,7 +2246,7 @@ async def test_entry_subentry_deleted_config_entry( ) -async def test_entry_subentry_unsupported( +async def test_entry_subentry_unsupported_subentry_type( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: """Test attempting to start a subentry flow for a config entry without support.""" @@ -2298,22 +2258,15 @@ async def test_entry_subentry_unsupported( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(data_entry_flow.FlowHandler): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_flows( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with ( mock_config_flow("test", TestFlow), @@ -2329,7 +2282,7 @@ async def test_entry_subentry_unsupported( ) -async def test_entry_subentry_unsupported_subentry_type( +async def test_entry_subentry_unsupported( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: """Test attempting to start a subentry flow for a config entry without support."""