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
pull/117066/head
Kevin Stillhammer 2024-05-08 07:56:17 +02:00 committed by GitHub
parent 7923471b94
commit 3844e2d533
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 367 additions and 76 deletions

View File

@ -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)

View File

@ -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,
)
),
}

View File

@ -5,5 +5,8 @@
"default": "mdi:car"
}
}
},
"services": {
"get_travel_times": "mdi:timelapse"
}
}

View File

@ -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

View File

@ -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:

View File

@ -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. "
}
}
}
}
}

View File

@ -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():

View File

@ -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"],
},
]
}

View File

@ -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."""