diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index b9ffa3e4baa761f49dbe8479ed17713ca9d118e8..28ecf3f3d8d8e00824f5a9bf9326de1399c258bd 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -1,38 +1,14 @@ """The HERE Travel Time integration.""" from __future__ import annotations -from datetime import datetime, time, timedelta import logging -import async_timeout -from herepy import NoRouteFoundError, RouteMode, RoutingApi, RoutingResponse -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_MODE, - CONF_UNIT_SYSTEM, - LENGTH_METERS, - LENGTH_MILES, - Platform, -) +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_UNIT_SYSTEM, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.location import find_coordinates -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt -from homeassistant.util.unit_conversion import DistanceConverter from .const import ( - ATTR_DESTINATION, - ATTR_DESTINATION_NAME, - ATTR_DISTANCE, - ATTR_DURATION, - ATTR_DURATION_IN_TRAFFIC, - ATTR_ORIGIN, - ATTR_ORIGIN_NAME, CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, @@ -42,24 +18,22 @@ from .const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - DEFAULT_SCAN_INTERVAL, DOMAIN, - IMPERIAL_UNITS, - NO_ROUTE_ERROR_MESSAGE, - TRAFFIC_MODE_ENABLED, - TRAVEL_MODES_VEHICLE, + TRAVEL_MODE_PUBLIC, +) +from .coordinator import ( + HERERoutingDataUpdateCoordinator, + HERETransitDataUpdateCoordinator, ) -from .model import HERERoutingData, HERETravelTimeConfig +from .model import HERETravelTimeConfig 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) arrival = ( dt.parse_time(config_entry.options[CONF_ARRIVAL_TIME]) @@ -86,12 +60,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b departure=departure, ) - coordinator = HereTravelTimeDataUpdateCoordinator( - hass, - here_client, - here_travel_time_config, - ) - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: + hass.data.setdefault(DOMAIN, {})[ + config_entry.entry_id + ] = HERETransitDataUpdateCoordinator( + hass, + api_key, + here_travel_time_config, + ) + else: + hass.data.setdefault(DOMAIN, {})[ + config_entry.entry_id + ] = HERERoutingDataUpdateCoordinator( + hass, + api_key, + here_travel_time_config, + ) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -106,173 +90,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok - - -class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): - """HERETravelTime DataUpdateCoordinator.""" - - def __init__( - self, - hass: HomeAssistant, - api: RoutingApi, - config: HERETravelTimeConfig, - ) -> None: - """Initialize.""" - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), - ) - self._api = api - self.config = config - - async def _async_update_data(self) -> HERERoutingData | None: - """Get the latest data from the HERE Routing API.""" - try: - async with async_timeout.timeout(10): - return await self.hass.async_add_executor_job(self._update) - except NoRouteFoundError as error: - raise UpdateFailed(NO_ROUTE_ERROR_MESSAGE) from error - - def _update(self) -> HERERoutingData | None: - """Get the latest data from the HERE Routing API.""" - try: - origin, destination, arrival, departure = self._prepare_parameters() - - _LOGGER.debug( - "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", - origin, - destination, - RouteMode[self.config.route_mode], - RouteMode[self.config.travel_mode], - RouteMode[TRAFFIC_MODE_ENABLED], - arrival, - departure, - ) - - response: RoutingResponse = self._api.public_transport_timetable( - origin, - destination, - True, - [ - RouteMode[self.config.route_mode], - RouteMode[self.config.travel_mode], - RouteMode[TRAFFIC_MODE_ENABLED], - ], - arrival=arrival, - departure=departure, - ) - - _LOGGER.debug("Raw response is: %s", response.response) - - attribution: str | None = None - if "sourceAttribution" in response.response: - attribution = build_hass_attribution( - response.response.get("sourceAttribution") - ) - route: list = response.response["route"] - summary: dict = route[0]["summary"] - waypoint: list = route[0]["waypoint"] - distance: float = summary["distance"] - traffic_time: float = summary["baseTime"] - if self.config.travel_mode in TRAVEL_MODES_VEHICLE: - traffic_time = summary["trafficTime"] - if self.config.units == IMPERIAL_UNITS: - # Convert to miles. - distance = DistanceConverter.convert( - distance, LENGTH_METERS, LENGTH_MILES - ) - else: - # Convert to kilometers - distance = distance / 1000 - return HERERoutingData( - { - ATTR_ATTRIBUTION: attribution, - ATTR_DURATION: round(summary["baseTime"] / 60), # type: ignore[misc] - ATTR_DURATION_IN_TRAFFIC: round(traffic_time / 60), - ATTR_DISTANCE: distance, - ATTR_ORIGIN: ",".join(origin), - ATTR_DESTINATION: ",".join(destination), - ATTR_ORIGIN_NAME: waypoint[0]["mappedRoadName"], - ATTR_DESTINATION_NAME: waypoint[1]["mappedRoadName"], - } - ) - except InvalidCoordinatesException as ex: - _LOGGER.error("Could not call HERE api: %s", ex) - return None - - def _prepare_parameters( - self, - ) -> tuple[list[str], list[str], str | None, str | None]: - """Prepare parameters for the HERE api.""" - - 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 = _from_entity_id(self.config.destination_entity_id) - else: - 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: - arrival = convert_time_to_isodate(self.config.arrival) - if self.config.departure is not None: - departure = convert_time_to_isodate(self.config.departure) - - if arrival is None and departure is None: - departure = "now" - - return (origin, destination, arrival, departure) - - -def build_hass_attribution(source_attribution: dict) -> str | None: - """Build a hass frontend ready string out of the sourceAttribution.""" - if (suppliers := source_attribution.get("supplier")) is not None: - supplier_titles = [] - for supplier in suppliers: - if (title := supplier.get("title")) is not None: - supplier_titles.append(title) - joined_supplier_titles = ",".join(supplier_titles) - return f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." - return None - - -def convert_time_to_isodate(simple_time: time) -> str: - """Take a time like 08:00:00 and combine it with the current date.""" - combined = datetime.combine(dt.start_of_local_day(), simple_time) - if combined < datetime.now(): - combined = combined + timedelta(days=1) - return combined.isoformat() - - -class InvalidCoordinatesException(Exception): - """Coordinates for origin or destination are malformed.""" diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index 38bd1742c9179d89a60dd4419a2e028b9bb0a673..c0b00ffc876df3e1d80b30eebd6896616c354e06 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -4,7 +4,14 @@ from __future__ import annotations import logging from typing import Any -from herepy import HEREError, InvalidCredentialsError, RouteMode, RoutingApi +from here_routing import ( + HERERoutingApi, + HERERoutingError, + HERERoutingUnauthorizedError, + Place, + TransportMode, +) +from here_transit import HERETransitError import voluptuous as vol from homeassistant import config_entries @@ -38,17 +45,14 @@ from .const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, IMPERIAL_UNITS, METRIC_UNITS, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_ENABLED, - TRAFFIC_MODES, TRAVEL_MODE_CAR, - TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_PUBLIC, TRAVEL_MODES, UNITS, ) @@ -56,26 +60,23 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def validate_api_key(api_key: str) -> None: +async def async_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", + known_working_origin = Place(latitude=38.9, longitude=-77.04833) + known_working_destination = Place(latitude=39.0, longitude=-77.1) + + await HERERoutingApi(api_key).route( + origin=known_working_origin, + destination=known_working_destination, + transport_mode=TransportMode.CAR, ) def get_user_step_schema(data: dict[str, Any]) -> vol.Schema: """Get a populated schema or default.""" + travel_mode = data.get(CONF_MODE, TRAVEL_MODE_CAR) + if travel_mode == "publicTransportTimeTable": + travel_mode = TRAVEL_MODE_PUBLIC return vol.Schema( { vol.Optional( @@ -92,7 +93,6 @@ def get_user_step_schema(data: dict[str, Any]) -> vol.Schema: def default_options(hass: HomeAssistant) -> dict[str, str | None]: """Get the default options.""" default = { - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_ARRIVAL_TIME: None, CONF_DEPARTURE_TIME: None, @@ -128,12 +128,10 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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: + await async_validate_api_key(user_input[CONF_API_KEY]) + except HERERoutingUnauthorizedError: errors["base"] = "invalid_auth" - except HEREError as error: + except (HERERoutingError, HERETransitError) as error: _LOGGER.exception("Unexpected exception: %s", error) errors["base"] = "unknown" if not errors: @@ -251,25 +249,14 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): """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"], + menu_options=["departure_time", "arrival_time", "no_time"], ) defaults = default_options(self.hass) schema = vol.Schema( { - vol.Optional( - CONF_TRAFFIC_MODE, - default=self.config_entry.options.get( - CONF_TRAFFIC_MODE, defaults[CONF_TRAFFIC_MODE] - ), - ): vol.In(TRAFFIC_MODES), vol.Optional( CONF_ROUTE_MODE, default=self.config_entry.options.get( diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index ea0dc5c136ea9702f893a6caa9443fe5a454d943..4fce053f7682f1b110b66ce92510c15f476a07d0 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -11,7 +11,6 @@ CONF_ORIGIN = "origin" 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" @@ -24,23 +23,17 @@ 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_MODES = [ TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, TRAVEL_MODE_PUBLIC, - TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, ] TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] -TRAFFIC_MODE_ENABLED = "traffic_enabled" -TRAFFIC_MODE_DISABLED = "traffic_disabled" -TRAFFIC_MODES = [TRAFFIC_MODE_ENABLED, TRAFFIC_MODE_DISABLED] - ROUTE_MODE_FASTEST = "fastest" ROUTE_MODE_SHORTEST = "shortest" ROUTE_MODES = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] @@ -55,7 +48,6 @@ ICONS = { TRAVEL_MODE_BICYCLE: ICON_BICYCLE, TRAVEL_MODE_PEDESTRIAN: ICON_PEDESTRIAN, TRAVEL_MODE_PUBLIC: ICON_PUBLIC, - TRAVEL_MODE_PUBLIC_TIME_TABLE: ICON_PUBLIC, TRAVEL_MODE_TRUCK: ICON_TRUCK, } @@ -69,10 +61,7 @@ ATTR_ORIGIN = "origin" ATTR_DESTINATION = "destination" ATTR_UNIT_SYSTEM = "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" - -NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py new file mode 100644 index 0000000000000000000000000000000000000000..de9bb9f1c6018f9d3c8e3df7bc1de3de48f96770 --- /dev/null +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -0,0 +1,290 @@ +"""The HERE Travel Time integration.""" +from __future__ import annotations + +from datetime import datetime, time, timedelta +import logging + +import here_routing +from here_routing import HERERoutingApi, Return, RoutingMode, Spans, TransportMode +import here_transit +from here_transit import HERETransitApi +import voluptuous as vol + +from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS, LENGTH_MILES +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.location import find_coordinates +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt +from homeassistant.util.unit_conversion import DistanceConverter + +from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, + ATTR_DURATION, + ATTR_DURATION_IN_TRAFFIC, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + IMPERIAL_UNITS, + ROUTE_MODE_FASTEST, +) +from .model import HERETravelTimeConfig, HERETravelTimeData + +_LOGGER = logging.getLogger(__name__) + + +class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): + """here_routing DataUpdateCoordinator.""" + + def __init__( + self, + hass: HomeAssistant, + api_key: str, + config: HERETravelTimeConfig, + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._api = HERERoutingApi(api_key) + self.config = config + + async def _async_update_data(self) -> HERETravelTimeData | None: + """Get the latest data from the HERE Routing API.""" + origin, destination, arrival, departure = prepare_parameters( + self.hass, self.config + ) + + route_mode = ( + RoutingMode.FAST + if self.config.route_mode == ROUTE_MODE_FASTEST + else RoutingMode.SHORT + ) + + _LOGGER.debug( + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, arrival: %s, departure: %s", + origin, + destination, + route_mode, + TransportMode(self.config.travel_mode), + arrival, + departure, + ) + + response = await self._api.route( + transport_mode=TransportMode(self.config.travel_mode), + origin=here_routing.Place(origin[0], origin[1]), + destination=here_routing.Place(destination[0], destination[1]), + routing_mode=route_mode, + arrival_time=arrival, + departure_time=departure, + return_values=[Return.POLYINE, Return.SUMMARY], + spans=[Spans.NAMES], + ) + + _LOGGER.debug("Raw response is: %s", response) + + return self._parse_routing_response(response) + + def _parse_routing_response(self, response) -> HERETravelTimeData: + """Parse the routing response dict to a HERETravelTimeData.""" + section: dict = response["routes"][0]["sections"][0] + summary: dict = section["summary"] + mapped_origin_lat: float = section["departure"]["place"]["location"]["lat"] + mapped_origin_lon: float = section["departure"]["place"]["location"]["lng"] + mapped_destination_lat: float = section["arrival"]["place"]["location"]["lat"] + mapped_destination_lon: float = section["arrival"]["place"]["location"]["lng"] + distance: float = summary["length"] + if self.config.units == IMPERIAL_UNITS: + # Convert to miles. + distance = DistanceConverter.convert(distance, LENGTH_METERS, LENGTH_MILES) + else: + # Convert to kilometers + distance = distance / 1000 + origin_name: str | None = None + if (names := section["spans"][0].get("names")) is not None: + origin_name = names[0]["value"] + destination_name: str | None = None + if (names := section["spans"][-1].get("names")) is not None: + destination_name = names[0]["value"] + return HERETravelTimeData( + { + ATTR_ATTRIBUTION: None, + ATTR_DURATION: round(summary["baseDuration"] / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(summary["duration"] / 60), + ATTR_DISTANCE: distance, + ATTR_ORIGIN: f"{mapped_origin_lat},{mapped_origin_lon}", + ATTR_DESTINATION: f"{mapped_destination_lat},{mapped_destination_lon}", + ATTR_ORIGIN_NAME: origin_name, + ATTR_DESTINATION_NAME: destination_name, + } + ) + + +class HERETransitDataUpdateCoordinator(DataUpdateCoordinator): + """HERETravelTime DataUpdateCoordinator.""" + + def __init__( + self, + hass: HomeAssistant, + api_key: str, + config: HERETravelTimeConfig, + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._api = HERETransitApi(api_key) + self.config = config + + async def _async_update_data(self) -> HERETravelTimeData | None: + """Get the latest data from the HERE Routing API.""" + origin, destination, arrival, departure = prepare_parameters( + self.hass, self.config + ) + + _LOGGER.debug( + "Requesting transit route for origin: %s, destination: %s, arrival: %s, departure: %s", + origin, + destination, + arrival, + departure, + ) + + response = await self._api.route( + origin=here_transit.Place(latitude=origin[0], longitude=origin[1]), + destination=here_transit.Place( + latitude=destination[0], longitude=destination[1] + ), + arrival_time=arrival, + departure_time=departure, + return_values=[ + here_transit.Return.POLYLINE, + here_transit.Return.TRAVEL_SUMMARY, + ], + ) + + _LOGGER.debug("Raw response is: %s", response) + + return self._parse_transit_response(response) + + def _parse_transit_response(self, response) -> HERETravelTimeData: + """Parse the transit response dict to a HERETravelTimeData.""" + sections: dict = response["routes"][0]["sections"] + attribution: str | None = build_hass_attribution(sections) + mapped_origin_lat: float = sections[0]["departure"]["place"]["location"]["lat"] + mapped_origin_lon: float = sections[0]["departure"]["place"]["location"]["lng"] + mapped_destination_lat: float = sections[-1]["arrival"]["place"]["location"][ + "lat" + ] + mapped_destination_lon: float = sections[-1]["arrival"]["place"]["location"][ + "lng" + ] + distance: float = sum( + section["travelSummary"]["length"] for section in sections + ) + duration: float = sum( + section["travelSummary"]["duration"] for section in sections + ) + if self.config.units == IMPERIAL_UNITS: + # Convert to miles. + distance = DistanceConverter.convert(distance, LENGTH_METERS, LENGTH_MILES) + else: + # Convert to kilometers + distance = distance / 1000 + return HERETravelTimeData( + { + ATTR_ATTRIBUTION: attribution, + ATTR_DURATION: round(duration / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(duration / 60), + ATTR_DISTANCE: distance, + ATTR_ORIGIN: f"{mapped_origin_lat},{mapped_origin_lon}", + ATTR_DESTINATION: f"{mapped_destination_lat},{mapped_destination_lon}", + ATTR_ORIGIN_NAME: sections[0]["departure"]["place"].get("name"), + ATTR_DESTINATION_NAME: sections[-1]["arrival"]["place"].get("name"), + } + ) + + +def prepare_parameters( + hass: HomeAssistant, + config: HERETravelTimeConfig, +) -> tuple[list[str], list[str], str | None, str | None]: + """Prepare parameters for the HERE api.""" + + def _from_entity_id(entity_id: str) -> list[str]: + coordinates = find_coordinates(hass, entity_id) + if coordinates is None: + raise InvalidCoordinatesException(f"No coordinates found for {entity_id}") + try: + formatted_coordinates = coordinates.split(",") + vol.Schema(cv.gps(formatted_coordinates)) + except (AttributeError, vol.ExactSequenceInvalid) as ex: + raise InvalidCoordinatesException( + f"{coordinates} are not valid coordinates" + ) from ex + return formatted_coordinates + + # Destination + if config.destination_entity_id is not None: + destination = _from_entity_id(config.destination_entity_id) + else: + destination = [ + str(config.destination_latitude), + str(config.destination_longitude), + ] + + # Origin + if config.origin_entity_id is not None: + origin = _from_entity_id(config.origin_entity_id) + else: + origin = [ + str(config.origin_latitude), + str(config.origin_longitude), + ] + + # Arrival/Departure + arrival: str | None = None + departure: str | None = None + if config.arrival is not None: + arrival = convert_time_to_isodate(config.arrival) + if config.departure is not None: + departure = convert_time_to_isodate(config.departure) + + return (origin, destination, arrival, departure) + + +def build_hass_attribution(sections: dict) -> str | None: + """Build a hass frontend ready string out of the attributions.""" + relevant_attributions = [] + for section in sections: + if (attributions := section.get("attributions")) is not None: + for attribution in attributions: + if (href := attribution.get("href")) is not None: + relevant_attributions.append(f"{href}") + if (text := attribution.get("text")) is not None: + relevant_attributions.append(text) + if len(relevant_attributions) > 0: + return ",".join(relevant_attributions) + return None + + +def convert_time_to_isodate(simple_time: time) -> str: + """Take a time like 08:00:00 and combine it with the current date.""" + combined = datetime.combine(dt.start_of_local_day(), simple_time) + if combined < datetime.now(): + combined = combined + timedelta(days=1) + return combined.isoformat() + + +class InvalidCoordinatesException(Exception): + """Coordinates for origin or destination are malformed.""" diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 683703112544773d1a65c7d2f91e27f67452a324..8efcf29b6b093d5c8588faa90e562a5fd283e0e4 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,8 +3,8 @@ "name": "HERE Travel Time", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/here_travel_time", - "requirements": ["herepy==2.0.0"], + "requirements": ["here_routing==0.1.1", "here_transit==1.0.0"], "codeowners": ["@eifinger"], "iot_class": "cloud_polling", - "loggers": ["herepy"] + "loggers": ["here_routing", "here_transit"] } diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index 7310ac24e77eea168f937b61c27a4e02978ef79a..737aae78a03d296776a39a86a554843b450ce680 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -6,8 +6,8 @@ from datetime import time from typing import TypedDict -class HERERoutingData(TypedDict): - """Routing information calculated from a herepy.RoutingResponse.""" +class HERETravelTimeData(TypedDict): + """Routing information.""" ATTR_ATTRIBUTION: str | None ATTR_DURATION: float diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 1ee0087eab7fba4b19d51178cfbcd95ddf744ed3..1fec002377e177bb65a088735f3c2e5e14f9f3da 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -28,7 +28,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.start import async_at_start from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import HereTravelTimeDataUpdateCoordinator from .const import ( ATTR_DESTINATION, ATTR_DESTINATION_NAME, @@ -42,6 +41,7 @@ from .const import ( ICONS, IMPERIAL_UNITS, ) +from .coordinator import HERERoutingDataUpdateCoordinator SCAN_INTERVAL = timedelta(minutes=5) @@ -101,7 +101,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): unique_id_prefix: str, name: str, sensor_description: SensorEntityDescription, - coordinator: HereTravelTimeDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -146,7 +146,7 @@ class OriginSensor(HERETravelTimeSensor): self, unique_id_prefix: str, name: str, - coordinator: HereTravelTimeDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" sensor_description = SensorEntityDescription( @@ -174,7 +174,7 @@ class DestinationSensor(HERETravelTimeSensor): self, unique_id_prefix: str, name: str, - coordinator: HereTravelTimeDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" sensor_description = SensorEntityDescription( @@ -202,7 +202,7 @@ class DistanceSensor(HERETravelTimeSensor): self, unique_id_prefix: str, name: str, - coordinator: HereTravelTimeDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" sensor_description = SensorEntityDescription( diff --git a/requirements_all.txt b/requirements_all.txt index 03778c2609efce85551ad2921ffb2d4ae29ad4dd..9252d244a53bd5b6bab64cf751b8b5eb0728563d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,10 @@ hdate==0.10.4 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -herepy==2.0.0 +here_routing==0.1.1 + +# homeassistant.components.here_travel_time +here_transit==1.0.0 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 983c60b3f7434dfe938ad61d8ed81085af40de8a..3a8681f3d9a187bbb230782de40f2570021a98fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -654,7 +654,10 @@ hatasmota==0.6.1 hdate==0.10.4 # homeassistant.components.here_travel_time -herepy==2.0.0 +here_routing==0.1.1 + +# homeassistant.components.here_travel_time +here_transit==1.0.0 # homeassistant.components.hlk_sw16 hlk-sw16==0.0.9 diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py index 368b070428eb5db417983de89efe7bfbd51c3b17..8069583df76c5b776c6027e28b0e5b22b4b3331a 100644 --- a/tests/components/here_travel_time/conftest.py +++ b/tests/components/here_travel_time/conftest.py @@ -2,37 +2,39 @@ import json from unittest.mock import patch -from herepy.models import RoutingResponse import pytest from tests.common import load_fixture -RESPONSE = RoutingResponse.new_from_jsondict( - json.loads(load_fixture("here_travel_time/car_response.json")) +RESPONSE = json.loads(load_fixture("here_travel_time/car_response.json")) +TRANSIT_RESPONSE = json.loads( + load_fixture("here_travel_time/transit_route_response.json") ) -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")) +NO_ATTRIBUTION_TRANSIT_RESPONSE = json.loads( + load_fixture("here_travel_time/no_attribution_transit_route_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(): """Return valid api response.""" with patch( - "herepy.RoutingApi.public_transport_timetable", + "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE + ), patch( + "here_routing.HERERoutingApi.route", 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.""" +@pytest.fixture(name="no_attribution_response") +def no_attribution_response_fixture(): + """Return valid api response without attribution.""" with patch( - "herepy.RoutingApi.public_transport_timetable", - return_value=EMPTY_ATTRIBUTION_RESPONSE, + "here_transit.HERETransitApi.route", + return_value=NO_ATTRIBUTION_TRANSIT_RESPONSE, + ), patch( + "here_routing.HERERoutingApi.route", + return_value=RESPONSE, ) as mock: yield mock diff --git a/tests/components/here_travel_time/const.py b/tests/components/here_travel_time/const.py index 0cc3143bc0bb76cae2284f71cda5e3bac0855f7c..167fd51dc5bb6dc5a0fff2e1b64b27296515ec69 100644 --- a/tests/components/here_travel_time/const.py +++ b/tests/components/here_travel_time/const.py @@ -1,8 +1,27 @@ """Constants for HERE Travel Time tests.""" +from homeassistant.components.here_travel_time.const import ( + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + TRAVEL_MODE_CAR, +) +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME + API_KEY = "test" -CAR_ORIGIN_LATITUDE = "38.9" -CAR_ORIGIN_LONGITUDE = "-77.04833" -CAR_DESTINATION_LATITUDE = "39.0" -CAR_DESTINATION_LONGITUDE = "-77.1" +ORIGIN_LATITUDE = "38.9" +ORIGIN_LONGITUDE = "-77.04833" +DESTINATION_LATITUDE = "39.0" +DESTINATION_LONGITUDE = "-77.1" + +DEFAULT_CONFIG = { + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", +} diff --git a/tests/components/here_travel_time/fixtures/car_response.json b/tests/components/here_travel_time/fixtures/car_response.json index cd479b2c9470148a498fbf987975e241c0c76dd7..99a2d9ef05178076eb30723f22145ecdca2f1545 100644 --- a/tests/components/here_travel_time/fixtures/car_response.json +++ b/tests/components/here_travel_time/fixtures/car_response.json @@ -1,304 +1,208 @@ { - "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 + "routes": [ + { + "id": "bace556c-2a16-4d93-a6c0-f595bbc89aa6", + "sections": [ + { + "id": "3ef521f3-7f88-480b-9b84-567a352c323c", + "type": "vehicle", + "departure": { + "time": "2022-10-24T05:53:00-04:00", + "place": { + "type": "place", + "location": { + "lat": 38.8999937, + "lng": -77.0479682 + }, + "originalLocation": { + "lat": 38.9, + "lng": -77.0483301 + } + } + }, + "arrival": { + "time": "2022-10-24T06:22:36-04:00", + "place": { + "type": "place", + "location": { + "lat": 38.99997, + "lng": -77.10014 + }, + "originalLocation": { + "lat": 38.9999999, + "lng": -77.1000001 + } + } + }, + "summary": { + "duration": 1776, + "length": 13682, + "baseDuration": 1571 + }, + "polyline": "BG0xomqC_p0-yE7ZXA_dA3XArnBTnpBkXAsYUkXAo4BTgZAA8GoB0F8BoG4DwHkDsEgFsEsEwC4D8BwCoB8BUwCU4DU0FT4DTsE7BkD7BwC7BgFzFwCrEkNwMkNwMwW8VgKsJoLAgPA4cUgeAwqBA0PAwHA8GA0yBAwbU4NA0FAgPAsTAoBAsnBAsEAkXAwvBU8VoBwMoBwMjD4NUkIUkD3IsEvMsEnLkI7V8GzUsE7LkD3IsEUoBAwCAoBAwCT8BToBTwC7BoBnB8BvCoB7BoBvCoBjDUvCA_EwCnLwC3IwCvHwC3I4D_E4DrE4D_EwCjD8BvCwCjDwCjDwC3DgFvHgFvH0KnQwRrY4XvgBkInLkDrEwCjDwHzK8GrJ0FjI0KrOgKjNwHzKsJjN8QjXkNjSwlBnzBoazjBwbnkB4D_EgenpBwCjDgFnGgF7G4cjmBoB7B4DnGwRzZkSrY4S_YgK3NoG3I0K_O4DzFoBnB4XvgB0K_OopBz3BoQrT4IjIoGrEkN_E8ajIkNrEoV_J0PrJsJvHwH7G4N3N0K3NgKjNkI7LsE7GwCrEoG_O4D_JsEvM0FnV0FjcoBvW8B3NwC_JwHvWUvCsJ7QokBnzBsTnasOrTwM7QsJjNoB7BoB7BsYvgB4I7LkNjSsTnawRrYgPnV4hB_sBgK3NoGrJwCjD0ZkDgZsEsEUkNwC4DU0UwCwlB0F0K8Bk_B0K4hBoG4I8B0FUgZkD0UwC8GUsYAkSAkSnB0KvC4rBjSkNzF0PvHwR3IoBToQjI0UnLwRrJwWvMwCnBozB3c4rB3XsT_JoVzKgjB_ToQrJsJzF8G3D0Z_OoL7GkrB3XoV7L0jBrTkIrE0P3IwCnBwWvM8VvMoBTwW7LkIrE4I_E4NvHoGjDsTnLwHrEkI_E4D7BoBT0FjDwMvHoBT4IrE4D7BsdzP0FjDgKzFoL7G4I_E8BnBoa_O4X3NoQ3IsTzK8BnBsJ_EwCnBgKrEwHjDsJvCwH7B8anG8QjD0PrE0ZnGoajI4D7B8zBvbgKrE8BnBwWnLwR3I0K_EoQ3I4I_EoLnGoBT4IrEgK_EofzP4X7L0UzKsxBzZkIrE4IrEoVzKsTrJ8QjIoGjDwH3DkcrOwWnLwR3I0UzKsYvMoajN08BnfwW7LwRjD0PnG0PnGwHjD0P7GgjBvRoVnLoVnL0PjI8VnLkmB3SoVzK8BnB4S3IoajN0ejN4SjIwHvCkS7GoBTwR_E8QrEoV_Ek6B7L8GnBoa3DwgB3D84B_J8zB7G03B_JsnBnGoQvCsYrEwHnB08B_Jo4BrJoVrE8L3D8GvC8QnG4NnGoQ7GkSjIsT_JkSrJ0UzK0Z3NgFvCwMnGoLzF0U_J4D7BwWnL4SrJ4D7BkmB3SwMnGoLzFkDnBkIrE0FvCof_O8ajN8V_JgK_EgZnL4N7G8QjI0Z3IkNrE8BToGjD0F3DwHzFsJvCwgBjIgU_EwMjD4DT4DTgFnBkhB3IoLjDoBAwqBrJ4DTwqB3IwHnBsTrEwH7BkS3DkI7BwCT4X_E8BToBA4I7BgKvCoQjDsY_E8azF0PjD8LjDwMvCgUzF0enGwR3DkI7B0UrEgUrEwR3D0PjDoQjD8L7BoVjD0P7B8GTsO7BgejD8Q7BoQ7BwMnBsJnBwHTgejDkSnBgPnBkNT0UnBoBAwHTwHT0UvC4N7B0KT4DT8BAT3DA_ET_d8BrOwC_J8GnQgPjXsEzFsJ7QsJ3XwHvWU_OoBvWAnGTzP_iBjSzK3D_JnB7LAvW8B7LUv5BsEoGgPoGsJ0FoGsEwC4D8BkDU8VToGA4SoGwHnBsJ3DoQA4DT8L7BgFkD8BwCUsE", + "spans": [ + { + "offset": 0, + "names": [ + { + "value": "22nd St NW", + "language": "en" + } + ] }, - "originalPosition": { - "latitude": 38.9, - "longitude": -77.0483301 + { + "offset": 1, + "names": [ + { + "value": "H St NW", + "language": "en" + } + ] }, - "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 + { + "offset": 5, + "names": [ + { + "value": "23rd St NW", + "language": "en" + } + ] }, - "originalPosition": { - "latitude": 38.9999999, - "longitude": -77.1000001 + { + "offset": 10, + "names": [ + { + "value": "Washington Cir NW", + "language": "en" + } + ] }, - "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" + { + "offset": 29, + "names": [ + { + "value": "New Hampshire Ave NW", + "language": "en" + } + ] }, - "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" + { + "offset": 33, + "names": [ + { + "value": "22nd St NW", + "language": "en" + } + ] }, - "length": 23903, - "travelTime": 1884, - "maneuver": [ - { - "position": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "instruction": "Head toward <span class=\"toward_street\">I St NW</span> on <span class=\"street\">22nd St NW</span>. <span class=\"distance-description\">Go for <span class=\"length\">279 m</span>.</span>", - "travelTime": 95, - "length": 279, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9021051, - "longitude": -77.048825 - }, - "instruction": "Turn <span class=\"direction\">left</span> toward <span class=\"sign\">Pennsylvania Ave NW</span>. <span class=\"distance-description\">Go for <span class=\"length\">71 m</span>.</span>", - "travelTime": 21, - "length": 71, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.902545, - "longitude": -77.0494151 - }, - "instruction": "Take the <span class=\"exit\">3rd exit</span> from Washington Cir NW roundabout onto <span class=\"next-street\">K St NW</span>. <span class=\"distance-description\">Go for <span class=\"length\">352 m</span>.</span>", - "travelTime": 90, - "length": 352, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9026523, - "longitude": -77.0529449 - }, - "instruction": "Keep <span class=\"direction\">left</span> onto <span class=\"next-street\">K St NW</span> <span class=\"number\">(US-29)</span>. <span class=\"distance-description\">Go for <span class=\"length\">201 m</span>.</span>", - "travelTime": 30, - "length": 201, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9025235, - "longitude": -77.0552516 - }, - "instruction": "Keep <span class=\"direction\">right</span> onto <span class=\"next-street\">Whitehurst Fwy</span> <span class=\"number\">(US-29)</span>. <span class=\"distance-description\">Go for <span class=\"length\">1.4 km</span>.</span>", - "travelTime": 131, - "length": 1381, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9050448, - "longitude": -77.0701969 - }, - "instruction": "Turn <span class=\"direction\">left</span> onto <span class=\"next-street\">M St NW</span>. <span class=\"distance-description\">Go for <span class=\"length\">784 m</span>.</span>", - "travelTime": 78, - "length": 784, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9060318, - "longitude": -77.0790696 - }, - "instruction": "Turn <span class=\"direction\">slightly left</span> onto <span class=\"next-street\">Canal Rd NW</span>. <span class=\"distance-description\">Go for <span class=\"length\">4.2 km</span>.</span>", - "travelTime": 277, - "length": 4230, - "id": "M7", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9303219, - "longitude": -77.1117926 - }, - "instruction": "Continue on <span class=\"next-street\">Clara Barton Pkwy</span>. <span class=\"distance-description\">Go for <span class=\"length\">844 m</span>.</span>", - "travelTime": 55, - "length": 844, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9368558, - "longitude": -77.1166742 - }, - "instruction": "Continue on <span class=\"next-street\">Clara Barton Pkwy</span>. <span class=\"distance-description\">Go for <span class=\"length\">4.7 km</span>.</span>", - "travelTime": 298, - "length": 4652, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9706838, - "longitude": -77.1461463 - }, - "instruction": "Keep <span class=\"direction\">right</span> onto <span class=\"next-street\">Cabin John Pkwy N</span> toward <span class=\"sign\"><span lang=\"en\">I-495 N</span></span>. <span class=\"distance-description\">Go for <span class=\"length\">2.1 km</span>.</span>", - "travelTime": 91, - "length": 2069, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9858222, - "longitude": -77.1571326 - }, - "instruction": "Take <span class=\"direction\">left</span> ramp onto <span class=\"number\">I-495 N</span> <span class=\"next-street\">(Capital Beltway)</span>. <span class=\"distance-description\">Go for <span class=\"length\">5.5 km</span>.</span>", - "travelTime": 238, - "length": 5538, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0153587, - "longitude": -77.1221781 - }, - "instruction": "Take exit <span class=\"exit\">36</span> toward <span class=\"sign\"><span lang=\"en\">Bethesda</span></span> onto <span class=\"number\">MD-187 S</span> <span class=\"next-street\">(Old Georgetown Rd)</span>. <span class=\"distance-description\">Go for <span class=\"length\">2.4 km</span>.</span>", - "travelTime": 211, - "length": 2365, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9981818, - "longitude": -77.1093571 - }, - "instruction": "Turn <span class=\"direction\">left</span> onto <span class=\"next-street\">Lincoln Dr</span>. <span class=\"distance-description\">Go for <span class=\"length\">506 m</span>.</span>", - "travelTime": 127, - "length": 506, - "id": "M13", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9987397, - "longitude": -77.1037138 - }, - "instruction": "Turn <span class=\"direction\">right</span> onto <span class=\"next-street\">Service Rd W</span>. <span class=\"distance-description\">Go for <span class=\"length\">121 m</span>.</span>", - "travelTime": 36, - "length": 121, - "id": "M14", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9976454, - "longitude": -77.1036172 - }, - "instruction": "Turn <span class=\"direction\">left</span> onto <span class=\"next-street\">Service Rd S</span>. <span class=\"distance-description\">Go for <span class=\"length\">510 m</span>.</span>", - "travelTime": 106, - "length": 510, - "id": "M15", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "instruction": "Arrive at <span class=\"street\">Service Rd S</span>. 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" + { + "offset": 57, + "names": [ + { + "value": "Massachusetts Ave NW", + "language": "en" + } + ] + }, + { + "offset": 64, + "names": [ + { + "value": "Massachusetts Ave NW", + "language": "en" + }, + { + "value": "Sheridan Cir NW", + "language": "en" + } + ] + }, + { + "offset": 78, + "names": [ + { + "value": "Sheridan Cir NW", + "language": "en" + }, + { + "value": "Massachusetts Ave NW", + "language": "en" + } + ] + }, + { + "offset": 79, + "names": [ + { + "value": "Massachusetts Ave NW", + "language": "en" + } + ] + }, + { + "offset": 174, + "names": [ + { + "value": "Wisconsin Ave NW", + "language": "en" + } + ] + }, + { + "offset": 291, + "names": [ + { + "value": "Wisconsin Ave", + "language": "en" + } + ] + }, + { + "offset": 408, + "names": [ + { + "value": "Rockville Pike", + "language": "en" + }, + { + "value": "Wisconsin Ave", + "language": "en" + } + ] + }, + { + "offset": 428, + "names": [ + { + "value": "South Dr", + "language": "en" + } + ] + }, + { + "offset": 443, + "names": [ + { + "value": "Center Dr", + "language": "en" + } + ] + }, + { + "offset": 450, + "names": [ + { + "value": "Service Rd S", + "language": "en" + } + ] + } ], - "text": "The trip takes <span class=\"length\">23.9 km</span> and <span class=\"time\">31 mins</span>.", - "travelTime": 1861, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us", - "sourceAttribution": { - "attribution": "With the support of <span class=\"company\"><a href=\"https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com\">HERE Technologies</a></span>. All information is provided without warranty of any kind.", - "supplier": [ - { - "title": "HERE Technologies", - "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" + "transport": { + "mode": "car" + } } ] } - } + ] } diff --git a/tests/components/here_travel_time/fixtures/empty_attribution_response.json b/tests/components/here_travel_time/fixtures/empty_attribution_response.json deleted file mode 100644 index cc1bb20a3733671bab27c042c5ae326081cb429b..0000000000000000000000000000000000000000 --- a/tests/components/here_travel_time/fixtures/empty_attribution_response.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "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 <span class=\"street\">Service Rd S</span>. 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 <span class=\"length\">23.9 km</span> and <span class=\"time\">31 mins</span>.", - "travelTime": 1861, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us", - "sourceAttribution": {} - } -} diff --git a/tests/components/here_travel_time/fixtures/no_attribution_transit_route_response.json b/tests/components/here_travel_time/fixtures/no_attribution_transit_route_response.json new file mode 100644 index 0000000000000000000000000000000000000000..1057a4a66b18b8dc37367ad21c616784ffe238f3 --- /dev/null +++ b/tests/components/here_travel_time/fixtures/no_attribution_transit_route_response.json @@ -0,0 +1,145 @@ +{ + "routes": [ + { + "id": "C0", + "sections": [ + { + "id": "C0-S0", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 1111, + "instruction": "Head west on Wilhelm-Fay-Straße. Go for 1.1 km.", + "length": 1099, + "offset": 0 + }, + { + "action": "roundaboutExit", + "duration": 73, + "instruction": "Walk right around the roundabout and turn at the 1st street Frankfurter Straße. Go for 63 m.", + "length": 63, + "offset": 40, + "exit": 1, + "direction": "right" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Frankfurter Straße. Your destination is on the left.", + "length": 0, + "offset": 47 + } + ], + "travelSummary": { + "duration": 1140, + "length": 1162 + }, + "departure": { + "time": "2022-07-19T15:39:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.127787, + "lng": 8.582082 + } + } + }, + "arrival": { + "time": "2022-07-19T15:58:00+02:00", + "place": { + "name": "Eschborn Alfred-Herrhausen-Allee", + "type": "station", + "location": { + "lat": 50.135176, + "lng": 8.572745 + }, + "id": "110439568" + } + }, + "polyline": "BGwhxz_Cgv5rQwDrQ8BvHkNr2BsE7LkI7V0FrO4I7QgF3IoGzKkSjXsTzU0FzFkXnV0tB7pBkN3N0FzF4DjD0KrJwW3S0UrJwWnG4cjI0FjDkDvC8BnB0F_EsErEkDrEkIrJ8G_J4IrOsJ_O8VzjB8Q7asJnQwbztB8G7L0FrJsE7GkNvW8BwC4D8BsEnB8BjDgFkIkD0FqHwL", + "transport": { + "mode": "pedestrian" + } + }, + { + "id": "C0-S3", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 166, + "instruction": "Head southwest on Hunsrückstraße. Go for 155 m.", + "length": 155, + "offset": 0 + }, + { + "action": "turn", + "duration": 91, + "instruction": "Turn right onto Stolberger Straße. Go for 82 m.", + "length": 82, + "offset": 4, + "direction": "right", + "severity": "quite" + }, + { + "action": "turn", + "duration": 476, + "instruction": "Turn left onto Horchheimer Straße. Go for 466 m.", + "length": 466, + "offset": 9, + "direction": "left", + "severity": "quite" + }, + { + "action": "turn", + "duration": 18, + "instruction": "Turn left onto Hessenring. Go for 18 m.", + "length": 18, + "offset": 21, + "direction": "left", + "severity": "quite" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Hessenring. Your destination is on the left.", + "length": 0, + "offset": 22 + } + ], + "travelSummary": { + "duration": 720, + "length": 721 + }, + "departure": { + "time": "2022-07-19T17:15:00+02:00", + "place": { + "name": "Wiesbaden-Nordenstadt Stolberger Straße", + "type": "station", + "location": { + "lat": 50.060615, + "lng": 8.344163 + }, + "id": "110812533" + } + }, + "arrival": { + "time": "2022-07-19T17:27:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.060941, + "lng": 8.336477 + } + } + }, + "polyline": "BG20uv_C4lp9P9nBn-B3DzFvC3DnQjX8GvHkDvC4DnBoGoBkX8L0F3cwCnLkN7iCnBzhCAnG8B3wBsEzZkD_OwCzP8GnkBwHjmB8GvlBhJ1G", + "transport": { + "mode": "pedestrian" + } + } + ] + } + ] +} diff --git a/tests/components/here_travel_time/fixtures/transit_route_response.json b/tests/components/here_travel_time/fixtures/transit_route_response.json new file mode 100644 index 0000000000000000000000000000000000000000..72b04a2d10ef5e43d4027055e51d9606797a2b00 --- /dev/null +++ b/tests/components/here_travel_time/fixtures/transit_route_response.json @@ -0,0 +1,153 @@ +{ + "routes": [ + { + "id": "C0", + "sections": [ + { + "id": "C0-S0", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 1111, + "instruction": "Head west on Wilhelm-Fay-Straße. Go for 1.1 km.", + "length": 1099, + "offset": 0 + }, + { + "action": "roundaboutExit", + "duration": 73, + "instruction": "Walk right around the roundabout and turn at the 1st street Frankfurter Straße. Go for 63 m.", + "length": 63, + "offset": 40, + "exit": 1, + "direction": "right" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Frankfurter Straße. Your destination is on the left.", + "length": 0, + "offset": 47 + } + ], + "travelSummary": { + "duration": 1140, + "length": 1162 + }, + "departure": { + "time": "2022-07-19T15:39:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.127787, + "lng": 8.582082 + } + } + }, + "arrival": { + "time": "2022-07-19T15:58:00+02:00", + "place": { + "name": "Eschborn Alfred-Herrhausen-Allee", + "type": "station", + "location": { + "lat": 50.135176, + "lng": 8.572745 + }, + "id": "110439568" + } + }, + "polyline": "BGwhxz_Cgv5rQwDrQ8BvHkNr2BsE7LkI7V0FrO4I7QgF3IoGzKkSjXsTzU0FzFkXnV0tB7pBkN3N0FzF4DjD0KrJwW3S0UrJwWnG4cjI0FjDkDvC8BnB0F_EsErEkDrEkIrJ8G_J4IrOsJ_O8VzjB8Q7asJnQwbztB8G7L0FrJsE7GkNvW8BwC4D8BsEnB8BjDgFkIkD0FqHwL", + "transport": { + "mode": "pedestrian" + } + }, + { + "id": "C0-S3", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 166, + "instruction": "Head southwest on Hunsrückstraße. Go for 155 m.", + "length": 155, + "offset": 0 + }, + { + "action": "turn", + "duration": 91, + "instruction": "Turn right onto Stolberger Straße. Go for 82 m.", + "length": 82, + "offset": 4, + "direction": "right", + "severity": "quite" + }, + { + "action": "turn", + "duration": 476, + "instruction": "Turn left onto Horchheimer Straße. Go for 466 m.", + "length": 466, + "offset": 9, + "direction": "left", + "severity": "quite" + }, + { + "action": "turn", + "duration": 18, + "instruction": "Turn left onto Hessenring. Go for 18 m.", + "length": 18, + "offset": 21, + "direction": "left", + "severity": "quite" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Hessenring. Your destination is on the left.", + "length": 0, + "offset": 22 + } + ], + "travelSummary": { + "duration": 720, + "length": 721 + }, + "departure": { + "time": "2022-07-19T17:15:00+02:00", + "place": { + "name": "Wiesbaden-Nordenstadt Stolberger Straße", + "type": "station", + "location": { + "lat": 50.060615, + "lng": 8.344163 + }, + "id": "110812533" + } + }, + "arrival": { + "time": "2022-07-19T17:27:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.060941, + "lng": 8.336477 + } + } + }, + "polyline": "BG20uv_C4lp9P9nBn-B3DzFvC3DnQjX8GvHkDvC4DnBoGoBkX8L0F3cwCnLkN7iCnBzhCAnG8B3wBsEzZkD_OwCzP8GnkBwHjmB8GvlBhJ1G", + "transport": { + "mode": "pedestrian" + }, + "attributions": [ + { + "id": "R00370b-C0-S2-link-0", + "href": "http://creativecommons.org/licenses/by/3.0/it/", + "text": "Some line names used in this product or service were edited to align with official transportation maps.", + "type": "disclaimer" + } + ] + } + ] + } + ] +} diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 120ffd828bc50a3593774aab22a2c322fe4d3125..d22d36a4d66f69496e29a4117d68233c124503f3 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -1,8 +1,7 @@ """Test the HERE Travel Time config flow.""" from unittest.mock import patch -from herepy import HEREError -from herepy.routing_api import InvalidCredentialsError +from here_routing import HERERoutingError, HERERoutingUnauthorizedError import pytest from homeassistant import config_entries, data_entry_flow @@ -15,12 +14,10 @@ from homeassistant.components.here_travel_time.const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, DOMAIN, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_CAR, - TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_PUBLIC, ) from homeassistant.const import ( CONF_API_KEY, @@ -39,10 +36,11 @@ from homeassistant.util.unit_system import ( from .const import ( API_KEY, - CAR_DESTINATION_LATITUDE, - CAR_DESTINATION_LONGITUDE, - CAR_ORIGIN_LATITUDE, - CAR_ORIGIN_LONGITUDE, + DEFAULT_CONFIG, + DESTINATION_LATITUDE, + DESTINATION_LONGITUDE, + ORIGIN_LATITUDE, + ORIGIN_LONGITUDE, ) from tests.common import MockConfigEntry @@ -83,12 +81,12 @@ async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.Flo 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_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, - CONF_MODE: TRAVEL_MODE_PUBLIC_TIME_TABLE, + CONF_MODE: TRAVEL_MODE_PUBLIC, CONF_NAME: "test", }, ) @@ -99,7 +97,6 @@ async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.Flo 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, }, @@ -120,8 +117,8 @@ async def origin_step_result_fixture( origin_menu_result["flow_id"], { "origin": { - "latitude": float(CAR_ORIGIN_LATITUDE), - "longitude": float(CAR_ORIGIN_LONGITUDE), + "latitude": float(ORIGIN_LATITUDE), + "longitude": float(ORIGIN_LONGITUDE), "radius": 3.0, } }, @@ -170,8 +167,8 @@ async def test_step_origin_coordinates( menu_result["flow_id"], { "origin": { - "latitude": float(CAR_ORIGIN_LATITUDE), - "longitude": float(CAR_ORIGIN_LONGITUDE), + "latitude": float(ORIGIN_LATITUDE), + "longitude": float(ORIGIN_LONGITUDE), "radius": 3.0, } }, @@ -210,8 +207,8 @@ async def test_step_destination_coordinates( menu_result["flow_id"], { "destination": { - "latitude": float(CAR_DESTINATION_LATITUDE), - "longitude": float(CAR_DESTINATION_LONGITUDE), + "latitude": float(DESTINATION_LATITUDE), + "longitude": float(DESTINATION_LONGITUDE), "radius": 3.0, } }, @@ -223,10 +220,10 @@ async def test_step_destination_coordinates( 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_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_MODE: TRAVEL_MODE_CAR, } @@ -261,15 +258,14 @@ async def test_step_destination_entity( 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_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), CONF_DESTINATION_ENTITY_ID: "zone.home", CONF_MODE: TRAVEL_MODE_CAR, } assert entry.options == { CONF_UNIT_SYSTEM: expected_unit_option, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, CONF_ARRIVAL_TIME: None, CONF_DEPARTURE_TIME: None, } @@ -282,8 +278,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) with patch( - "herepy.RoutingApi.public_transport_timetable", - side_effect=InvalidCredentialsError, + "here_routing.HERERoutingApi.route", + side_effect=HERERoutingUnauthorizedError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -305,8 +301,8 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) with patch( - "herepy.RoutingApi.public_transport_timetable", - side_effect=HEREError, + "here_routing.HERERoutingApi.route", + side_effect=HERERoutingError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -327,15 +323,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: 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", - }, + data=DEFAULT_CONFIG, ) entry.add_to_hass(hass) @@ -351,7 +339,6 @@ async def test_options_flow(hass: HomeAssistant) -> None: 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, }, @@ -381,7 +368,6 @@ async def test_options_flow_arrival_time_step( 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", } @@ -407,7 +393,6 @@ async def test_options_flow_departure_time_step( 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", } @@ -426,5 +411,4 @@ async def test_options_flow_no_time_step( assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, } diff --git a/tests/components/here_travel_time/test_init.py b/tests/components/here_travel_time/test_init.py index 05b7f6983dbc1915b60d6c1463921c8165df01e7..b8bbdec241a20c770aafc13b456ec7f030fe086b 100644 --- a/tests/components/here_travel_time/test_init.py +++ b/tests/components/here_travel_time/test_init.py @@ -3,24 +3,10 @@ import pytest from homeassistant.components.here_travel_time.config_flow import default_options -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.components.here_travel_time.const import DOMAIN from homeassistant.core import HomeAssistant -from .const import ( - API_KEY, - CAR_DESTINATION_LATITUDE, - CAR_DESTINATION_LONGITUDE, - CAR_ORIGIN_LATITUDE, - CAR_ORIGIN_LONGITUDE, -) +from .const import DEFAULT_CONFIG from tests.common import MockConfigEntry @@ -31,15 +17,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: 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", - }, + data=DEFAULT_CONFIG, options=default_options(hass), ) entry.add_to_hass(hass) diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 5cc4802d253ded0bac41eded720629592f100ad6..8a6f7f3f5d3dd889329b7ecc8320d94c8cc23459 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,8 +1,14 @@ """The test for the HERE Travel Time sensor platform.""" from unittest.mock import MagicMock, patch -from herepy.here_enum import RouteMode -from herepy.routing_api import NoRouteFoundError +from here_routing import ( + HERERoutingError, + Place, + Return, + RoutingMode, + Spans, + TransportMode, +) import pytest from homeassistant.components.here_travel_time.config_flow import default_options @@ -20,15 +26,14 @@ from homeassistant.components.here_travel_time.const import ( ICON_BICYCLE, ICON_CAR, ICON_PEDESTRIAN, - ICON_PUBLIC, ICON_TRUCK, - NO_ROUTE_ERROR_MESSAGE, + IMPERIAL_UNITS, + METRIC_UNITS, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, - TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_PUBLIC, TRAVEL_MODE_TRUCK, ) from homeassistant.const import ( @@ -51,10 +56,10 @@ from homeassistant.setup import async_setup_component from .const import ( API_KEY, - CAR_DESTINATION_LATITUDE, - CAR_DESTINATION_LONGITUDE, - CAR_ORIGIN_LATITUDE, - CAR_ORIGIN_LONGITUDE, + DESTINATION_LATITUDE, + DESTINATION_LONGITUDE, + ORIGIN_LATITUDE, + ORIGIN_LONGITUDE, ) from tests.common import MockConfigEntry @@ -69,9 +74,9 @@ from tests.common import MockConfigEntry "metric", None, None, + "26", + 13.682, "30", - 23.903, - "31", LENGTH_KILOMETERS, ), ( @@ -80,8 +85,8 @@ from tests.common import MockConfigEntry "metric", None, None, - "30", - 23.903, + "26", + 13.682, "30", LENGTH_KILOMETERS, ), @@ -91,19 +96,8 @@ from tests.common import MockConfigEntry "imperial", None, None, - "30", - 14.85263, - "30", - LENGTH_MILES, - ), - ( - TRAVEL_MODE_PUBLIC_TIME_TABLE, - ICON_PUBLIC, - "imperial", - "08:00:00", - None, - "30", - 14.85263, + "26", + 8.5016, "30", LENGTH_MILES, ), @@ -113,9 +107,9 @@ from tests.common import MockConfigEntry "metric", None, "08:00:00", + "26", + 13.682, "30", - 23.903, - "31", LENGTH_KILOMETERS, ), ], @@ -138,10 +132,10 @@ async def test_sensor( 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_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: mode, CONF_NAME: "test", @@ -161,10 +155,6 @@ async def test_sensor( duration = hass.states.get("sensor.test_duration") assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES - assert ( - duration.attributes.get(ATTR_ATTRIBUTION) - == "With the support of HERE Technologies. All information is provided without warranty of any kind." - ) assert duration.attributes.get(ATTR_ICON) == icon assert duration.state == expected_duration @@ -186,31 +176,21 @@ async def test_sensor( assert hass.states.get("sensor.test_origin").state == "22nd St NW" assert ( hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) - == CAR_ORIGIN_LATITUDE + == "38.8999937" ) assert ( hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) - == CAR_ORIGIN_LONGITUDE - ) - - assert hass.states.get("sensor.test_origin").state == "22nd St NW" - assert ( - hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) - == CAR_ORIGIN_LATITUDE - ) - assert ( - hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) - == CAR_ORIGIN_LONGITUDE + == "-77.0479682" ) assert hass.states.get("sensor.test_destination").state == "Service Rd S" assert ( hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) - == CAR_DESTINATION_LATITUDE + == "38.99997" ) assert ( hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) - == CAR_DESTINATION_LONGITUDE + == "-77.10014" ) @@ -227,8 +207,8 @@ async def test_circular_ref(hass: HomeAssistant, caplog): 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_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", @@ -242,22 +222,69 @@ async def test_circular_ref(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert "No coordinatnes found for test.first" in caplog.text + assert "No coordinates found for test.first" in caplog.text + + +@pytest.mark.usefixtures("valid_response") +@pytest.mark.parametrize( + "unit_system,expected_distance", + [ + (METRIC_UNITS, "1.883"), + (IMPERIAL_UNITS, "1.1700419549829"), + ], +) +async def test_public_transport( + hass: HomeAssistant, unit_system: str, expected_distance: str +): + """Test that public transport mode is handled.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_PUBLIC, + CONF_NAME: "test", + }, + options={ + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_ARRIVAL_TIME: "08:00:00", + CONF_DEPARTURE_TIME: None, + 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() + + assert ( + hass.states.get("sensor.test_duration").attributes.get(ATTR_ATTRIBUTION) + == "http://creativecommons.org/licenses/by/3.0/it/,Some line names used in this product or service were edited to align with official transportation maps." + ) + assert hass.states.get("sensor.test_distance").state == pytest.approx( + expected_distance + ) -@pytest.mark.usefixtures("empty_attribution_response") -async def test_no_attribution(hass: HomeAssistant): - """Test that an empty attribution is handled.""" +@pytest.mark.usefixtures("no_attribution_response") +async def test_no_attribution_response(hass: HomeAssistant): + """Test that no_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_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, - CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_MODE: TRAVEL_MODE_PUBLIC, CONF_NAME: "test", }, options=default_options(hass), @@ -280,8 +307,8 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): "zone": [ { "name": "Origin", - "latitude": CAR_ORIGIN_LATITUDE, - "longitude": CAR_ORIGIN_LONGITUDE, + "latitude": ORIGIN_LATITUDE, + "longitude": ORIGIN_LONGITUDE, "radius": 250, "passive": False, }, @@ -292,8 +319,8 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): "device_tracker.test", "not_home", { - "latitude": float(CAR_DESTINATION_LATITUDE), - "longitude": float(CAR_DESTINATION_LONGITUDE), + "latitude": float(DESTINATION_LATITUDE), + "longitude": float(DESTINATION_LONGITUDE), }, ) entry = MockConfigEntry( @@ -315,19 +342,17 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert hass.states.get("sensor.test_distance").state == "23.903" + assert hass.states.get("sensor.test_distance").state == "13.682" valid_response.assert_called_with( - [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], - [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE], - True, - [ - RouteMode[ROUTE_MODE_FASTEST], - RouteMode[TRAVEL_MODE_TRUCK], - RouteMode[TRAFFIC_MODE_ENABLED], - ], - arrival=None, - departure="now", + transport_mode=TransportMode.TRUCK, + origin=Place(ORIGIN_LATITUDE, ORIGIN_LONGITUDE), + destination=Place(DESTINATION_LATITUDE, DESTINATION_LONGITUDE), + routing_mode=RoutingMode.FAST, + arrival_time=None, + departure_time=None, + return_values=[Return.POLYINE, Return.SUMMARY], + spans=[Spans.NAMES], ) @@ -338,8 +363,8 @@ async def test_destination_entity_not_found(hass: HomeAssistant, caplog): domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), CONF_DESTINATION_ENTITY_ID: "device_tracker.test", CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, @@ -365,8 +390,8 @@ async def test_origin_entity_not_found(hass: HomeAssistant, caplog): 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_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", @@ -394,8 +419,8 @@ async def test_invalid_destination_entity_state(hass: HomeAssistant, caplog): domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), CONF_DESTINATION_ENTITY_ID: "device_tracker.test", CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, @@ -425,8 +450,8 @@ async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog): 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_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", @@ -446,17 +471,19 @@ async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog): async def test_route_not_found(hass: HomeAssistant, caplog): """Test that route not found error is correctly handled.""" with patch( - "herepy.RoutingApi.public_transport_timetable", - side_effect=NoRouteFoundError, + "here_routing.HERERoutingApi.route", + side_effect=HERERoutingError( + "Route calculation failed: Couldn't find a route." + ), ): 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_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", @@ -469,4 +496,4 @@ async def test_route_not_found(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert NO_ROUTE_ERROR_MESSAGE in caplog.text + assert "Route calculation failed: Couldn't find a route." in caplog.text