1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-17 23:53:49 +01:00
Files
core/homeassistant/components/google_travel_time/helpers.py
Kevin Stillhammer acf739df81 add services to google_travel_time (#160740)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-12 22:31:36 +01:00

235 lines
7.8 KiB
Python

"""Helpers for Google Time Travel integration."""
import datetime
import logging
from google.api_core.client_options import ClientOptions
from google.api_core.exceptions import (
Forbidden,
GatewayTimeout,
GoogleAPIError,
PermissionDenied,
Unauthorized,
)
from google.maps.routing_v2 import (
ComputeRoutesRequest,
ComputeRoutesResponse,
Location,
RouteModifiers,
RoutesAsyncClient,
RouteTravelMode,
RoutingPreference,
TransitPreferences,
Waypoint,
)
from google.protobuf import timestamp_pb2
from google.type import latlng_pb2
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.location import find_coordinates
from homeassistant.util import dt as dt_util
from .const import (
DOMAIN,
TRAFFIC_MODELS_TO_GOOGLE_SDK_ENUM,
TRANSIT_PREFS_TO_GOOGLE_SDK_ENUM,
TRANSPORT_TYPES_TO_GOOGLE_SDK_ENUM,
UNITS_TO_GOOGLE_SDK_ENUM,
)
_LOGGER = logging.getLogger(__name__)
def convert_time(time_str: str) -> timestamp_pb2.Timestamp:
"""Convert a string like '08:00' to a google pb2 Timestamp.
If the time is in the past, it will be shifted to the next day.
"""
parsed_time = dt_util.parse_time(time_str)
if parsed_time is None:
raise ValueError(f"Invalid time format: {time_str}")
start_of_day = dt_util.start_of_local_day()
combined = datetime.datetime.combine(
start_of_day,
parsed_time,
start_of_day.tzinfo,
)
if combined < dt_util.now():
combined = combined + datetime.timedelta(days=1)
timestamp = timestamp_pb2.Timestamp()
timestamp.FromDatetime(dt=combined)
return timestamp
def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
"""Convert a location to a Waypoint.
Will either use coordinates or if none are found, use the location as an address.
"""
coordinates = find_coordinates(hass, location)
if coordinates is None:
return None
try:
formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates))
except AttributeError, vol.Invalid:
return Waypoint(address=location)
return Waypoint(
location=Location(
lat_lng=latlng_pb2.LatLng(
latitude=float(formatted_coordinates[0]),
longitude=float(formatted_coordinates[1]),
)
)
)
async def validate_config_entry(
hass: HomeAssistant, api_key: str, origin: str, destination: str
) -> None:
"""Return whether the config entry data is valid."""
resolved_origin = convert_to_waypoint(hass, origin)
resolved_destination = convert_to_waypoint(hass, destination)
client_options = ClientOptions(api_key=api_key)
client = RoutesAsyncClient(client_options=client_options)
field_mask = "routes.duration"
request = ComputeRoutesRequest(
origin=resolved_origin,
destination=resolved_destination,
travel_mode=RouteTravelMode.DRIVE,
)
try:
await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)]
)
except PermissionDenied as permission_error:
_LOGGER.error("Permission denied: %s", permission_error.message)
raise PermissionDeniedException from permission_error
except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error
except GatewayTimeout as timeout_error:
_LOGGER.error("Timeout error")
raise TimeoutError from timeout_error
except GoogleAPIError as unknown_error:
_LOGGER.error("Unknown error: %s", unknown_error)
raise UnknownException from unknown_error
class InvalidApiKeyException(Exception):
"""Invalid API Key Error."""
class UnknownException(Exception):
"""Unknown API Error."""
class PermissionDeniedException(Exception):
"""Permission Denied Error."""
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Create an issue for the Routes API being disabled."""
async_create_issue(
hass,
DOMAIN,
f"routes_api_disabled_{entry.entry_id}",
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="routes_api_disabled",
translation_placeholders={
"entry_title": entry.title,
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
},
)
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Delete the issue for the Routes API being disabled."""
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")
async def async_compute_routes(
client: RoutesAsyncClient,
origin: str,
destination: str,
hass: HomeAssistant,
travel_mode: int,
units: str,
language: str | None = None,
avoid: str | None = None,
traffic_model: str | None = None,
transit_mode: str | None = None,
transit_routing_preference: str | None = None,
departure_time: str | None = None,
arrival_time: str | None = None,
field_mask: str = "routes.duration,routes.distanceMeters,routes.localized_values",
) -> ComputeRoutesResponse | None:
"""Compute routes using Google Routes API."""
origin_waypoint = convert_to_waypoint(hass, origin)
destination_waypoint = convert_to_waypoint(hass, destination)
if origin_waypoint is None or destination_waypoint is None:
return None
route_modifiers = None
routing_preference = None
if travel_mode == RouteTravelMode.DRIVE:
routing_preference = RoutingPreference.TRAFFIC_AWARE_OPTIMAL
route_modifiers = RouteModifiers(
avoid_tolls=avoid == "tolls",
avoid_ferries=avoid == "ferries",
avoid_highways=avoid == "highways",
avoid_indoor=avoid == "indoor",
)
transit_preferences = None
if travel_mode == RouteTravelMode.TRANSIT:
transit_routing_pref = None
transit_travel_mode = (
TransitPreferences.TransitTravelMode.TRANSIT_TRAVEL_MODE_UNSPECIFIED
)
if transit_routing_preference is not None:
transit_routing_pref = TRANSIT_PREFS_TO_GOOGLE_SDK_ENUM[
transit_routing_preference
]
if transit_mode is not None:
transit_travel_mode = TRANSPORT_TYPES_TO_GOOGLE_SDK_ENUM[transit_mode]
transit_preferences = TransitPreferences(
routing_preference=transit_routing_pref,
allowed_travel_modes=[transit_travel_mode],
)
departure_timestamp = convert_time(departure_time) if departure_time else None
arrival_timestamp = convert_time(arrival_time) if arrival_time else None
request = ComputeRoutesRequest(
origin=origin_waypoint,
destination=destination_waypoint,
travel_mode=travel_mode,
routing_preference=routing_preference,
departure_time=departure_timestamp,
arrival_time=arrival_timestamp,
route_modifiers=route_modifiers,
language_code=language,
units=UNITS_TO_GOOGLE_SDK_ENUM[units],
traffic_model=TRAFFIC_MODELS_TO_GOOGLE_SDK_ENUM[traffic_model]
if traffic_model
else None,
transit_preferences=transit_preferences,
)
return await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)]
)