diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py
index 0ad344e3fdf..310fe97fad8 100644
--- a/homeassistant/components/here_travel_time/__init__.py
+++ b/homeassistant/components/here_travel_time/__init__.py
@@ -8,7 +8,15 @@ import async_timeout
from herepy import NoRouteFoundError, RouteMode, RoutingApi, RoutingResponse
import voluptuous as vol
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, Platform
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ CONF_API_KEY,
+ CONF_MODE,
+ CONF_UNIT_SYSTEM,
+ CONF_UNIT_SYSTEM_IMPERIAL,
+ Platform,
+)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.location import find_coordinates
@@ -24,9 +32,20 @@ from .const import (
ATTR_ORIGIN,
ATTR_ORIGIN_NAME,
ATTR_ROUTE,
+ 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,
+ CONF_TRAFFIC_MODE,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
NO_ROUTE_ERROR_MESSAGE,
+ ROUTE_MODE_FASTEST,
TRAFFIC_MODE_ENABLED,
TRAVEL_MODES_VEHICLE,
)
@@ -37,6 +56,74 @@ PLATFORMS = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Set up HERE Travel Time from a config entry."""
+ api_key = config_entry.data[CONF_API_KEY]
+ here_client = RoutingApi(api_key)
+ setup_options(hass, config_entry)
+
+ arrival = (
+ dt.parse_time(config_entry.options[CONF_ARRIVAL_TIME])
+ if config_entry.options[CONF_ARRIVAL_TIME] is not None
+ else None
+ )
+ departure = (
+ dt.parse_time(config_entry.options[CONF_DEPARTURE_TIME])
+ if config_entry.options[CONF_DEPARTURE_TIME] is not None
+ else None
+ )
+
+ here_travel_time_config = HERETravelTimeConfig(
+ destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE),
+ destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE),
+ destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID),
+ origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE),
+ origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE),
+ origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID),
+ travel_mode=config_entry.data[CONF_MODE],
+ route_mode=config_entry.options[CONF_ROUTE_MODE],
+ units=config_entry.options[CONF_UNIT_SYSTEM],
+ arrival=arrival,
+ departure=departure,
+ )
+
+ coordinator = HereTravelTimeDataUpdateCoordinator(
+ hass,
+ here_client,
+ here_travel_time_config,
+ )
+ hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
+ hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
+
+ return True
+
+
+def setup_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
+ """Set up options for a config entry if not set."""
+ if not config_entry.options:
+ hass.config_entries.async_update_entry(
+ config_entry,
+ options={
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_ARRIVAL_TIME: None,
+ CONF_DEPARTURE_TIME: None,
+ CONF_UNIT_SYSTEM: hass.config.units.name,
+ },
+ )
+
+
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ unload_ok = await hass.config_entries.async_unload_platforms(
+ config_entry, PLATFORMS
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return unload_ok
+
+
class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator):
"""HERETravelTime DataUpdateCoordinator."""
@@ -135,33 +222,40 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator):
) -> tuple[list[str], list[str], str | None, str | None]:
"""Prepare parameters for the HERE api."""
- if self.config.origin_entity_id is not None:
- origin = find_coordinates(self.hass, self.config.origin_entity_id)
- else:
- origin = self.config.origin
+ def _from_entity_id(entity_id: str) -> list[str]:
+ coordinates = find_coordinates(self.hass, entity_id)
+ if coordinates is None:
+ raise InvalidCoordinatesException(
+ f"No coordinatnes found for {entity_id}"
+ )
+ try:
+ here_formatted_coordinates = coordinates.split(",")
+ vol.Schema(cv.gps(here_formatted_coordinates))
+ except (AttributeError, vol.Invalid) as ex:
+ raise InvalidCoordinatesException(
+ f"{coordinates} are not valid coordinates"
+ ) from ex
+ return here_formatted_coordinates
+ # Destination
if self.config.destination_entity_id is not None:
- destination = find_coordinates(self.hass, self.config.destination_entity_id)
+ destination = _from_entity_id(self.config.destination_entity_id)
else:
- destination = self.config.destination
- if destination is None:
- raise InvalidCoordinatesException("Destination must be configured")
- try:
- here_formatted_destination = destination.split(",")
- vol.Schema(cv.gps(here_formatted_destination))
- except (vol.Invalid) as ex:
- raise InvalidCoordinatesException(
- f"{destination} are not valid coordinates"
- ) from ex
- if origin is None:
- raise InvalidCoordinatesException("Origin must be configured")
- try:
- here_formatted_origin = origin.split(",")
- vol.Schema(cv.gps(here_formatted_origin))
- except (AttributeError, vol.Invalid) as ex:
- raise InvalidCoordinatesException(
- f"{origin} are not valid coordinates"
- ) from ex
+ destination = [
+ str(self.config.destination_latitude),
+ str(self.config.destination_longitude),
+ ]
+
+ # Origin
+ if self.config.origin_entity_id is not None:
+ origin = _from_entity_id(self.config.origin_entity_id)
+ else:
+ origin = [
+ str(self.config.origin_latitude),
+ str(self.config.origin_longitude),
+ ]
+
+ # Arrival/Departure
arrival: str | None = None
departure: str | None = None
if self.config.arrival is not None:
@@ -172,7 +266,7 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator):
if arrival is None and departure is None:
departure = "now"
- return (here_formatted_origin, here_formatted_destination, arrival, departure)
+ return (origin, destination, arrival, departure)
def build_hass_attribution(source_attribution: dict) -> str | None:
diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py
new file mode 100644
index 00000000000..bc6d57aa892
--- /dev/null
+++ b/homeassistant/components/here_travel_time/config_flow.py
@@ -0,0 +1,369 @@
+"""Config flow for HERE Travel Time integration."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from herepy import HEREError, InvalidCredentialsError, RouteMode, RoutingApi
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM
+from homeassistant.core import callback
+from homeassistant.data_entry_flow import FlowResult
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.selector import (
+ EntitySelector,
+ LocationSelector,
+ TimeSelector,
+ selector,
+)
+
+from .const import (
+ CONF_ARRIVAL,
+ CONF_ARRIVAL_TIME,
+ CONF_DEPARTURE,
+ CONF_DEPARTURE_TIME,
+ CONF_ROUTE_MODE,
+ CONF_TRAFFIC_MODE,
+ DEFAULT_NAME,
+ DOMAIN,
+ ROUTE_MODE_FASTEST,
+ ROUTE_MODES,
+ TRAFFIC_MODE_DISABLED,
+ TRAFFIC_MODE_ENABLED,
+ TRAFFIC_MODES,
+ TRAVEL_MODE_CAR,
+ TRAVEL_MODE_PUBLIC_TIME_TABLE,
+ TRAVEL_MODES,
+ UNITS,
+)
+from .sensor import (
+ CONF_DESTINATION_ENTITY_ID,
+ CONF_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE,
+ CONF_ORIGIN_ENTITY_ID,
+ CONF_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def is_dupe_import(
+ entry: config_entries.ConfigEntry,
+ user_input: dict[str, Any],
+ options: dict[str, Any],
+) -> bool:
+ """Return whether imported config already exists."""
+ # Check the main data keys
+ if any(
+ user_input[key] != entry.data[key]
+ for key in (CONF_API_KEY, CONF_MODE, CONF_NAME)
+ ):
+ return False
+
+ # Check origin/destination
+ for key in (
+ CONF_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE,
+ CONF_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_ENTITY_ID,
+ CONF_ORIGIN_ENTITY_ID,
+ ):
+ if user_input.get(key) != entry.data.get(key):
+ return False
+
+ # We have to check for options that don't have defaults
+ for key in (
+ CONF_TRAFFIC_MODE,
+ CONF_UNIT_SYSTEM,
+ CONF_ROUTE_MODE,
+ CONF_ARRIVAL_TIME,
+ CONF_DEPARTURE_TIME,
+ ):
+ if options.get(key) != entry.options.get(key):
+ return False
+
+ return True
+
+
+def validate_api_key(api_key: str) -> None:
+ """Validate the user input allows us to connect."""
+ known_working_origin = [38.9, -77.04833]
+ known_working_destination = [39.0, -77.1]
+ RoutingApi(api_key).public_transport_timetable(
+ known_working_origin,
+ known_working_destination,
+ True,
+ [
+ RouteMode[ROUTE_MODE_FASTEST],
+ RouteMode[TRAVEL_MODE_CAR],
+ RouteMode[TRAFFIC_MODE_ENABLED],
+ ],
+ arrival=None,
+ departure="now",
+ )
+
+
+def get_user_step_schema(data: dict[str, Any]) -> vol.Schema:
+ """Get a populated schema or default."""
+ return vol.Schema(
+ {
+ vol.Optional(
+ CONF_NAME, default=data.get(CONF_NAME, DEFAULT_NAME)
+ ): cv.string,
+ vol.Required(CONF_API_KEY, default=data.get(CONF_API_KEY)): cv.string,
+ vol.Optional(
+ CONF_MODE, default=data.get(CONF_MODE, TRAVEL_MODE_CAR)
+ ): vol.In(TRAVEL_MODES),
+ }
+ )
+
+
+class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for HERE Travel Time."""
+
+ VERSION = 1
+
+ _config: dict[str, Any] = {}
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(
+ config_entry: config_entries.ConfigEntry,
+ ) -> HERETravelTimeOptionsFlow:
+ """Get the options flow."""
+ return HERETravelTimeOptionsFlow(config_entry)
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle the initial step."""
+ errors = {}
+ user_input = user_input or {}
+ if user_input:
+ try:
+ await self.hass.async_add_executor_job(
+ validate_api_key, user_input[CONF_API_KEY]
+ )
+ except InvalidCredentialsError:
+ errors["base"] = "invalid_auth"
+ except HEREError as error:
+ _LOGGER.exception("Unexpected exception: %s", error)
+ errors["base"] = "unknown"
+ if not errors:
+ self._config = user_input
+ return self.async_show_menu(
+ step_id="origin_menu",
+ menu_options=["origin_coordinates", "origin_entity"],
+ )
+ return self.async_show_form(
+ step_id="user", data_schema=get_user_step_schema(user_input), errors=errors
+ )
+
+ async def async_step_origin_coordinates(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Configure origin by using gps coordinates."""
+ if user_input is not None:
+ self._config[CONF_ORIGIN_LATITUDE] = user_input["origin"]["latitude"]
+ self._config[CONF_ORIGIN_LONGITUDE] = user_input["origin"]["longitude"]
+ return self.async_show_menu(
+ step_id="destination_menu",
+ menu_options=["destination_coordinates", "destination_entity"],
+ )
+ schema = vol.Schema({"origin": selector({LocationSelector.selector_type: {}})})
+ return self.async_show_form(step_id="origin_coordinates", data_schema=schema)
+
+ async def async_step_origin_entity(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Configure origin by using an entity."""
+ if user_input is not None:
+ self._config[CONF_ORIGIN_ENTITY_ID] = user_input[CONF_ORIGIN_ENTITY_ID]
+ return self.async_show_menu(
+ step_id="destination_menu",
+ menu_options=["destination_coordinates", "destination_entity"],
+ )
+ schema = vol.Schema(
+ {CONF_ORIGIN_ENTITY_ID: selector({EntitySelector.selector_type: {}})}
+ )
+ return self.async_show_form(step_id="origin_entity", data_schema=schema)
+
+ async def async_step_destination_coordinates(
+ self,
+ user_input: dict[str, Any] | None = None,
+ ) -> FlowResult:
+ """Configure destination by using gps coordinates."""
+ if user_input is not None:
+ self._config[CONF_DESTINATION_LATITUDE] = user_input["destination"][
+ "latitude"
+ ]
+ self._config[CONF_DESTINATION_LONGITUDE] = user_input["destination"][
+ "longitude"
+ ]
+ return self.async_create_entry(
+ title=self._config[CONF_NAME], data=self._config
+ )
+ schema = vol.Schema(
+ {"destination": selector({LocationSelector.selector_type: {}})}
+ )
+ return self.async_show_form(
+ step_id="destination_coordinates", data_schema=schema
+ )
+
+ async def async_step_destination_entity(
+ self,
+ user_input: dict[str, Any] | None = None,
+ ) -> FlowResult:
+ """Configure destination by using an entity."""
+ if user_input is not None:
+ self._config[CONF_DESTINATION_ENTITY_ID] = user_input[
+ CONF_DESTINATION_ENTITY_ID
+ ]
+ return self.async_create_entry(
+ title=self._config[CONF_NAME], data=self._config
+ )
+ schema = vol.Schema(
+ {CONF_DESTINATION_ENTITY_ID: selector({EntitySelector.selector_type: {}})}
+ )
+ return self.async_show_form(step_id="destination_entity", data_schema=schema)
+
+ async def async_step_import(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Import from configuration.yaml."""
+ options: dict[str, Any] = {}
+ user_input, options = self._transform_import_input(user_input)
+ # We need to prevent duplicate imports
+ if any(
+ is_dupe_import(entry, user_input, options)
+ for entry in self.hass.config_entries.async_entries(DOMAIN)
+ if entry.source == config_entries.SOURCE_IMPORT
+ ):
+ return self.async_abort(reason="already_configured")
+ return self.async_create_entry(
+ title=user_input[CONF_NAME], data=user_input, options=options
+ )
+
+ def _transform_import_input(
+ self, user_input
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
+ """Transform platform schema input to new model."""
+ options: dict[str, Any] = {}
+ if user_input.get(CONF_ORIGIN_LATITUDE) is not None:
+ user_input[CONF_ORIGIN_LATITUDE] = user_input.pop(CONF_ORIGIN_LATITUDE)
+ user_input[CONF_ORIGIN_LONGITUDE] = user_input.pop(CONF_ORIGIN_LONGITUDE)
+ else:
+ user_input[CONF_ORIGIN_ENTITY_ID] = user_input.pop(CONF_ORIGIN_ENTITY_ID)
+
+ if user_input.get(CONF_DESTINATION_LATITUDE) is not None:
+ user_input[CONF_DESTINATION_LATITUDE] = user_input.pop(
+ CONF_DESTINATION_LATITUDE
+ )
+ user_input[CONF_DESTINATION_LONGITUDE] = user_input.pop(
+ CONF_DESTINATION_LONGITUDE
+ )
+ else:
+ user_input[CONF_DESTINATION_ENTITY_ID] = user_input.pop(
+ CONF_DESTINATION_ENTITY_ID
+ )
+
+ options[CONF_TRAFFIC_MODE] = (
+ TRAFFIC_MODE_ENABLED
+ if user_input.pop(CONF_TRAFFIC_MODE, False)
+ else TRAFFIC_MODE_DISABLED
+ )
+ options[CONF_ROUTE_MODE] = user_input.pop(CONF_ROUTE_MODE)
+ options[CONF_UNIT_SYSTEM] = user_input.pop(
+ CONF_UNIT_SYSTEM, self.hass.config.units.name
+ )
+ options[CONF_ARRIVAL_TIME] = user_input.pop(CONF_ARRIVAL, None)
+ options[CONF_DEPARTURE_TIME] = user_input.pop(CONF_DEPARTURE, None)
+
+ return user_input, options
+
+
+class HERETravelTimeOptionsFlow(config_entries.OptionsFlow):
+ """Handle HERE Travel Time options."""
+
+ _config: dict[str, Any] = {}
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Initialize HERE Travel Time options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the HERE Travel Time options."""
+ if user_input is not None:
+ self._config = user_input
+ if self.config_entry.data[CONF_MODE] == TRAVEL_MODE_PUBLIC_TIME_TABLE:
+ return self.async_show_menu(
+ step_id="time_menu",
+ menu_options=["departure_time", "arrival_time", "no_time"],
+ )
+ return self.async_show_menu(
+ step_id="time_menu",
+ menu_options=["departure_time", "no_time"],
+ )
+
+ options = {
+ vol.Optional(
+ CONF_TRAFFIC_MODE,
+ default=self.config_entry.options.get(
+ CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED
+ ),
+ ): vol.In(TRAFFIC_MODES),
+ vol.Optional(
+ CONF_ROUTE_MODE,
+ default=self.config_entry.options.get(
+ CONF_ROUTE_MODE, ROUTE_MODE_FASTEST
+ ),
+ ): vol.In(ROUTE_MODES),
+ vol.Optional(
+ CONF_UNIT_SYSTEM,
+ default=self.config_entry.options.get(
+ CONF_UNIT_SYSTEM, self.hass.config.units.name
+ ),
+ ): vol.In(UNITS),
+ }
+
+ return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
+
+ async def async_step_no_time(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Create Options Entry."""
+ return self.async_create_entry(title="", data=self._config)
+
+ async def async_step_arrival_time(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Configure arrival time."""
+ if user_input is not None:
+ self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME]
+ return self.async_create_entry(title="", data=self._config)
+
+ options = {"arrival_time": selector({TimeSelector.selector_type: {}})}
+
+ return self.async_show_form(
+ step_id="arrival_time", data_schema=vol.Schema(options)
+ )
+
+ async def async_step_departure_time(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Configure departure time."""
+ if user_input is not None:
+ self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME]
+ return self.async_create_entry(title="", data=self._config)
+
+ options = {"departure_time": selector({TimeSelector.selector_type: {}})}
+
+ return self.async_show_form(
+ step_id="departure_time", data_schema=vol.Schema(options)
+ )
diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py
index a6b958ebf5e..bde17f5c306 100644
--- a/homeassistant/components/here_travel_time/const.py
+++ b/homeassistant/components/here_travel_time/const.py
@@ -8,20 +8,19 @@ from homeassistant.const import (
DOMAIN = "here_travel_time"
DEFAULT_SCAN_INTERVAL = 300
-CONF_DESTINATION = "destination"
-CONF_ORIGIN = "origin"
+
+CONF_DESTINATION_LATITUDE = "destination_latitude"
+CONF_DESTINATION_LONGITUDE = "destination_longitude"
+CONF_DESTINATION_ENTITY_ID = "destination_entity_id"
+CONF_ORIGIN_LATITUDE = "origin_latitude"
+CONF_ORIGIN_LONGITUDE = "origin_longitude"
+CONF_ORIGIN_ENTITY_ID = "origin_entity_id"
CONF_TRAFFIC_MODE = "traffic_mode"
CONF_ROUTE_MODE = "route_mode"
CONF_ARRIVAL = "arrival"
CONF_DEPARTURE = "departure"
CONF_ARRIVAL_TIME = "arrival_time"
CONF_DEPARTURE_TIME = "departure_time"
-CONF_TIME_TYPE = "time_type"
-CONF_TIME = "time"
-
-ARRIVAL_TIME = "Arrival Time"
-DEPARTURE_TIME = "Departure Time"
-TIME_TYPES = [ARRIVAL_TIME, DEPARTURE_TIME]
DEFAULT_NAME = "HERE Travel Time"
diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json
index b620153bba7..68370311254 100644
--- a/homeassistant/components/here_travel_time/manifest.json
+++ b/homeassistant/components/here_travel_time/manifest.json
@@ -1,6 +1,7 @@
{
"domain": "here_travel_time",
"name": "HERE Travel Time",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/here_travel_time",
"requirements": ["herepy==2.0.0"],
"codeowners": ["@eifinger"],
diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py
index eb85e966edf..65673a1e8b6 100644
--- a/homeassistant/components/here_travel_time/model.py
+++ b/homeassistant/components/here_travel_time/model.py
@@ -24,10 +24,12 @@ class HERERoutingData(TypedDict):
class HERETravelTimeConfig:
"""Configuration for HereTravelTimeDataUpdateCoordinator."""
- origin: str | None
- destination: str | None
- origin_entity_id: str | None
+ destination_latitude: float | None
+ destination_longitude: float | None
destination_entity_id: str | None
+ origin_latitude: float | None
+ origin_longitude: float | None
+ origin_entity_id: str | None
travel_mode: str
route_mode: str
units: str
diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py
index 304c49b6bed..4a09252f068 100644
--- a/homeassistant/components/here_travel_time/sensor.py
+++ b/homeassistant/components/here_travel_time/sensor.py
@@ -4,11 +4,10 @@ from __future__ import annotations
from datetime import timedelta
import logging
-import herepy
-from herepy.here_enum import RouteMode
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_MODE,
@@ -16,8 +15,6 @@ from homeassistant.const import (
CONF_MODE,
CONF_NAME,
CONF_UNIT_SYSTEM,
- CONF_UNIT_SYSTEM_IMPERIAL,
- CONF_UNIT_SYSTEM_METRIC,
TIME_MINUTES,
)
from homeassistant.core import HomeAssistant
@@ -28,74 +25,47 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HereTravelTimeDataUpdateCoordinator
-from .model import HERETravelTimeConfig
-
-_LOGGER = logging.getLogger(__name__)
-
-CONF_DESTINATION_LATITUDE = "destination_latitude"
-CONF_DESTINATION_LONGITUDE = "destination_longitude"
-CONF_DESTINATION_ENTITY_ID = "destination_entity_id"
-CONF_ORIGIN_LATITUDE = "origin_latitude"
-CONF_ORIGIN_LONGITUDE = "origin_longitude"
-CONF_ORIGIN_ENTITY_ID = "origin_entity_id"
-CONF_TRAFFIC_MODE = "traffic_mode"
-CONF_ROUTE_MODE = "route_mode"
-CONF_ARRIVAL = "arrival"
-CONF_DEPARTURE = "departure"
-
-DEFAULT_NAME = "HERE Travel Time"
-
-TRAVEL_MODE_BICYCLE = "bicycle"
-TRAVEL_MODE_CAR = "car"
-TRAVEL_MODE_PEDESTRIAN = "pedestrian"
-TRAVEL_MODE_PUBLIC = "publicTransport"
-TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable"
-TRAVEL_MODE_TRUCK = "truck"
-TRAVEL_MODE = [
+from .const import (
+ ATTR_DURATION,
+ ATTR_DURATION_IN_TRAFFIC,
+ ATTR_TRAFFIC_MODE,
+ ATTR_UNIT_SYSTEM,
+ CONF_ARRIVAL,
+ CONF_DEPARTURE,
+ CONF_DESTINATION_ENTITY_ID,
+ CONF_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE,
+ CONF_ORIGIN_ENTITY_ID,
+ CONF_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE,
+ CONF_ROUTE_MODE,
+ CONF_TRAFFIC_MODE,
+ DEFAULT_NAME,
+ DOMAIN,
+ ICON_BICYCLE,
+ ICON_CAR,
+ ICON_PEDESTRIAN,
+ ICON_PUBLIC,
+ ICON_TRUCK,
+ ROUTE_MODE_FASTEST,
+ ROUTE_MODES,
+ TRAFFIC_MODE_ENABLED,
TRAVEL_MODE_BICYCLE,
TRAVEL_MODE_CAR,
TRAVEL_MODE_PEDESTRIAN,
TRAVEL_MODE_PUBLIC,
TRAVEL_MODE_PUBLIC_TIME_TABLE,
TRAVEL_MODE_TRUCK,
-]
+ TRAVEL_MODES,
+ TRAVEL_MODES_PUBLIC,
+ UNITS,
+)
-TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE]
-TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK]
-TRAVEL_MODES_NON_VEHICLE = [TRAVEL_MODE_BICYCLE, TRAVEL_MODE_PEDESTRIAN]
+_LOGGER = logging.getLogger(__name__)
-TRAFFIC_MODE_ENABLED = "traffic_enabled"
-TRAFFIC_MODE_DISABLED = "traffic_disabled"
-
-ROUTE_MODE_FASTEST = "fastest"
-ROUTE_MODE_SHORTEST = "shortest"
-ROUTE_MODE = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST]
-
-ICON_BICYCLE = "mdi:bike"
-ICON_CAR = "mdi:car"
-ICON_PEDESTRIAN = "mdi:walk"
-ICON_PUBLIC = "mdi:bus"
-ICON_TRUCK = "mdi:truck"
-
-UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]
-
-ATTR_DURATION = "duration"
-ATTR_DISTANCE = "distance"
-ATTR_ROUTE = "route"
-ATTR_ORIGIN = "origin"
-ATTR_DESTINATION = "destination"
-
-ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM
-ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE
-
-ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic"
-ATTR_ORIGIN_NAME = "origin_name"
-ATTR_DESTINATION_NAME = "destination_name"
SCAN_INTERVAL = timedelta(minutes=5)
-NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input"
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
@@ -113,8 +83,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id,
vol.Optional(CONF_DEPARTURE): cv.time,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE),
- vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODE),
+ vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODES),
+ vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODES),
vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean,
vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS),
}
@@ -150,79 +120,36 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the HERE travel time platform."""
- api_key = config[CONF_API_KEY]
- here_client = herepy.RoutingApi(api_key)
-
- if not await hass.async_add_executor_job(
- _are_valid_client_credentials, here_client
- ):
- _LOGGER.error(
- "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token"
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
)
- return
-
- if config.get(CONF_ORIGIN_LATITUDE) is not None:
- origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}"
- origin_entity_id = None
- else:
- origin = None
- origin_entity_id = config[CONF_ORIGIN_ENTITY_ID]
-
- if config.get(CONF_DESTINATION_LATITUDE) is not None:
- destination = (
- f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}"
- )
- destination_entity_id = None
- else:
- destination = None
- destination_entity_id = config[CONF_DESTINATION_ENTITY_ID]
-
- traffic_mode = config[CONF_TRAFFIC_MODE]
- name = config[CONF_NAME]
-
- here_travel_time_config = HERETravelTimeConfig(
- origin=origin,
- destination=destination,
- origin_entity_id=origin_entity_id,
- destination_entity_id=destination_entity_id,
- travel_mode=config[CONF_MODE],
- route_mode=config[CONF_ROUTE_MODE],
- units=config.get(CONF_UNIT_SYSTEM, hass.config.units.name),
- arrival=config.get(CONF_ARRIVAL),
- departure=config.get(CONF_DEPARTURE),
)
- coordinator = HereTravelTimeDataUpdateCoordinator(
- hass,
- here_client,
- here_travel_time_config,
+ _LOGGER.warning(
+ "Your HERE travel time configuration has been imported into the UI; "
+ "please remove it from configuration.yaml as support for it will be "
+ "removed in a future release"
)
- sensor = HERETravelTimeSensor(name, traffic_mode, coordinator)
- async_add_entities([sensor])
-
-
-def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool:
- """Check if the provided credentials are correct using defaults."""
- known_working_origin = [38.9, -77.04833]
- known_working_destination = [39.0, -77.1]
- try:
- here_client.public_transport_timetable(
- known_working_origin,
- known_working_destination,
- True,
- [
- RouteMode[ROUTE_MODE_FASTEST],
- RouteMode[TRAVEL_MODE_CAR],
- RouteMode[TRAFFIC_MODE_ENABLED],
- ],
- arrival=None,
- departure="now",
- )
- except herepy.InvalidCredentialsError:
- return False
- return True
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Add HERE travel time entities from a config_entry."""
+ async_add_entities(
+ [
+ HERETravelTimeSensor(
+ config_entry.data[CONF_NAME],
+ config_entry.options[CONF_TRAFFIC_MODE],
+ hass.data[DOMAIN][config_entry.entry_id],
+ )
+ ],
+ )
class HERETravelTimeSensor(SensorEntity, CoordinatorEntity):
@@ -231,12 +158,12 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity):
def __init__(
self,
name: str,
- traffic_mode: bool,
+ traffic_mode: str,
coordinator: HereTravelTimeDataUpdateCoordinator,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
- self._traffic_mode = traffic_mode
+ self._traffic_mode = traffic_mode == TRAFFIC_MODE_ENABLED
self._attr_native_unit_of_measurement = TIME_MINUTES
self._attr_name = name
diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json
new file mode 100644
index 00000000000..e4a20a38d6b
--- /dev/null
+++ b/homeassistant/components/here_travel_time/strings.json
@@ -0,0 +1,82 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "[%key:common::config_flow::data::name%]",
+ "api_key": "[%key:common::config_flow::data::api_key%]",
+ "mode": "Travel Mode"
+ }
+ },
+ "origin_coordinates": {
+ "title": "Choose Origin",
+ "data": {
+ "origin": "Origin as GPS coordinates"
+ }
+ },
+ "origin_entity_id": {
+ "title": "Choose Origin",
+ "data": {
+ "origin_entity_id": "Origin using an entity"
+ }
+ },
+ "destination_menu": {
+ "title": "Choose Destination",
+ "menu_options": {
+ "destination_coordinates": "Using a map location",
+ "destination_entity": "Using an entity"
+ }
+ },
+ "destination_coordinates": {
+ "title": "Choose Destination",
+ "data": {
+ "destination": "Destination as GPS coordinates"
+ }
+ },
+ "destination_entity_id": {
+ "title": "Choose Destination",
+ "data": {
+ "destination_entity_id": "Destination using an entity"
+ }
+ }
+ },
+ "error": {
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "traffic_mode": "Traffic Mode",
+ "route_mode": "Route Mode",
+ "unit_system": "Unit system"
+ }
+ },
+ "time_menu": {
+ "title": "Choose Time Type",
+ "menu_options": {
+ "departure_time": "Configure a departure time",
+ "arrival_time": "Configure an arrival time",
+ "no_time": "Do not configure a time"
+ }
+ },
+ "departure_time": {
+ "title": "Choose Departure Time",
+ "data": {
+ "departure_time": "Departure Time"
+ }
+ },
+ "arrival_time": {
+ "title": "Choose Arrival Time",
+ "data": {
+ "arrival_time": "Arrival Time"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 7f0059b2da9..67299f403c2 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -143,6 +143,7 @@ FLOWS = {
"hangouts",
"harmony",
"heos",
+ "here_travel_time",
"hisense_aehw4a1",
"hive",
"hlk_sw16",
diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py
index 83f0659f516..368b070428e 100644
--- a/tests/components/here_travel_time/conftest.py
+++ b/tests/components/here_travel_time/conftest.py
@@ -12,6 +12,11 @@ RESPONSE = RoutingResponse.new_from_jsondict(
)
RESPONSE.route_short = "US-29 - K St NW; US-29 - Whitehurst Fwy; I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd"
+EMPTY_ATTRIBUTION_RESPONSE = RoutingResponse.new_from_jsondict(
+ json.loads(load_fixture("here_travel_time/empty_attribution_response.json"))
+)
+EMPTY_ATTRIBUTION_RESPONSE.route_short = "US-29 - K St NW; US-29 - Whitehurst Fwy; I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd"
+
@pytest.fixture(name="valid_response")
def valid_response_fixture():
@@ -21,3 +26,13 @@ def valid_response_fixture():
return_value=RESPONSE,
) as mock:
yield mock
+
+
+@pytest.fixture(name="empty_attribution_response")
+def empty_attribution_response_fixture():
+ """Return valid api response with an empty attribution."""
+ with patch(
+ "herepy.RoutingApi.public_transport_timetable",
+ return_value=EMPTY_ATTRIBUTION_RESPONSE,
+ ) as mock:
+ yield mock
diff --git a/tests/components/here_travel_time/fixtures/empty_attribution_response.json b/tests/components/here_travel_time/fixtures/empty_attribution_response.json
new file mode 100644
index 00000000000..cc1bb20a373
--- /dev/null
+++ b/tests/components/here_travel_time/fixtures/empty_attribution_response.json
@@ -0,0 +1,131 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-19T07:38:39Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201928-4446",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": ["8.30.98.154"]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "+732182239",
+ "mappedPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0488358
+ },
+ "originalPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0483301
+ },
+ "type": "stopOver",
+ "spot": 0.4946237,
+ "sideOfStreet": "right",
+ "mappedRoadName": "22nd St NW",
+ "label": "22nd St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "+942865877",
+ "mappedPosition": {
+ "latitude": 38.9999735,
+ "longitude": -77.100141
+ },
+ "originalPosition": {
+ "latitude": 38.9999999,
+ "longitude": -77.1000001
+ },
+ "type": "stopOver",
+ "spot": 1,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Service Rd S",
+ "label": "Service Rd S",
+ "shapeIndex": 279,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": ["car"],
+ "trafficMode": "enabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "+732182239",
+ "mappedPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0488358
+ },
+ "originalPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0483301
+ },
+ "type": "stopOver",
+ "spot": 0.4946237,
+ "sideOfStreet": "right",
+ "mappedRoadName": "22nd St NW",
+ "label": "22nd St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "+942865877",
+ "mappedPosition": {
+ "latitude": 38.9999735,
+ "longitude": -77.100141
+ },
+ "originalPosition": {
+ "latitude": 38.9999999,
+ "longitude": -77.1000001
+ },
+ "type": "stopOver",
+ "spot": 1,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Service Rd S",
+ "label": "Service Rd S",
+ "shapeIndex": 279,
+ "source": "user"
+ },
+ "length": 23903,
+ "travelTime": 1884,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 38.9999735,
+ "longitude": -77.100141
+ },
+ "instruction": "Arrive at Service Rd S. Your destination is on the left.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M16",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "distance": 23903,
+ "trafficTime": 1861,
+ "baseTime": 1803,
+ "flags": [
+ "noThroughRoad",
+ "motorway",
+ "builtUpArea",
+ "park",
+ "privateRoad"
+ ],
+ "text": "The trip takes 23.9 km and 31 mins.",
+ "travelTime": 1861,
+ "_type": "RouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us",
+ "sourceAttribution": {}
+ }
+}
diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py
new file mode 100644
index 00000000000..105128c7cfb
--- /dev/null
+++ b/tests/components/here_travel_time/test_config_flow.py
@@ -0,0 +1,589 @@
+"""Test the HERE Travel Time config flow."""
+from unittest.mock import patch
+
+from herepy import HEREError
+from herepy.routing_api import InvalidCredentialsError
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.here_travel_time.const import (
+ CONF_ARRIVAL,
+ CONF_ARRIVAL_TIME,
+ CONF_DEPARTURE,
+ CONF_DEPARTURE_TIME,
+ CONF_ROUTE_MODE,
+ CONF_TRAFFIC_MODE,
+ DOMAIN,
+ ROUTE_MODE_FASTEST,
+ TRAFFIC_MODE_ENABLED,
+ TRAVEL_MODE_CAR,
+ TRAVEL_MODE_PUBLIC_TIME_TABLE,
+)
+from homeassistant.components.here_travel_time.sensor import (
+ CONF_DESTINATION_ENTITY_ID,
+ CONF_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE,
+ CONF_ORIGIN_ENTITY_ID,
+ CONF_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE,
+)
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_MODE,
+ CONF_NAME,
+ CONF_UNIT_SYSTEM,
+ CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_UNIT_SYSTEM_METRIC,
+)
+from homeassistant.core import HomeAssistant
+
+from .const import (
+ API_KEY,
+ CAR_DESTINATION_LATITUDE,
+ CAR_DESTINATION_LONGITUDE,
+ CAR_ORIGIN_LATITUDE,
+ CAR_ORIGIN_LONGITUDE,
+)
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="user_step_result")
+async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult:
+ """Provide the result of a completed user step."""
+ init_result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ user_step_result = await hass.config_entries.flow.async_configure(
+ init_result["flow_id"],
+ {
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_NAME: "test",
+ },
+ )
+ await hass.async_block_till_done()
+ yield user_step_result
+
+
+@pytest.fixture(name="option_init_result")
+async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult:
+ """Provide the result of a completed options init step."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_PUBLIC_TIME_TABLE,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ flow = await hass.config_entries.options.async_init(entry.entry_id)
+ result = await hass.config_entries.options.async_configure(
+ flow["flow_id"],
+ user_input={
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ },
+ )
+ yield result
+
+
+@pytest.fixture(name="origin_step_result")
+async def origin_step_result_fixture(
+ hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult
+) -> data_entry_flow.FlowResult:
+ """Provide the result of a completed origin by coordinates step."""
+ origin_menu_result = await hass.config_entries.flow.async_configure(
+ user_step_result["flow_id"], {"next_step_id": "origin_coordinates"}
+ )
+
+ location_selector_result = await hass.config_entries.flow.async_configure(
+ origin_menu_result["flow_id"],
+ {
+ "origin": {
+ "latitude": float(CAR_ORIGIN_LATITUDE),
+ "longitude": float(CAR_ORIGIN_LONGITUDE),
+ "radius": 3.0,
+ }
+ },
+ )
+ yield location_selector_result
+
+
+@pytest.mark.parametrize(
+ "menu_options",
+ (["origin_coordinates", "origin_entity"],),
+)
+@pytest.mark.usefixtures("valid_response")
+async def test_step_user(hass: HomeAssistant, menu_options) -> None:
+ """Test the user step."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_NAME: "test",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_MENU
+ assert result2["menu_options"] == menu_options
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_step_origin_coordinates(
+ hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult
+) -> None:
+ """Test the origin coordinates step."""
+ menu_result = await hass.config_entries.flow.async_configure(
+ user_step_result["flow_id"], {"next_step_id": "origin_coordinates"}
+ )
+ assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ location_selector_result = await hass.config_entries.flow.async_configure(
+ menu_result["flow_id"],
+ {
+ "origin": {
+ "latitude": float(CAR_ORIGIN_LATITUDE),
+ "longitude": float(CAR_ORIGIN_LONGITUDE),
+ "radius": 3.0,
+ }
+ },
+ )
+ assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_step_origin_entity(
+ hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult
+) -> None:
+ """Test the origin coordinates step."""
+ menu_result = await hass.config_entries.flow.async_configure(
+ user_step_result["flow_id"], {"next_step_id": "origin_entity"}
+ )
+ assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ entity_selector_result = await hass.config_entries.flow.async_configure(
+ menu_result["flow_id"],
+ {"origin_entity_id": "zone.home"},
+ )
+ assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_step_destination_coordinates(
+ hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult
+) -> None:
+ """Test the origin coordinates step."""
+ menu_result = await hass.config_entries.flow.async_configure(
+ origin_step_result["flow_id"], {"next_step_id": "destination_coordinates"}
+ )
+ assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ location_selector_result = await hass.config_entries.flow.async_configure(
+ menu_result["flow_id"],
+ {
+ "destination": {
+ "latitude": float(CAR_DESTINATION_LATITUDE),
+ "longitude": float(CAR_DESTINATION_LONGITUDE),
+ "radius": 3.0,
+ }
+ },
+ )
+ assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.data == {
+ CONF_NAME: "test",
+ CONF_API_KEY: API_KEY,
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_MODE: TRAVEL_MODE_CAR,
+ }
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_step_destination_entity(
+ hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult
+) -> None:
+ """Test the origin coordinates step."""
+ menu_result = await hass.config_entries.flow.async_configure(
+ origin_step_result["flow_id"], {"next_step_id": "destination_entity"}
+ )
+ assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ entity_selector_result = await hass.config_entries.flow.async_configure(
+ menu_result["flow_id"],
+ {"destination_entity_id": "zone.home"},
+ )
+ assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.data == {
+ CONF_NAME: "test",
+ CONF_API_KEY: API_KEY,
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_ENTITY_ID: "zone.home",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ }
+
+
+async def test_form_invalid_auth(hass: HomeAssistant) -> None:
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "herepy.RoutingApi.public_transport_timetable",
+ side_effect=InvalidCredentialsError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_NAME: "test",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_unknown_error(hass: HomeAssistant) -> None:
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "herepy.RoutingApi.public_transport_timetable",
+ side_effect=HEREError,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_NAME: "test",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "unknown"}
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_options_flow(hass: HomeAssistant) -> None:
+ """Test the options flow."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(entry.entry_id)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
+ },
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_MENU
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_options_flow_arrival_time_step(
+ hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult
+) -> None:
+ """Test the options flow arrival time type."""
+ menu_result = await hass.config_entries.options.async_configure(
+ option_init_result["flow_id"], {"next_step_id": "arrival_time"}
+ )
+ assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ time_selector_result = await hass.config_entries.options.async_configure(
+ option_init_result["flow_id"],
+ user_input={
+ "arrival_time": "08:00:00",
+ },
+ )
+
+ assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.options == {
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ CONF_ARRIVAL_TIME: "08:00:00",
+ }
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_options_flow_departure_time_step(
+ hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult
+) -> None:
+ """Test the options flow departure time type."""
+ menu_result = await hass.config_entries.options.async_configure(
+ option_init_result["flow_id"], {"next_step_id": "departure_time"}
+ )
+ assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ time_selector_result = await hass.config_entries.options.async_configure(
+ option_init_result["flow_id"],
+ user_input={
+ "departure_time": "08:00:00",
+ },
+ )
+
+ assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.options == {
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ CONF_DEPARTURE_TIME: "08:00:00",
+ }
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_options_flow_no_time_step(
+ hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult
+) -> None:
+ """Test the options flow arrival time type."""
+ menu_result = await hass.config_entries.options.async_configure(
+ option_init_result["flow_id"], {"next_step_id": "no_time"}
+ )
+
+ assert menu_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.options == {
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ }
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_import_flow_entity_id(hass: HomeAssistant) -> None:
+ """Test import_flow with entity ids."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_ENTITY_ID: "sensor.origin",
+ CONF_DESTINATION_ENTITY_ID: "sensor.destination",
+ CONF_NAME: "test_name",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_DEPARTURE: "08:00:00",
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "test_name"
+
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.data == {
+ CONF_NAME: "test_name",
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_ENTITY_ID: "sensor.origin",
+ CONF_DESTINATION_ENTITY_ID: "sensor.destination",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ }
+ assert entry.options == {
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ CONF_DEPARTURE_TIME: "08:00:00",
+ CONF_ARRIVAL_TIME: None,
+ }
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_import_flow_coordinates(hass: HomeAssistant) -> None:
+ """Test import_flow with coordinates."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE,
+ CONF_NAME: "test_name",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_ARRIVAL: "08:00:00",
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "test_name"
+
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+ assert entry.data == {
+ CONF_NAME: "test_name",
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE,
+ CONF_MODE: TRAVEL_MODE_CAR,
+ }
+ assert entry.options == {
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ CONF_DEPARTURE_TIME: None,
+ CONF_ARRIVAL_TIME: "08:00:00",
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ }
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_dupe_import(hass: HomeAssistant) -> None:
+ """Test duplicate import."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE,
+ CONF_NAME: "test_name",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_ARRIVAL: "08:00:00",
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE,
+ CONF_NAME: "test_name2",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_ARRIVAL: "08:00:00",
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE,
+ CONF_NAME: "test_name",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_ARRIVAL: "08:00:01",
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_LATITUDE: "40.0",
+ CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE,
+ CONF_NAME: "test_name",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_ARRIVAL: "08:00:01",
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data={
+ CONF_API_KEY: CONF_API_KEY,
+ CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE,
+ CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE,
+ CONF_NAME: "test_name",
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_ARRIVAL: "08:00:00",
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+ CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
diff --git a/tests/components/here_travel_time/test_init.py b/tests/components/here_travel_time/test_init.py
new file mode 100644
index 00000000000..02827acc4df
--- /dev/null
+++ b/tests/components/here_travel_time/test_init.py
@@ -0,0 +1,48 @@
+"""The test for the HERE Travel Time integration."""
+
+import pytest
+
+from homeassistant.components.here_travel_time.const import (
+ CONF_DESTINATION_LATITUDE,
+ CONF_DESTINATION_LONGITUDE,
+ CONF_ORIGIN_LATITUDE,
+ CONF_ORIGIN_LONGITUDE,
+ DOMAIN,
+ TRAVEL_MODE_CAR,
+)
+from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
+from homeassistant.core import HomeAssistant
+
+from .const import (
+ API_KEY,
+ CAR_DESTINATION_LATITUDE,
+ CAR_DESTINATION_LONGITUDE,
+ CAR_ORIGIN_LATITUDE,
+ CAR_ORIGIN_LONGITUDE,
+)
+
+from tests.common import MockConfigEntry
+
+
+@pytest.mark.usefixtures("valid_response")
+async def test_unload_entry(hass: HomeAssistant) -> None:
+ """Test that unloading an entry works."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_CAR,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ assert not hass.data[DOMAIN]
diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py
index 4ad2f757c77..585d5adc9d3 100644
--- a/tests/components/here_travel_time/test_sensor.py
+++ b/tests/components/here_travel_time/test_sensor.py
@@ -2,15 +2,10 @@
from unittest.mock import MagicMock, patch
from herepy.here_enum import RouteMode
-from herepy.routing_api import InvalidCredentialsError, NoRouteFoundError
+from herepy.routing_api import NoRouteFoundError
import pytest
from homeassistant.components.here_travel_time.const import (
- ROUTE_MODE_FASTEST,
- TRAFFIC_MODE_ENABLED,
-)
-from homeassistant.components.here_travel_time.sensor import (
- ATTR_ATTRIBUTION,
ATTR_DESTINATION,
ATTR_DESTINATION_NAME,
ATTR_DISTANCE,
@@ -19,16 +14,27 @@ from homeassistant.components.here_travel_time.sensor import (
ATTR_ORIGIN,
ATTR_ORIGIN_NAME,
ATTR_ROUTE,
- CONF_MODE,
+ 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,
CONF_TRAFFIC_MODE,
CONF_UNIT_SYSTEM,
+ DOMAIN,
ICON_BICYCLE,
ICON_CAR,
ICON_PEDESTRIAN,
ICON_PUBLIC,
ICON_TRUCK,
NO_ROUTE_ERROR_MESSAGE,
- TIME_MINUTES,
+ ROUTE_MODE_FASTEST,
+ TRAFFIC_MODE_DISABLED,
+ TRAFFIC_MODE_ENABLED,
TRAVEL_MODE_BICYCLE,
TRAVEL_MODE_CAR,
TRAVEL_MODE_PEDESTRIAN,
@@ -36,11 +42,20 @@ from homeassistant.components.here_travel_time.sensor import (
TRAVEL_MODE_TRUCK,
TRAVEL_MODES_VEHICLE,
)
-from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ ATTR_ICON,
+ CONF_API_KEY,
+ CONF_MODE,
+ CONF_NAME,
+ EVENT_HOMEASSISTANT_START,
+ TIME_MINUTES,
+)
+from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
-from tests.components.here_travel_time.const import (
+from .const import (
API_KEY,
CAR_DESTINATION_LATITUDE,
CAR_DESTINATION_LONGITUDE,
@@ -48,21 +63,41 @@ from tests.components.here_travel_time.const import (
CAR_ORIGIN_LONGITUDE,
)
-DOMAIN = "sensor"
-
-PLATFORM = "here_travel_time"
+from tests.common import MockConfigEntry
@pytest.mark.parametrize(
- "mode,icon,traffic_mode,unit_system,expected_state,expected_distance,expected_duration_in_traffic",
+ "mode,icon,traffic_mode,unit_system,arrival_time,departure_time,expected_state,expected_distance,expected_duration_in_traffic",
[
- (TRAVEL_MODE_CAR, ICON_CAR, True, "metric", "31", 23.903, 31.016666666666666),
- (TRAVEL_MODE_BICYCLE, ICON_BICYCLE, False, "metric", "30", 23.903, 30.05),
+ (
+ TRAVEL_MODE_CAR,
+ ICON_CAR,
+ TRAFFIC_MODE_ENABLED,
+ "metric",
+ None,
+ None,
+ "31",
+ 23.903,
+ 31.016666666666666,
+ ),
+ (
+ TRAVEL_MODE_BICYCLE,
+ ICON_BICYCLE,
+ TRAFFIC_MODE_DISABLED,
+ "metric",
+ None,
+ None,
+ "30",
+ 23.903,
+ 30.05,
+ ),
(
TRAVEL_MODE_PEDESTRIAN,
ICON_PEDESTRIAN,
- False,
+ TRAFFIC_MODE_DISABLED,
"imperial",
+ None,
+ None,
"30",
14.852635608048994,
30.05,
@@ -70,8 +105,10 @@ PLATFORM = "here_travel_time"
(
TRAVEL_MODE_PUBLIC_TIME_TABLE,
ICON_PUBLIC,
- False,
+ TRAFFIC_MODE_DISABLED,
"imperial",
+ "08:00:00",
+ None,
"30",
14.852635608048994,
30.05,
@@ -79,41 +116,52 @@ PLATFORM = "here_travel_time"
(
TRAVEL_MODE_TRUCK,
ICON_TRUCK,
- True,
+ TRAFFIC_MODE_ENABLED,
"metric",
+ None,
+ "08:00:00",
"31",
23.903,
31.016666666666666,
),
],
)
+@pytest.mark.usefixtures("valid_response")
async def test_sensor(
- hass,
+ hass: HomeAssistant,
mode,
icon,
traffic_mode,
unit_system,
+ arrival_time,
+ departure_time,
expected_state,
expected_distance,
expected_duration_in_traffic,
- valid_response,
):
"""Test that sensor works."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_latitude": CAR_DESTINATION_LATITUDE,
- "destination_longitude": CAR_DESTINATION_LONGITUDE,
- "api_key": API_KEY,
- "traffic_mode": traffic_mode,
- "unit_system": unit_system,
- "mode": mode,
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: mode,
+ CONF_NAME: "test",
+ },
+ options={
+ CONF_TRAFFIC_MODE: traffic_mode,
+ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+ CONF_ARRIVAL_TIME: arrival_time,
+ CONF_DEPARTURE_TIME: departure_time,
+ CONF_UNIT_SYSTEM: unit_system,
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
@@ -145,7 +193,9 @@ async def test_sensor(
assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW"
assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S"
assert sensor.attributes.get(CONF_MODE) == mode
- assert sensor.attributes.get(CONF_TRAFFIC_MODE) is traffic_mode
+ assert sensor.attributes.get(CONF_TRAFFIC_MODE) is (
+ traffic_mode == TRAFFIC_MODE_ENABLED
+ )
assert sensor.attributes.get(ATTR_ICON) == icon
@@ -156,7 +206,63 @@ async def test_sensor(
)
-async def test_entity_ids(hass, valid_response: MagicMock):
+@pytest.mark.usefixtures("valid_response")
+async def test_circular_ref(hass: HomeAssistant, caplog):
+ """Test that a circular ref is handled."""
+ hass.states.async_set(
+ "test.first",
+ "test.second",
+ )
+ hass.states.async_set("test.second", "test.first")
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_ENTITY_ID: "test.first",
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ assert "No coordinatnes found for test.first" in caplog.text
+
+
+@pytest.mark.usefixtures("empty_attribution_response")
+async def test_no_attribution(hass: HomeAssistant):
+ """Test that an empty attribution is handled."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.test").attributes.get(ATTR_ATTRIBUTION) is None
+
+
+async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock):
"""Test that origin/destination supplied by entities works."""
utcnow = dt_util.utcnow()
# Patching 'utcnow' to gain more control over the timed update.
@@ -181,17 +287,19 @@ async def test_entity_ids(hass, valid_response: MagicMock):
"longitude": float(CAR_DESTINATION_LONGITUDE),
},
)
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_entity_id": "zone.origin",
- "destination_entity_id": "device_tracker.test",
- "api_key": API_KEY,
- "mode": TRAVEL_MODE_TRUCK,
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_ENTITY_ID: "zone.origin",
+ CONF_DESTINATION_ENTITY_ID: "device_tracker.test",
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@@ -214,20 +322,23 @@ async def test_entity_ids(hass, valid_response: MagicMock):
)
-async def test_destination_entity_not_found(hass, caplog, valid_response: MagicMock):
+@pytest.mark.usefixtures("valid_response")
+async def test_destination_entity_not_found(hass: HomeAssistant, caplog):
"""Test that a not existing destination_entity_id is caught."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_entity_id": "device_tracker.test",
- "api_key": API_KEY,
- "mode": TRAVEL_MODE_TRUCK,
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_ENTITY_ID: "device_tracker.test",
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@@ -236,20 +347,23 @@ async def test_destination_entity_not_found(hass, caplog, valid_response: MagicM
assert "device_tracker.test are not valid coordinates" in caplog.text
-async def test_origin_entity_not_found(hass, caplog, valid_response: MagicMock):
+@pytest.mark.usefixtures("valid_response")
+async def test_origin_entity_not_found(hass: HomeAssistant, caplog):
"""Test that a not existing origin_entity_id is caught."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_entity_id": "device_tracker.test",
- "destination_latitude": CAR_ORIGIN_LATITUDE,
- "destination_longitude": CAR_ORIGIN_LONGITUDE,
- "api_key": API_KEY,
- "mode": TRAVEL_MODE_TRUCK,
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_ENTITY_ID: "device_tracker.test",
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@@ -258,26 +372,27 @@ async def test_origin_entity_not_found(hass, caplog, valid_response: MagicMock):
assert "device_tracker.test are not valid coordinates" in caplog.text
-async def test_invalid_destination_entity_state(
- hass, caplog, valid_response: MagicMock
-):
+@pytest.mark.usefixtures("valid_response")
+async def test_invalid_destination_entity_state(hass: HomeAssistant, caplog):
"""Test that an invalid state of the destination_entity_id is caught."""
hass.states.async_set(
"device_tracker.test",
"test_state",
)
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_entity_id": "device_tracker.test",
- "api_key": API_KEY,
- "mode": TRAVEL_MODE_TRUCK,
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_ENTITY_ID: "device_tracker.test",
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@@ -286,24 +401,27 @@ async def test_invalid_destination_entity_state(
assert "test_state are not valid coordinates" in caplog.text
-async def test_invalid_origin_entity_state(hass, caplog, valid_response: MagicMock):
+@pytest.mark.usefixtures("valid_response")
+async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog):
"""Test that an invalid state of the origin_entity_id is caught."""
hass.states.async_set(
"device_tracker.test",
"test_state",
)
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_entity_id": "device_tracker.test",
- "destination_latitude": CAR_ORIGIN_LATITUDE,
- "destination_longitude": CAR_ORIGIN_LONGITUDE,
- "api_key": API_KEY,
- "mode": TRAVEL_MODE_TRUCK,
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_ENTITY_ID: "device_tracker.test",
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
@@ -312,27 +430,30 @@ async def test_invalid_origin_entity_state(hass, caplog, valid_response: MagicMo
assert "test_state are not valid coordinates" in caplog.text
-async def test_route_not_found(hass, caplog):
+async def test_route_not_found(hass: HomeAssistant, caplog):
"""Test that route not found error is correctly handled."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_latitude": CAR_DESTINATION_LATITUDE,
- "destination_longitude": CAR_DESTINATION_LONGITUDE,
- "api_key": API_KEY,
- }
- }
with patch(
- "homeassistant.components.here_travel_time.sensor._are_valid_client_credentials",
- return_value=True,
+ "homeassistant.components.here_travel_time.config_flow.validate_api_key",
+ return_value=None,
), patch(
"herepy.RoutingApi.public_transport_timetable",
side_effect=NoRouteFoundError,
):
- assert await async_setup_component(hass, DOMAIN, config)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="0123456789",
+ data={
+ CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE),
+ CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE),
+ CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE),
+ CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE),
+ CONF_API_KEY: API_KEY,
+ CONF_MODE: TRAVEL_MODE_TRUCK,
+ CONF_NAME: "test",
+ },
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
@@ -340,113 +461,26 @@ async def test_route_not_found(hass, caplog):
assert NO_ROUTE_ERROR_MESSAGE in caplog.text
-async def test_invalid_credentials(hass, caplog):
- """Test that invalid credentials error is correctly handled."""
+@pytest.mark.usefixtures("valid_response")
+async def test_setup_platform(hass: HomeAssistant, caplog):
+ """Test that setup platform migration works."""
+ config = {
+ "sensor": {
+ "platform": DOMAIN,
+ "name": "test",
+ "origin_latitude": CAR_ORIGIN_LATITUDE,
+ "origin_longitude": CAR_ORIGIN_LONGITUDE,
+ "destination_latitude": CAR_DESTINATION_LATITUDE,
+ "destination_longitude": CAR_DESTINATION_LONGITUDE,
+ "api_key": API_KEY,
+ }
+ }
with patch(
- "herepy.RoutingApi.public_transport_timetable",
- side_effect=InvalidCredentialsError,
+ "homeassistant.components.here_travel_time.async_setup_entry", return_value=True
):
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_latitude": CAR_DESTINATION_LATITUDE,
- "destination_longitude": CAR_DESTINATION_LONGITUDE,
- "api_key": API_KEY,
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
+ await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
- assert "Invalid credentials" in caplog.text
-
-
-async def test_arrival(hass, valid_response):
- """Test that arrival works."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_latitude": CAR_DESTINATION_LATITUDE,
- "destination_longitude": CAR_DESTINATION_LONGITUDE,
- "api_key": API_KEY,
- "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
- "arrival": "01:00:00",
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
- await hass.async_block_till_done()
-
- hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
- await hass.async_block_till_done()
-
- sensor = hass.states.get("sensor.test")
- assert sensor.state == "30"
-
-
-async def test_departure(hass, valid_response):
- """Test that departure works."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_latitude": CAR_DESTINATION_LATITUDE,
- "destination_longitude": CAR_DESTINATION_LONGITUDE,
- "api_key": API_KEY,
- "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
- "departure": "23:00:00",
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
- await hass.async_block_till_done()
-
- hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
- await hass.async_block_till_done()
-
- sensor = hass.states.get("sensor.test")
- assert sensor.state == "30"
-
-
-async def test_arrival_only_allowed_for_timetable(hass, caplog):
- """Test that arrival is only allowed when mode is publicTransportTimeTable."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_latitude": CAR_DESTINATION_LATITUDE,
- "destination_longitude": CAR_DESTINATION_LONGITUDE,
- "api_key": API_KEY,
- "arrival": "01:00:00",
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
- await hass.async_block_till_done()
- assert "[arrival] is an invalid option" in caplog.text
-
-
-async def test_exclusive_arrival_and_departure(hass, caplog):
- """Test that arrival and departure are exclusive."""
- config = {
- DOMAIN: {
- "platform": PLATFORM,
- "name": "test",
- "origin_latitude": CAR_ORIGIN_LATITUDE,
- "origin_longitude": CAR_ORIGIN_LONGITUDE,
- "destination_latitude": CAR_DESTINATION_LATITUDE,
- "destination_longitude": CAR_DESTINATION_LONGITUDE,
- "api_key": API_KEY,
- "arrival": "01:00:00",
- "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
- "departure": "01:00:00",
- }
- }
- assert await async_setup_component(hass, DOMAIN, config)
- await hass.async_block_till_done()
- assert "two or more values in the same group of exclusion" in caplog.text
+ assert (
+ "Your HERE travel time configuration has been imported into the UI"
+ in caplog.text
+ )