From 3844e2d5337e8195ee907de335187f104cab31c8 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 8 May 2024 07:56:17 +0200 Subject: [PATCH] Add service waze_travel_time.get_travel_times (#108170) * Add service waze_travel_time.get_travel_times * Align strings with home-assistant.io * Remove not needed service args * Use SelectSelectorConfig.sort * Move vehicle_type mangling to async_get_travel_times --- .../components/waze_travel_time/__init__.py | 166 +++++++++++++++++- .../waze_travel_time/config_flow.py | 9 +- .../components/waze_travel_time/icons.json | 3 + .../components/waze_travel_time/sensor.py | 86 ++++----- .../components/waze_travel_time/services.yaml | 57 ++++++ .../components/waze_travel_time/strings.json | 44 +++++ tests/components/waze_travel_time/conftest.py | 19 ++ .../components/waze_travel_time/test_init.py | 45 +++++ .../waze_travel_time/test_sensor.py | 14 -- 9 files changed, 367 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/waze_travel_time/services.yaml create mode 100644 tests/components/waze_travel_time/test_init.py diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 9c131f3242c..83b2e2aa7c7 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,24 +1,184 @@ """The waze_travel_time component.""" import asyncio +import logging + +from pywaze.route_calculator import CalcRoutesResponse, WazeRouteCalculator, WRCError +import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_REGION, Platform +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.selector import ( + BooleanSelector, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, +) -from .const import DOMAIN, SEMAPHORE +from .const import ( + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + CONF_DESTINATION, + CONF_ORIGIN, + CONF_REALTIME, + CONF_UNITS, + CONF_VEHICLE_TYPE, + DEFAULT_VEHICLE_TYPE, + DOMAIN, + METRIC_UNITS, + REGIONS, + SEMAPHORE, + UNITS, + VEHICLE_TYPES, +) PLATFORMS = [Platform.SENSOR] +SERVICE_GET_TRAVEL_TIMES = "get_travel_times" +SERVICE_GET_TRAVEL_TIMES_SCHEMA = vol.Schema( + { + vol.Required(CONF_ORIGIN): TextSelector(), + vol.Required(CONF_DESTINATION): TextSelector(), + vol.Required(CONF_REGION): SelectSelector( + SelectSelectorConfig( + options=REGIONS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_REGION, + sort=True, + ) + ), + vol.Optional(CONF_REALTIME, default=False): BooleanSelector(), + vol.Optional(CONF_VEHICLE_TYPE, default=DEFAULT_VEHICLE_TYPE): SelectSelector( + SelectSelectorConfig( + options=VEHICLE_TYPES, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_VEHICLE_TYPE, + sort=True, + ) + ), + vol.Optional(CONF_UNITS, default=METRIC_UNITS): SelectSelector( + SelectSelectorConfig( + options=UNITS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_UNITS, + sort=True, + ) + ), + vol.Optional(CONF_AVOID_TOLL_ROADS, default=False): BooleanSelector(), + vol.Optional(CONF_AVOID_SUBSCRIPTION_ROADS, default=False): BooleanSelector(), + vol.Optional(CONF_AVOID_FERRIES, default=False): BooleanSelector(), + } +) + +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" if SEMAPHORE not in hass.data.setdefault(DOMAIN, {}): hass.data.setdefault(DOMAIN, {})[SEMAPHORE] = asyncio.Semaphore(1) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + async def async_get_travel_times_service(service: ServiceCall) -> ServiceResponse: + httpx_client = get_async_client(hass) + client = WazeRouteCalculator( + region=service.data[CONF_REGION].upper(), client=httpx_client + ) + response = await async_get_travel_times( + client=client, + origin=service.data[CONF_ORIGIN], + destination=service.data[CONF_DESTINATION], + vehicle_type=service.data[CONF_VEHICLE_TYPE], + avoid_toll_roads=service.data[CONF_AVOID_TOLL_ROADS], + avoid_subscription_roads=service.data[CONF_AVOID_SUBSCRIPTION_ROADS], + avoid_ferries=service.data[CONF_AVOID_FERRIES], + realtime=service.data[CONF_REALTIME], + ) + return {"routes": [vars(route) for route in response]} if response else None + + hass.services.async_register( + DOMAIN, + SERVICE_GET_TRAVEL_TIMES, + async_get_travel_times_service, + SERVICE_GET_TRAVEL_TIMES_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) return True +async def async_get_travel_times( + client: WazeRouteCalculator, + origin: str, + destination: str, + vehicle_type: str, + avoid_toll_roads: bool, + avoid_subscription_roads: bool, + avoid_ferries: bool, + realtime: bool, + incl_filter: str | None = None, + excl_filter: str | None = None, +) -> list[CalcRoutesResponse] | None: + """Get all available routes.""" + + _LOGGER.debug( + "Getting update for origin: %s destination: %s", + origin, + destination, + ) + routes = [] + vehicle_type = "" if vehicle_type.upper() == "CAR" else vehicle_type.upper() + try: + routes = await client.calc_routes( + origin, + destination, + vehicle_type=vehicle_type, + avoid_toll_roads=avoid_toll_roads, + avoid_subscription_roads=avoid_subscription_roads, + avoid_ferries=avoid_ferries, + real_time=realtime, + alternatives=3, + ) + + if incl_filter not in {None, ""}: + routes = [ + r + for r in routes + if any( + incl_filter.lower() == street_name.lower() # type: ignore[union-attr] + for street_name in r.street_names + ) + ] + + if excl_filter not in {None, ""}: + routes = [ + r + for r in routes + if not any( + excl_filter.lower() == street_name.lower() # type: ignore[union-attr] + for street_name in r.street_names + ) + ] + + if len(routes) < 1: + _LOGGER.warning("No routes found") + return None + except WRCError as exp: + _LOGGER.warning("Error on retrieving data: %s", exp) + return None + + else: + return routes + + async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index d0f63b97b78..12dc8336f92 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -51,16 +51,18 @@ OPTIONS_SCHEMA = vol.Schema( vol.Optional(CONF_REALTIME): BooleanSelector(), vol.Required(CONF_VEHICLE_TYPE): SelectSelector( SelectSelectorConfig( - options=sorted(VEHICLE_TYPES), + options=VEHICLE_TYPES, mode=SelectSelectorMode.DROPDOWN, translation_key=CONF_VEHICLE_TYPE, + sort=True, ) ), vol.Required(CONF_UNITS): SelectSelector( SelectSelectorConfig( - options=sorted(UNITS), + options=UNITS, mode=SelectSelectorMode.DROPDOWN, translation_key=CONF_UNITS, + sort=True, ) ), vol.Optional(CONF_AVOID_TOLL_ROADS): BooleanSelector(), @@ -76,9 +78,10 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_DESTINATION): TextSelector(), vol.Required(CONF_REGION): SelectSelector( SelectSelectorConfig( - options=sorted(REGIONS), + options=REGIONS, mode=SelectSelectorMode.DROPDOWN, translation_key=CONF_REGION, + sort=True, ) ), } diff --git a/homeassistant/components/waze_travel_time/icons.json b/homeassistant/components/waze_travel_time/icons.json index 54d3183363e..fa95e8fdd8a 100644 --- a/homeassistant/components/waze_travel_time/icons.json +++ b/homeassistant/components/waze_travel_time/icons.json @@ -5,5 +5,8 @@ "default": "mdi:car" } } + }, + "services": { + "get_travel_times": "mdi:timelapse" } } diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 518de269bc5..7663b4a102e 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -8,7 +8,7 @@ import logging from typing import Any import httpx -from pywaze.route_calculator import WazeRouteCalculator, WRCError +from pywaze.route_calculator import WazeRouteCalculator from homeassistant.components.sensor import ( SensorDeviceClass, @@ -30,6 +30,7 @@ from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.location import find_coordinates from homeassistant.util.unit_conversion import DistanceConverter +from . import async_get_travel_times from .const import ( CONF_AVOID_FERRIES, CONF_AVOID_SUBSCRIPTION_ROADS, @@ -186,65 +187,38 @@ class WazeTravelTimeData: excl_filter = self.config_entry.options.get(CONF_EXCL_FILTER) realtime = self.config_entry.options[CONF_REALTIME] vehicle_type = self.config_entry.options[CONF_VEHICLE_TYPE] - vehicle_type = "" if vehicle_type.upper() == "CAR" else vehicle_type.upper() avoid_toll_roads = self.config_entry.options[CONF_AVOID_TOLL_ROADS] avoid_subscription_roads = self.config_entry.options[ CONF_AVOID_SUBSCRIPTION_ROADS ] avoid_ferries = self.config_entry.options[CONF_AVOID_FERRIES] - units = self.config_entry.options[CONF_UNITS] - - routes = {} - try: - routes = await self.client.calc_routes( - self.origin, - self.destination, - vehicle_type=vehicle_type, - avoid_toll_roads=avoid_toll_roads, - avoid_subscription_roads=avoid_subscription_roads, - avoid_ferries=avoid_ferries, - real_time=realtime, - alternatives=3, - ) - - if incl_filter not in {None, ""}: - routes = [ - r - for r in routes - if any( - incl_filter.lower() == street_name.lower() - for street_name in r.street_names - ) - ] - - if excl_filter not in {None, ""}: - routes = [ - r - for r in routes - if not any( - excl_filter.lower() == street_name.lower() - for street_name in r.street_names - ) - ] - - if len(routes) < 1: - _LOGGER.warning("No routes found") - return - + routes = await async_get_travel_times( + self.client, + self.origin, + self.destination, + vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, + realtime, + incl_filter, + excl_filter, + ) + if routes: route = routes[0] - - self.duration = route.duration - distance = route.distance - - if units == IMPERIAL_UNITS: - # Convert to miles. - self.distance = DistanceConverter.convert( - distance, UnitOfLength.KILOMETERS, UnitOfLength.MILES - ) - else: - self.distance = distance - - self.route = route.name - except WRCError as exp: - _LOGGER.warning("Error on retrieving data: %s", exp) + else: + _LOGGER.warning("No routes found") return + + self.duration = route.duration + distance = route.distance + + if self.config_entry.options[CONF_UNITS] == IMPERIAL_UNITS: + # Convert to miles. + self.distance = DistanceConverter.convert( + distance, UnitOfLength.KILOMETERS, UnitOfLength.MILES + ) + else: + self.distance = distance + + self.route = route.name diff --git a/homeassistant/components/waze_travel_time/services.yaml b/homeassistant/components/waze_travel_time/services.yaml new file mode 100644 index 00000000000..7fba565dd47 --- /dev/null +++ b/homeassistant/components/waze_travel_time/services.yaml @@ -0,0 +1,57 @@ +get_travel_times: + fields: + origin: + required: true + example: "38.9" + selector: + text: + destination: + required: true + example: "-77.04833" + selector: + text: + region: + required: true + default: "us" + selector: + select: + translation_key: region + options: + - us + - na + - eu + - il + - au + units: + default: "metric" + selector: + select: + translation_key: units + options: + - metric + - imperial + vehicle_type: + default: "car" + selector: + select: + translation_key: vehicle_type + options: + - car + - taxi + - motorcycle + realtime: + required: false + selector: + boolean: + avoid_toll_roads: + required: false + selector: + boolean: + avoid_ferries: + required: false + selector: + boolean: + avoid_subscription_roads: + required: false + selector: + boolean: diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json index e6dd3c3a22e..6b0b4184af7 100644 --- a/homeassistant/components/waze_travel_time/strings.json +++ b/homeassistant/components/waze_travel_time/strings.json @@ -60,5 +60,49 @@ "au": "Australia" } } + }, + "services": { + "get_travel_times": { + "name": "Get Travel Times", + "description": "Get route alternatives and travel times between two locations.", + "fields": { + "origin": { + "name": "[%key:component::waze_travel_time::config::step::user::data::origin%]", + "description": "The origin of the route." + }, + "destination": { + "name": "[%key:component::waze_travel_time::config::step::user::data::destination%]", + "description": "The destination of the route." + }, + "region": { + "name": "[%key:component::waze_travel_time::config::step::user::data::region%]", + "description": "The region. Controls which waze server is used." + }, + "units": { + "name": "[%key:component::waze_travel_time::options::step::init::data::units%]", + "description": "Which unit system to use." + }, + "vehicle_type": { + "name": "[%key:component::waze_travel_time::options::step::init::data::vehicle_type%]", + "description": "Which vehicle to use." + }, + "realtime": { + "name": "[%key:component::waze_travel_time::options::step::init::data::realtime%]", + "description": "Use real-time or statistical data." + }, + "avoid_toll_roads": { + "name": "[%key:component::waze_travel_time::options::step::init::data::avoid_toll_roads%]", + "description": "Whether to avoid toll roads." + }, + "avoid_ferries": { + "name": "[%key:component::waze_travel_time::options::step::init::data::avoid_ferries%]", + "description": "Whether to avoid ferries." + }, + "avoid_subscription_roads": { + "name": "[%key:component::waze_travel_time::options::step::init::data::avoid_subscription_roads%]", + "description": "Whether to avoid subscription roads. " + } + } + } } } diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index 01642ace86a..c929fc219f9 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -5,6 +5,25 @@ from unittest.mock import patch import pytest from pywaze.route_calculator import CalcRoutesResponse, WRCError +from homeassistant.components.waze_travel_time.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_config") +async def mock_config_fixture(hass: HomeAssistant, data, options): + """Mock a Waze Travel Time config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options=options, + entry_id="test", + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + @pytest.fixture(name="mock_update") def mock_update_fixture(): diff --git a/tests/components/waze_travel_time/test_init.py b/tests/components/waze_travel_time/test_init.py new file mode 100644 index 00000000000..58aaa8983a7 --- /dev/null +++ b/tests/components/waze_travel_time/test_init.py @@ -0,0 +1,45 @@ +"""Test waze_travel_time services.""" + +import pytest + +from homeassistant.components.waze_travel_time.const import DEFAULT_OPTIONS +from homeassistant.core import HomeAssistant + +from .const import MOCK_CONFIG + + +@pytest.mark.parametrize( + ("data", "options"), + [(MOCK_CONFIG, DEFAULT_OPTIONS)], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_service_get_travel_times(hass: HomeAssistant) -> None: + """Test service get_travel_times.""" + response_data = await hass.services.async_call( + "waze_travel_time", + "get_travel_times", + { + "origin": "location1", + "destination": "location2", + "vehicle_type": "car", + "region": "us", + }, + blocking=True, + return_response=True, + ) + assert response_data == { + "routes": [ + { + "distance": 300, + "duration": 150, + "name": "E1337 - Teststreet", + "street_names": ["E1337", "IncludeThis", "Teststreet"], + }, + { + "distance": 500, + "duration": 600, + "name": "E0815 - Otherstreet", + "street_names": ["E0815", "ExcludeThis", "Otherstreet"], + }, + ] + } diff --git a/tests/components/waze_travel_time/test_sensor.py b/tests/components/waze_travel_time/test_sensor.py index db0ece32cae..e09a7199ff4 100644 --- a/tests/components/waze_travel_time/test_sensor.py +++ b/tests/components/waze_travel_time/test_sensor.py @@ -24,20 +24,6 @@ from .const import MOCK_CONFIG from tests.common import MockConfigEntry -@pytest.fixture(name="mock_config") -async def mock_config_fixture(hass: HomeAssistant, data, options): - """Mock a Waze Travel Time config entry.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=data, - options=options, - entry_id="test", - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - @pytest.fixture(name="mock_update_wrcerror") def mock_update_wrcerror_fixture(mock_update): """Mock an update to the sensor failed with WRCError."""