diff --git a/.coveragerc b/.coveragerc index 0eec1c3a0a6..506f8024d5e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1272,6 +1272,7 @@ omit = homeassistant/components/tradfri/light.py homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/switch.py + homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py diff --git a/CODEOWNERS b/CODEOWNERS index 8bfaa2c5c8c..521f5268885 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1048,6 +1048,7 @@ build.json @home-assistant/supervisor /homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu /tests/components/tractive/ @Danielhiversen @zhulik @bieniu /homeassistant/components/trafikverket_train/ @endor-force @gjohansson-ST +/tests/components/trafikverket_train/ @endor-force @gjohansson-ST /homeassistant/components/trafikverket_weatherstation/ @endor-force @gjohansson-ST /tests/components/trafikverket_weatherstation/ @endor-force @gjohansson-ST /homeassistant/components/transmission/ @engrbm87 @JPHutchins @@ -1201,4 +1202,4 @@ build.json @home-assistant/supervisor /homeassistant/components/demo/weather.py @fabaff # Remove codeowners from files -/homeassistant/components/*/translations/ +/homeassistant/components/*/translations/ \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index 3adcec068da..4411ccab948 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -1 +1,21 @@ """The trafikverket_train component.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Trafikverket Train from a config entry.""" + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Trafikverket Weatherstation config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py new file mode 100644 index 00000000000..79c5978de2c --- /dev/null +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -0,0 +1,164 @@ +"""Adds config flow for Trafikverket Train integration.""" +from __future__ import annotations + +from typing import Any + +from pytrafikverket import TrafikverketTrain +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + +from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN +from .util import create_unique_id + +ERROR_INVALID_AUTH = "Source: Security, message: Invalid authentication" +ERROR_INVALID_STATION = "Could not find a station with the specified name" +ERROR_MULTIPLE_STATION = "Found multiple stations with the specified name" + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_FROM): cv.string, + vol.Required(CONF_TO): cv.string, + vol.Optional(CONF_TIME): cv.string, + vol.Required(CONF_WEEKDAY, default=WEEKDAYS): cv.multi_select( + {day: day for day in WEEKDAYS} + ), + } +) +DATA_SCHEMA_REAUTH = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + } +) + + +class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Trafikverket Train integration.""" + + VERSION = 1 + + entry: config_entries.ConfigEntry | None + + async def validate_input( + self, api_key: str, train_from: str, train_to: str + ) -> None: + """Validate input from user input.""" + web_session = async_get_clientsession(self.hass) + train_api = TrafikverketTrain(web_session, api_key) + await train_api.async_get_train_station(train_from) + await train_api.async_get_train_station(train_to) + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Trafikverket.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Trafikverket.""" + errors: dict[str, str] = {} + + if user_input: + api_key = user_input[CONF_API_KEY] + + assert self.entry is not None + try: + await self.validate_input( + api_key, self.entry.data[CONF_FROM], self.entry.data[CONF_TO] + ) + except ValueError as err: + if str(err) == ERROR_INVALID_AUTH: + errors["base"] = "invalid_auth" + elif str(err) == ERROR_INVALID_STATION: + errors["base"] = "invalid_station" + elif str(err) == ERROR_MULTIPLE_STATION: + errors["base"] = "more_stations" + else: + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_KEY: api_key, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA_REAUTH, + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any] | None) -> FlowResult: + """Import a configuration from config.yaml.""" + + return await self.async_step_user(user_input=config) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step.""" + errors: dict[str, str] = {} + + if user_input is not None: + api_key: str = user_input[CONF_API_KEY] + train_from: str = user_input[CONF_FROM] + train_to: str = user_input[CONF_TO] + train_time: str | None = user_input.get(CONF_TIME) + train_days: list = user_input[CONF_WEEKDAY] + + name = f"{train_from} to {train_to}" + if train_time: + name = f"{train_from} to {train_to} at {train_time}" + + try: + await self.validate_input(api_key, train_from, train_to) + except ValueError as err: + if str(err) == ERROR_INVALID_AUTH: + errors["base"] = "invalid_auth" + elif str(err) == ERROR_INVALID_STATION: + errors["base"] = "invalid_station" + elif str(err) == ERROR_MULTIPLE_STATION: + errors["base"] = "more_stations" + else: + errors["base"] = "cannot_connect" + else: + if train_time: + if bool(dt_util.parse_time(train_time) is None): + errors["base"] = "invalid_time" + if not errors: + unique_id = create_unique_id( + train_from, train_to, train_time, train_days + ) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_API_KEY: api_key, + CONF_NAME: name, + CONF_FROM: train_from, + CONF_TO: train_to, + CONF_TIME: train_time, + CONF_WEEKDAY: train_days, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/trafikverket_train/const.py b/homeassistant/components/trafikverket_train/const.py new file mode 100644 index 00000000000..f0a6a1d6a18 --- /dev/null +++ b/homeassistant/components/trafikverket_train/const.py @@ -0,0 +1,11 @@ +"""Adds constants for Trafikverket Train integration.""" +from homeassistant.const import Platform + +DOMAIN = "trafikverket_train" +PLATFORMS = [Platform.SENSOR] +ATTRIBUTION = "Data provided by Trafikverket" + +CONF_TRAINS = "trains" +CONF_FROM = "from" +CONF_TO = "to" +CONF_TIME = "time" diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index da1d4de6c13..b3bf23ce5c4 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "requirements": ["pytrafikverket==0.1.6.2"], "codeowners": ["@endor-force", "@gjohansson-ST"], + "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pytrafikverket"] } diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index ef50bcc9229..82f248f9de0 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -14,21 +14,23 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.dt import as_utc, get_time_zone +from homeassistant.util.dt import as_utc, get_time_zone, parse_time + +from .const import CONF_FROM, CONF_TIME, CONF_TO, CONF_TRAINS, DOMAIN +from .util import create_unique_id _LOGGER = logging.getLogger(__name__) -CONF_TRAINS = "trains" -CONF_FROM = "from" -CONF_TO = "to" -CONF_TIME = "time" - ATTR_DEPARTURE_STATE = "departure_state" ATTR_CANCELED = "canceled" ATTR_DELAY_TIME = "number_of_minutes_delayed" @@ -66,43 +68,66 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the departure sensor.""" - httpsession = async_get_clientsession(hass) - train_api = TrafikverketTrain(httpsession, config[CONF_API_KEY]) - sensors = [] - station_cache = {} + """Import Trafikverket Train configuration from YAML.""" + _LOGGER.warning( + # Config flow added in Home Assistant Core 2022.3, remove import flow in 2022.7 + "Loading Trafikverket Train via platform setup is deprecated; Please remove it from your configuration" + ) + for train in config[CONF_TRAINS]: - try: - trainstops = [train[CONF_FROM], train[CONF_TO]] - for station in trainstops: - if station not in station_cache: - station_cache[station] = await train_api.async_get_train_station( - station - ) - except ValueError as station_error: - if "Invalid authentication" in station_error.args[0]: - _LOGGER.error("Unable to set up up component: %s", station_error) - return - _LOGGER.error( - "Problem when trying station %s to %s. Error: %s ", - train[CONF_FROM], - train[CONF_TO], - station_error, + new_config = { + CONF_API_KEY: config[CONF_API_KEY], + CONF_FROM: train[CONF_FROM], + CONF_TO: train[CONF_TO], + CONF_TIME: str(train.get(CONF_TIME)), + CONF_WEEKDAY: train.get(CONF_WEEKDAY, WEEKDAYS), + } + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=new_config, ) - continue - - sensor = TrainSensor( - train_api, - train[CONF_NAME], - station_cache[train[CONF_FROM]], - station_cache[train[CONF_TO]], - train[CONF_WEEKDAY], - train.get(CONF_TIME), ) - sensors.append(sensor) - async_add_entities(sensors, update_before_add=True) + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Trafikverket sensor entry.""" + + httpsession = async_get_clientsession(hass) + train_api = TrafikverketTrain(httpsession, entry.data[CONF_API_KEY]) + + try: + to_station = await train_api.async_get_train_station(entry.data[CONF_TO]) + from_station = await train_api.async_get_train_station(entry.data[CONF_FROM]) + except ValueError as error: + if "Invalid authentication" in error.args[0]: + raise ConfigEntryAuthFailed from error + raise ConfigEntryNotReady( + f"Problem when trying station {entry.data[CONF_FROM]} to {entry.data[CONF_TO]}. Error: {error} " + ) from error + + train_time = ( + parse_time(entry.data.get(CONF_TIME, "")) if entry.data.get(CONF_TIME) else None + ) + + async_add_entities( + [ + TrainSensor( + train_api, + entry.data[CONF_NAME], + from_station, + to_station, + entry.data[CONF_WEEKDAY], + train_time, + entry.entry_id, + ) + ], + True, + ) def next_weekday(fromdate: date, weekday: int) -> date: @@ -144,7 +169,8 @@ class TrainSensor(SensorEntity): from_station: str, to_station: str, weekday: list, - departuretime: time, + departuretime: time | None, + entry_id: str, ) -> None: """Initialize the sensor.""" self._train_api = train_api @@ -153,6 +179,17 @@ class TrainSensor(SensorEntity): self._to_station = to_station self._weekday = weekday self._time = departuretime + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="Trafikverket", + model="v1.2", + name=name, + configuration_url="https://api.trafikinfo.trafikverket.se/", + ) + self._attr_unique_id = create_unique_id( + from_station, to_station, departuretime, weekday + ) async def async_update(self) -> None: """Retrieve latest state.""" diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json new file mode 100644 index 00000000000..1224210482e --- /dev/null +++ b/homeassistant/components/trafikverket_train/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_station": "Could not find a station with the specified name", + "more_stations": "Found multiple stations with the specified name", + "invalid_time": "Invalid time provided", + "incorrect_api_key": "Invalid API key for selected account" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "to": "To station", + "from": "From station", + "time": "Time (optional)", + "weekday": "Days" + } + }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/en.json b/homeassistant/components/trafikverket_train/translations/en.json new file mode 100644 index 00000000000..dc91c2097f3 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_station": "Could not find a station with the specified name", + "more_stations": "Found multiple stations with the specified name", + "invalid_time": "Invalid time provided", + "incorrect_api_key": "Invalid API key for selected account" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "to": "To station", + "from": "From station", + "time": "Time (optional)", + "weekday": "Days" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/util.py b/homeassistant/components/trafikverket_train/util.py new file mode 100644 index 00000000000..6ed672c9e7e --- /dev/null +++ b/homeassistant/components/trafikverket_train/util.py @@ -0,0 +1,15 @@ +"""Utils for trafikverket_train.""" +from __future__ import annotations + +from datetime import time + + +def create_unique_id( + from_station: str, to_station: str, depart_time: time | str | None, weekdays: list +) -> str: + """Create unique id.""" + timestr = str(depart_time) if depart_time else "" + return ( + f"{from_station.casefold().replace(' ', '')}-{to_station.casefold().replace(' ', '')}" + f"-{timestr.casefold().replace(' ', '')}-{str(weekdays)}" + ) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2f8339f4fe5..1bbe51fcadf 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -348,6 +348,7 @@ FLOWS = { "traccar", "tractive", "tradfri", + "trafikverket_train", "trafikverket_weatherstation", "transmission", "tuya", diff --git a/tests/components/trafikverket_train/__init__.py b/tests/components/trafikverket_train/__init__.py new file mode 100644 index 00000000000..060b6a344a1 --- /dev/null +++ b/tests/components/trafikverket_train/__init__.py @@ -0,0 +1 @@ +"""Tests for the Trafikverket Train integration.""" diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py new file mode 100644 index 00000000000..b8e3548af2e --- /dev/null +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -0,0 +1,354 @@ +"""Test the Trafikverket Train config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.trafikverket_train.const import ( + CONF_FROM, + CONF_TIME, + CONF_TO, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: ["mon", "fri"], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Stockholm C to Uppsala C at 10:00" + assert result2["data"] == { + "api_key": "1234567890", + "name": "Stockholm C to Uppsala C at 10:00", + "from": "Stockholm C", + "to": "Uppsala C", + "time": "10:00", + "weekday": ["mon", "fri"], + } + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == "{}-{}-{}-{}".format( + "stockholmc", "uppsalac", "10:00", "['mon', 'fri']" + ) + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: ["mon", "fri"], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Stockholm C to Uppsala C at 10:00" + assert result2["data"] == { + "api_key": "1234567890", + "name": "Stockholm C to Uppsala C at 10:00", + "from": "Stockholm C", + "to": "Uppsala C", + "time": "10:00", + "weekday": ["mon", "fri"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_NAME: "Stockholm C to Uppsala C", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + unique_id=f"stockholmc-uppsalac-10:00-{WEEKDAYS}", + ).add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Stockholm C to Uppsala C", + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "error_message,base_error", + [ + ( + "Source: Security, message: Invalid authentication", + "invalid_auth", + ), + ( + "Could not find a station with the specified name", + "invalid_station", + ), + ( + "Found multiple stations with the specified name", + "more_stations", + ), + ( + "Unknown", + "cannot_connect", + ), + ], +) +async def test_flow_fails( + hass: HomeAssistant, error_message: str, base_error: str +) -> None: + """Test config flow errors.""" + result4 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result4["type"] == RESULT_TYPE_FORM + assert result4["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + side_effect=ValueError(error_message), + ): + result4 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + }, + ) + + assert result4["errors"] == {"base": base_error} + + +async def test_flow_fails_incorrect_time(hass: HomeAssistant) -> None: + """Test config flow errors due to bad time.""" + result5 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result5["type"] == RESULT_TYPE_FORM + assert result5["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ): + result6 = await hass.config_entries.flow.async_configure( + result5["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "25:25", + }, + ) + + assert result6["errors"] == {"base": "invalid_time"} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_NAME: "Stockholm C to Uppsala C at 10:00", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + unique_id=f"stockholmc-uppsalac-10:00-{WEEKDAYS}", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567891"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == { + "api_key": "1234567891", + "name": "Stockholm C to Uppsala C at 10:00", + "from": "Stockholm C", + "to": "Uppsala C", + "time": "10:00", + "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], + } + + +@pytest.mark.parametrize( + "sideeffect,p_error", + [ + ( + ValueError("Source: Security, message: Invalid authentication"), + "invalid_auth", + ), + ( + ValueError("Could not find a station with the specified name"), + "invalid_station", + ), + ( + ValueError("Found multiple stations with the specified name"), + "more_stations", + ), + ( + ValueError("Unknown"), + "cannot_connect", + ), + ], +) +async def test_reauth_flow_error( + hass: HomeAssistant, sideeffect: Exception, p_error: str +) -> None: + """Test a reauthentication flow with error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_NAME: "Stockholm C to Uppsala C at 10:00", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + unique_id=f"stockholmc-uppsalac-10:00-{WEEKDAYS}", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + side_effect=sideeffect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": p_error} + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567891"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == { + "api_key": "1234567891", + "name": "Stockholm C to Uppsala C at 10:00", + "from": "Stockholm C", + "to": "Uppsala C", + "time": "10:00", + "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], + }