Add Here travel time arrival departure (#29909)

* here_travel_time: Add modes arrival and departure

* convert arrival/departure from datetime to time

* Default departure is set by external lib on None

* Use cv.key_value_schemas
pull/32897/head
Kevin Eifinger 2020-03-17 05:16:49 +01:00 committed by GitHub
parent 7ac014744c
commit 2cda7bf1e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 207 additions and 40 deletions

View File

@ -1,5 +1,5 @@
"""Support for HERE travel time sensors."""
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from typing import Callable, Dict, Optional, Union
@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import location
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt
_LOGGER = logging.getLogger(__name__)
@ -36,6 +37,8 @@ CONF_ORIGIN_ENTITY_ID = "origin_entity_id"
CONF_API_KEY = "api_key"
CONF_TRAFFIC_MODE = "traffic_mode"
CONF_ROUTE_MODE = "route_mode"
CONF_ARRIVAL = "arrival"
CONF_DEPARTURE = "departure"
DEFAULT_NAME = "HERE Travel Time"
@ -90,32 +93,49 @@ SCAN_INTERVAL = timedelta(minutes=5)
NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Inclusive(
CONF_DESTINATION_LATITUDE, "destination_coordinates"
): cv.latitude,
vol.Inclusive(
CONF_DESTINATION_LONGITUDE, "destination_coordinates"
): cv.longitude,
vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude,
vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id,
vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude,
vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude,
vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude,
vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id,
vol.Optional(CONF_DEPARTURE): cv.time,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE),
vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODE),
vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean,
vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS),
}
)
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID),
cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID),
PLATFORM_SCHEMA.extend(
cv.key_value_schemas(
CONF_MODE,
{
vol.Required(CONF_API_KEY): cv.string,
vol.Inclusive(
CONF_DESTINATION_LATITUDE, "destination_coordinates"
): cv.latitude,
vol.Inclusive(
CONF_DESTINATION_LONGITUDE, "destination_coordinates"
): cv.longitude,
vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude,
vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id,
vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude,
vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude,
vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude,
vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE),
vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(
ROUTE_MODE
None: PLATFORM_SCHEMA,
TRAVEL_MODE_BICYCLE: PLATFORM_SCHEMA,
TRAVEL_MODE_CAR: PLATFORM_SCHEMA,
TRAVEL_MODE_PEDESTRIAN: PLATFORM_SCHEMA,
TRAVEL_MODE_PUBLIC: PLATFORM_SCHEMA,
TRAVEL_MODE_TRUCK: PLATFORM_SCHEMA,
TRAVEL_MODE_PUBLIC_TIME_TABLE: PLATFORM_SCHEMA.extend(
{
vol.Exclusive(CONF_ARRIVAL, "arrival_departure"): cv.time,
vol.Exclusive(CONF_DEPARTURE, "arrival_departure"): cv.time,
}
),
vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean,
vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS),
}
},
),
)
@ -160,9 +180,11 @@ async def async_setup_platform(
route_mode = config[CONF_ROUTE_MODE]
name = config[CONF_NAME]
units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name)
arrival = config.get(CONF_ARRIVAL)
departure = config.get(CONF_DEPARTURE)
here_data = HERETravelTimeData(
here_client, travel_mode, traffic_mode, route_mode, units
here_client, travel_mode, traffic_mode, route_mode, units, arrival, departure
)
sensor = HERETravelTimeSensor(
@ -361,6 +383,8 @@ class HERETravelTimeData:
traffic_mode: bool,
route_mode: str,
units: str,
arrival: datetime,
departure: datetime,
) -> None:
"""Initialize herepy."""
self.origin = None
@ -368,6 +392,8 @@ class HERETravelTimeData:
self.travel_mode = travel_mode
self.traffic_mode = traffic_mode
self.route_mode = route_mode
self.arrival = arrival
self.departure = departure
self.attribution = None
self.traffic_time = None
self.distance = None
@ -377,6 +403,7 @@ class HERETravelTimeData:
self.destination_name = None
self.units = units
self._client = here_client
self.combine_change = True
def update(self) -> None:
"""Get the latest data from HERE."""
@ -389,24 +416,36 @@ class HERETravelTimeData:
# Convert location to HERE friendly location
destination = self.destination.split(",")
origin = self.origin.split(",")
arrival = self.arrival
if arrival is not None:
arrival = convert_time_to_isodate(arrival)
departure = self.departure
if departure is not None:
departure = convert_time_to_isodate(departure)
_LOGGER.debug(
"Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s",
"Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s",
origin,
destination,
herepy.RouteMode[self.route_mode],
herepy.RouteMode[self.travel_mode],
herepy.RouteMode[traffic_mode],
arrival,
departure,
)
try:
response = self._client.car_route(
response = self._client.public_transport_timetable(
origin,
destination,
self.combine_change,
[
herepy.RouteMode[self.route_mode],
herepy.RouteMode[self.travel_mode],
herepy.RouteMode[traffic_mode],
],
arrival=arrival,
departure=departure,
)
except herepy.NoRouteFoundError:
# Better error message for cryptic no route error codes
@ -453,3 +492,11 @@ class HERETravelTimeData:
joined_supplier_titles = ",".join(supplier_titles)
attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind."
return attribution
def convert_time_to_isodate(timestr: str) -> str:
"""Take a string like 08:00:00 and combine it with the current date."""
combined = datetime.combine(dt.start_of_local_day(), dt.parse_time(timestr))
if combined < datetime.now():
combined = combined + timedelta(days=1)
return combined.isoformat()

View File

@ -37,6 +37,7 @@ from homeassistant.components.here_travel_time.sensor import (
TRAVEL_MODE_PUBLIC,
TRAVEL_MODE_PUBLIC_TIME_TABLE,
TRAVEL_MODE_TRUCK,
convert_time_to_isodate,
)
from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START
from homeassistant.setup import async_setup_component
@ -66,7 +67,7 @@ CAR_DESTINATION_LATITUDE = "39.0"
CAR_DESTINATION_LONGITUDE = "-77.1"
def _build_mock_url(origin, destination, modes, api_key, departure):
def _build_mock_url(origin, destination, modes, api_key, departure=None, arrival=None):
"""Construct a url for HERE."""
base_url = "https://route.ls.hereapi.com/routing/7.2/calculateroute.json?"
parameters = {
@ -74,9 +75,13 @@ def _build_mock_url(origin, destination, modes, api_key, departure):
"waypoint1": f"geo!{destination}",
"mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes),
"apikey": api_key,
"departure": departure,
}
if arrival is not None:
parameters["arrival"] = arrival
if departure is not None:
parameters["departure"] = departure
url = base_url + urllib.parse.urlencode(parameters)
print(url)
return url
@ -117,7 +122,6 @@ def requests_mock_credentials_check(requests_mock):
",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
modes,
API_KEY,
"now",
)
requests_mock.get(
response_url, text=load_fixture("here_travel_time/car_response.json")
@ -134,7 +138,6 @@ def requests_mock_truck_response(requests_mock_credentials_check):
",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]),
modes,
API_KEY,
"now",
)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/truck_response.json")
@ -150,7 +153,6 @@ def requests_mock_car_disabled_response(requests_mock_credentials_check):
",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
modes,
API_KEY,
"now",
)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/car_response.json")
@ -214,7 +216,6 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check):
",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
modes,
API_KEY,
"now",
)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/car_enabled_response.json")
@ -272,7 +273,7 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check):
origin = "38.902981,-77.048338"
destination = "39.042158,-77.119116"
modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/car_shortest_response.json")
)
@ -303,7 +304,7 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check):
origin = "38.902981,-77.048338"
destination = "39.042158,-77.119116"
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/car_enabled_response.json")
)
@ -357,7 +358,7 @@ async def test_public_transport(hass, requests_mock_credentials_check):
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/public_response.json")
)
@ -406,7 +407,7 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url,
text=load_fixture("here_travel_time/public_time_table_response.json"),
@ -456,7 +457,7 @@ async def test_pedestrian(hass, requests_mock_credentials_check):
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PEDESTRIAN, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/pedestrian_response.json")
)
@ -508,7 +509,7 @@ async def test_bicycle(hass, requests_mock_credentials_check):
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_BICYCLE, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/bike_response.json")
)
@ -841,7 +842,7 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog):
origin = "52.516,13.3779"
destination = "47.013399,-10.171986"
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url,
text=load_fixture("here_travel_time/routing_error_no_route_found.json"),
@ -914,7 +915,6 @@ async def test_invalid_credentials(hass, requests_mock, caplog):
",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
modes,
API_KEY,
"now",
)
requests_mock.get(
response_url,
@ -942,7 +942,7 @@ async def test_attribution(hass, requests_mock_credentials_check):
origin = "50.037751372637686,14.39233448220898"
destination = "50.07993838201255,14.42582157361062"
modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_ENABLED]
response_url = _build_mock_url(origin, destination, modes, API_KEY, "now")
response_url = _build_mock_url(origin, destination, modes, API_KEY)
requests_mock_credentials_check.get(
response_url, text=load_fixture("here_travel_time/attribution_response.json")
)
@ -1051,3 +1051,123 @@ async def test_delayed_update(hass, requests_mock_truck_response, caplog):
await hass.async_block_till_done()
assert "Unable to find entity" not in caplog.text
async def test_arrival(hass, requests_mock_credentials_check):
"""Test that arrival works."""
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
arrival = "01:00:00"
arrival_isodate = convert_time_to_isodate(arrival)
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(
origin, destination, modes, API_KEY, arrival=arrival_isodate
)
requests_mock_credentials_check.get(
response_url,
text=load_fixture("here_travel_time/public_time_table_response.json"),
)
config = {
DOMAIN: {
"platform": PLATFORM,
"name": "test",
"origin_latitude": origin.split(",")[0],
"origin_longitude": origin.split(",")[1],
"destination_latitude": destination.split(",")[0],
"destination_longitude": destination.split(",")[1],
"api_key": API_KEY,
"mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
"arrival": arrival,
}
}
assert await async_setup_component(hass, DOMAIN, config)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
sensor = hass.states.get("sensor.test")
assert sensor.state == "80"
async def test_departure(hass, requests_mock_credentials_check):
"""Test that arrival works."""
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
departure = "23:00:00"
departure_isodate = convert_time_to_isodate(departure)
modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED]
response_url = _build_mock_url(
origin, destination, modes, API_KEY, departure=departure_isodate
)
requests_mock_credentials_check.get(
response_url,
text=load_fixture("here_travel_time/public_time_table_response.json"),
)
config = {
DOMAIN: {
"platform": PLATFORM,
"name": "test",
"origin_latitude": origin.split(",")[0],
"origin_longitude": origin.split(",")[1],
"destination_latitude": destination.split(",")[0],
"destination_longitude": destination.split(",")[1],
"api_key": API_KEY,
"mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
"departure": departure,
}
}
assert await async_setup_component(hass, DOMAIN, config)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
sensor = hass.states.get("sensor.test")
assert sensor.state == "80"
async def test_arrival_only_allowed_for_timetable(hass, caplog):
"""Test that arrival is only allowed when mode is publicTransportTimeTable."""
caplog.set_level(logging.ERROR)
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
config = {
DOMAIN: {
"platform": PLATFORM,
"name": "test",
"origin_latitude": origin.split(",")[0],
"origin_longitude": origin.split(",")[1],
"destination_latitude": destination.split(",")[0],
"destination_longitude": destination.split(",")[1],
"api_key": API_KEY,
"arrival": "01:00:00",
}
}
assert await async_setup_component(hass, DOMAIN, config)
assert len(caplog.records) == 1
assert "[arrival] is an invalid option" in caplog.text
async def test_exclusive_arrival_and_departure(hass, caplog):
"""Test that arrival and departure are exclusive."""
caplog.set_level(logging.ERROR)
origin = "41.9798,-87.8801"
destination = "41.9043,-87.9216"
config = {
DOMAIN: {
"platform": PLATFORM,
"name": "test",
"origin_latitude": origin.split(",")[0],
"origin_longitude": origin.split(",")[1],
"destination_latitude": destination.split(",")[0],
"destination_longitude": destination.split(",")[1],
"api_key": API_KEY,
"arrival": "01:00:00",
"mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
"departure": "01:00:00",
}
}
assert await async_setup_component(hass, DOMAIN, config)
assert len(caplog.records) == 1
assert "two or more values in the same group of exclusion" in caplog.text