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_timespull/117066/head
parent
7923471b94
commit
3844e2d533
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
|
|
@ -5,5 +5,8 @@
|
|||
"default": "mdi:car"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_travel_times": "mdi:timelapse"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,57 +187,33 @@ 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(
|
||||
routes = await async_get_travel_times(
|
||||
self.client,
|
||||
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,
|
||||
vehicle_type,
|
||||
avoid_toll_roads,
|
||||
avoid_subscription_roads,
|
||||
avoid_ferries,
|
||||
realtime,
|
||||
incl_filter,
|
||||
excl_filter,
|
||||
)
|
||||
|
||||
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:
|
||||
if routes:
|
||||
route = routes[0]
|
||||
else:
|
||||
_LOGGER.warning("No routes found")
|
||||
return
|
||||
|
||||
route = routes[0]
|
||||
|
||||
self.duration = route.duration
|
||||
distance = route.distance
|
||||
|
||||
if units == IMPERIAL_UNITS:
|
||||
if self.config_entry.options[CONF_UNITS] == IMPERIAL_UNITS:
|
||||
# Convert to miles.
|
||||
self.distance = DistanceConverter.convert(
|
||||
distance, UnitOfLength.KILOMETERS, UnitOfLength.MILES
|
||||
|
@ -245,6 +222,3 @@ class WazeTravelTimeData:
|
|||
self.distance = distance
|
||||
|
||||
self.route = route.name
|
||||
except WRCError as exp:
|
||||
_LOGGER.warning("Error on retrieving data: %s", exp)
|
||||
return
|
||||
|
|
|
@ -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:
|
|
@ -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. "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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"],
|
||||
},
|
||||
]
|
||||
}
|
|
@ -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."""
|
||||
|
|
Loading…
Reference in New Issue