From cddb3bb668328b3ee0dac0dbf89d54539d33c7e9 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 31 Jul 2024 15:08:25 +0200 Subject: [PATCH] Add reconfigure step for here_travel_time (#114667) * Add reconfigure step for here_travel_time * Add comments, reuse step_user, TYPE_CHECKING, remove defaults --- .../here_travel_time/config_flow.py | 183 +++++++++++++----- .../components/here_travel_time/strings.json | 3 +- .../here_travel_time/test_config_flow.py | 102 ++++++++++ 3 files changed, 235 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index 36d5c1efe1e..b708fd9cd3d 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any from here_routing import ( HERERoutingApi, @@ -104,6 +104,8 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Init Config Flow.""" self._config: dict[str, Any] = {} + self._entry: ConfigEntry | None = None + self._is_reconfigure_flow: bool = False @staticmethod @callback @@ -119,21 +121,36 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} user_input = user_input or {} - if user_input: - try: - await async_validate_api_key(user_input[CONF_API_KEY]) - except HERERoutingUnauthorizedError: - errors["base"] = "invalid_auth" - except (HERERoutingError, HERETransitError): - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - if not errors: - self._config = user_input - return await self.async_step_origin_menu() + if not self._is_reconfigure_flow: # Always show form first for reconfiguration + if user_input: + try: + await async_validate_api_key(user_input[CONF_API_KEY]) + except HERERoutingUnauthorizedError: + errors["base"] = "invalid_auth" + except (HERERoutingError, HERETransitError): + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + if not errors: + self._config[CONF_NAME] = user_input[CONF_NAME] + self._config[CONF_API_KEY] = user_input[CONF_API_KEY] + self._config[CONF_MODE] = user_input[CONF_MODE] + return await self.async_step_origin_menu() + self._is_reconfigure_flow = False return self.async_show_form( step_id="user", data_schema=get_user_step_schema(user_input), errors=errors ) + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration.""" + self._is_reconfigure_flow = True + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if TYPE_CHECKING: + assert self._entry + self._config = self._entry.data.copy() + return await self.async_step_user(self._config) + async def async_step_origin_menu(self, _: None = None) -> ConfigFlowResult: """Show the origin menu.""" return self.async_show_menu( @@ -150,37 +167,57 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): self._config[CONF_ORIGIN_LONGITUDE] = user_input[CONF_ORIGIN][ CONF_LONGITUDE ] + # Remove possible previous configuration using an entity_id + self._config.pop(CONF_ORIGIN_ENTITY_ID, None) return await self.async_step_destination_menu() - schema = vol.Schema( + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_ORIGIN, + ): LocationSelector() + } + ), { - vol.Required( - CONF_ORIGIN, - default={ - CONF_LATITUDE: self.hass.config.latitude, - CONF_LONGITUDE: self.hass.config.longitude, - }, - ): LocationSelector() - } + CONF_ORIGIN: { + CONF_LATITUDE: self._config.get(CONF_ORIGIN_LATITUDE) + or self.hass.config.latitude, + CONF_LONGITUDE: self._config.get(CONF_ORIGIN_LONGITUDE) + or self.hass.config.longitude, + } + }, ) return self.async_show_form(step_id="origin_coordinates", data_schema=schema) - async def async_step_destination_menu(self, _: None = None) -> ConfigFlowResult: - """Show the destination menu.""" - return self.async_show_menu( - step_id="destination_menu", - menu_options=["destination_coordinates", "destination_entity"], - ) - async def async_step_origin_entity( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Configure origin by using an entity.""" if user_input is not None: self._config[CONF_ORIGIN_ENTITY_ID] = user_input[CONF_ORIGIN_ENTITY_ID] + # Remove possible previous configuration using coordinates + self._config.pop(CONF_ORIGIN_LATITUDE, None) + self._config.pop(CONF_ORIGIN_LONGITUDE, None) return await self.async_step_destination_menu() - schema = vol.Schema({vol.Required(CONF_ORIGIN_ENTITY_ID): EntitySelector()}) + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_ORIGIN_ENTITY_ID, + ): EntitySelector() + } + ), + {CONF_ORIGIN_ENTITY_ID: self._config.get(CONF_ORIGIN_ENTITY_ID)}, + ) return self.async_show_form(step_id="origin_entity", data_schema=schema) + async def async_step_destination_menu(self, _: None = None) -> ConfigFlowResult: + """Show the destination menu.""" + return self.async_show_menu( + step_id="destination_menu", + menu_options=["destination_coordinates", "destination_entity"], + ) + async def async_step_destination_coordinates( self, user_input: dict[str, Any] | None = None, @@ -193,21 +230,36 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): self._config[CONF_DESTINATION_LONGITUDE] = user_input[CONF_DESTINATION][ CONF_LONGITUDE ] + # Remove possible previous configuration using an entity_id + self._config.pop(CONF_DESTINATION_ENTITY_ID, None) + if self._entry: + return self.async_update_reload_and_abort( + self._entry, + title=self._config[CONF_NAME], + data=self._config, + reason="reconfigure_successful", + ) return self.async_create_entry( title=self._config[CONF_NAME], data=self._config, options=DEFAULT_OPTIONS, ) - schema = vol.Schema( + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_DESTINATION, + ): LocationSelector() + } + ), { - vol.Required( - CONF_DESTINATION, - default={ - CONF_LATITUDE: self.hass.config.latitude, - CONF_LONGITUDE: self.hass.config.longitude, - }, - ): LocationSelector() - } + CONF_DESTINATION: { + CONF_LATITUDE: self._config.get(CONF_DESTINATION_LATITUDE) + or self.hass.config.latitude, + CONF_LONGITUDE: self._config.get(CONF_DESTINATION_LONGITUDE) + or self.hass.config.longitude, + }, + }, ) return self.async_show_form( step_id="destination_coordinates", data_schema=schema @@ -222,13 +274,27 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): self._config[CONF_DESTINATION_ENTITY_ID] = user_input[ CONF_DESTINATION_ENTITY_ID ] + # Remove possible previous configuration using coordinates + self._config.pop(CONF_DESTINATION_LATITUDE, None) + self._config.pop(CONF_DESTINATION_LONGITUDE, None) + if self._entry: + return self.async_update_reload_and_abort( + self._entry, data=self._config, reason="reconfigure_successful" + ) return self.async_create_entry( title=self._config[CONF_NAME], data=self._config, options=DEFAULT_OPTIONS, ) - schema = vol.Schema( - {vol.Required(CONF_DESTINATION_ENTITY_ID): EntitySelector()} + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_DESTINATION_ENTITY_ID, + ): EntitySelector() + } + ), + {CONF_DESTINATION_ENTITY_ID: self._config.get(CONF_DESTINATION_ENTITY_ID)}, ) return self.async_show_form(step_id="destination_entity", data_schema=schema) @@ -249,15 +315,22 @@ class HERETravelTimeOptionsFlow(OptionsFlow): self._config = user_input return await self.async_step_time_menu() - schema = vol.Schema( + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Optional( + CONF_ROUTE_MODE, + default=self.config_entry.options.get( + CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE] + ), + ): vol.In(ROUTE_MODES), + } + ), { - vol.Optional( - CONF_ROUTE_MODE, - default=self.config_entry.options.get( - CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE] - ), - ): vol.In(ROUTE_MODES), - } + CONF_ROUTE_MODE: self.config_entry.options.get( + CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE] + ), + }, ) return self.async_show_form(step_id="init", data_schema=schema) @@ -283,8 +356,11 @@ class HERETravelTimeOptionsFlow(OptionsFlow): self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME] return self.async_create_entry(title="", data=self._config) - schema = vol.Schema( - {vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()} + schema = self.add_suggested_values_to_schema( + vol.Schema( + {vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()} + ), + {CONF_ARRIVAL_TIME: "00:00:00"}, ) return self.async_show_form(step_id="arrival_time", data_schema=schema) @@ -297,8 +373,11 @@ class HERETravelTimeOptionsFlow(OptionsFlow): self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME] return self.async_create_entry(title="", data=self._config) - schema = vol.Schema( - {vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()} + schema = self.add_suggested_values_to_schema( + vol.Schema( + {vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()} + ), + {CONF_DEPARTURE_TIME: "00:00:00"}, ) return self.async_show_form(step_id="departure_time", data_schema=schema) diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json index 124aa070595..cfa14a3e3ca 100644 --- a/homeassistant/components/here_travel_time/strings.json +++ b/homeassistant/components/here_travel_time/strings.json @@ -52,7 +52,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "options": { diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 9b15a42dd56..ea3de64ed0c 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -6,17 +6,20 @@ from here_routing import HERERoutingError, HERERoutingUnauthorizedError import pytest from homeassistant import config_entries +from homeassistant.components.here_travel_time.config_flow import DEFAULT_OPTIONS from homeassistant.components.here_travel_time.const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, CONF_DESTINATION_LATITUDE, CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, DOMAIN, ROUTE_MODE_FASTEST, + TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PUBLIC, ) @@ -249,6 +252,105 @@ async def test_step_destination_entity( } +@pytest.mark.usefixtures("valid_response") +async def test_reconfigure_destination_entity(hass: HomeAssistant) -> None: + """Test reconfigure flow when choosing a destination entity.""" + origin_entity_selector_result = await do_common_reconfiguration_steps(hass) + menu_result = await hass.config_entries.flow.async_configure( + origin_entity_selector_result["flow_id"], {"next_step_id": "destination_entity"} + ) + assert menu_result["type"] is FlowResultType.FORM + + destination_entity_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + {"destination_entity_id": "zone.home"}, + ) + assert destination_entity_selector_result["type"] is FlowResultType.ABORT + assert destination_entity_selector_result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test", + CONF_API_KEY: API_KEY, + CONF_ORIGIN_ENTITY_ID: "zone.home", + CONF_DESTINATION_ENTITY_ID: "zone.home", + CONF_MODE: TRAVEL_MODE_BICYCLE, + } + + +@pytest.mark.usefixtures("valid_response") +async def test_reconfigure_destination_coordinates(hass: HomeAssistant) -> None: + """Test reconfigure flow when choosing destination coordinates.""" + origin_entity_selector_result = await do_common_reconfiguration_steps(hass) + menu_result = await hass.config_entries.flow.async_configure( + origin_entity_selector_result["flow_id"], + {"next_step_id": "destination_coordinates"}, + ) + assert menu_result["type"] is FlowResultType.FORM + + destination_entity_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + { + "destination": { + "latitude": 43.0, + "longitude": -80.3, + "radius": 5.0, + } + }, + ) + assert destination_entity_selector_result["type"] is FlowResultType.ABORT + assert destination_entity_selector_result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test", + CONF_API_KEY: API_KEY, + CONF_ORIGIN_ENTITY_ID: "zone.home", + CONF_DESTINATION_LATITUDE: 43.0, + CONF_DESTINATION_LONGITUDE: -80.3, + CONF_MODE: TRAVEL_MODE_BICYCLE, + } + + +async def do_common_reconfiguration_steps(hass: HomeAssistant) -> None: + """Walk through common flow steps for reconfiguring.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data=DEFAULT_CONFIG, + options=DEFAULT_OPTIONS, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + reconfigure_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "user" + + user_step_result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + { + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_BICYCLE, + CONF_NAME: "test", + }, + ) + await hass.async_block_till_done() + menu_result = await hass.config_entries.flow.async_configure( + user_step_result["flow_id"], {"next_step_id": "origin_entity"} + ) + return await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + {"origin_entity_id": "zone.home"}, + ) + + async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init(