From d229cb46b1ad583410d33784968331ed180340e8 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny <me@robbiet.us> Date: Thu, 12 May 2016 22:37:08 -0700 Subject: [PATCH] Google travel time improvements (#2047) * Update google_travel_time.py * Update google_travel_time.py * pylint: disable=too-many-instance-attributes * Add the mode to the title of the sensor * Expose the travel mode on the sensor attributes * Big improvements to the Google Travel Time sensor. Allow passing any options that Google supports in the options dict of your configuration. Deprecate travel_mode. Change name format to show the mode * fu farcy * Dynamically convert departure and arrival times * Add a warning if user provides both departure and arrival times * Add deprecation warning for travel_mode outside options and other minor fixes * Use a copy of options dict to not overwrite the departure/arrival times constantly. * Remove default travel_mode, but set default options.mode to driving * Google doesnt let us query time in the past, so if the date we generate from a time string is in the past, add 1 day * spacing fix * Add config validation for all possible parameters * flake8 and pylint fixes --- .../components/sensor/google_travel_time.py | 110 +++++++++++++++--- 1 file changed, 91 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index 867a84b1f03..a264723b47a 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -10,8 +10,10 @@ import logging import voluptuous as vol from homeassistant.helpers.entity import Entity -from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS +from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -23,47 +25,102 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) CONF_ORIGIN = 'origin' CONF_DESTINATION = 'destination' CONF_TRAVEL_MODE = 'travel_mode' +CONF_OPTIONS = 'options' +CONF_MODE = 'mode' +CONF_NAME = 'name' + +ALL_LANGUAGES = ['ar', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', + 'eu', 'fa', 'fi', 'fr', 'gl', 'gu', 'hi', 'hr', 'hu', 'id', + 'it', 'iw', 'ja', 'kn', 'ko', 'lt', 'lv', 'ml', 'mr', 'nl', + 'no', 'pl', 'pt', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sl', + 'sr', 'sv', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'vi', + 'zh-CN', 'zh-TW'] + +TRANSIT_PREFS = ['less_walking', 'fewer_transfers'] PLATFORM_SCHEMA = vol.Schema({ vol.Required('platform'): 'google_travel_time', + vol.Optional(CONF_NAME): vol.Coerce(str), vol.Required(CONF_API_KEY): vol.Coerce(str), vol.Required(CONF_ORIGIN): vol.Coerce(str), vol.Required(CONF_DESTINATION): vol.Coerce(str), - vol.Optional(CONF_TRAVEL_MODE, default='driving'): - vol.In(["driving", "walking", "bicycling", "transit"]) + vol.Optional(CONF_TRAVEL_MODE): + vol.In(["driving", "walking", "bicycling", "transit"]), + vol.Optional(CONF_OPTIONS): vol.All( + dict, vol.Schema({ + vol.Optional(CONF_MODE, default='driving'): + vol.In(["driving", "walking", "bicycling", "transit"]), + vol.Optional('language'): vol.In(ALL_LANGUAGES), + vol.Optional('avoid'): vol.In(['tolls', 'highways', + 'ferries', 'indoor']), + vol.Optional('units'): vol.In(['metric', 'imperial']), + vol.Exclusive('arrival_time', 'time'): cv.string, + vol.Exclusive('departure_time', 'time'): cv.string, + vol.Optional('traffic_model'): vol.In(['best_guess', + 'pessimistic', + 'optimistic']), + vol.Optional('transit_mode'): vol.In(['bus', 'subway', 'train', + 'tram', 'rail']), + vol.Optional('transit_routing_preference'): vol.In(TRANSIT_PREFS) + })) }) +def convert_time_to_utc(timestr): + """Take a string like 08:00:00 and convert it to a unix timestamp.""" + combined = datetime.combine(dt_util.start_of_local_day(), + dt_util.parse_time(timestr)) + if combined < datetime.now(): + combined = combined + timedelta(days=1) + return dt_util.as_timestamp(combined) + + def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the travel time platform.""" # pylint: disable=too-many-locals + options = config.get(CONF_OPTIONS) - is_metric = (hass.config.temperature_unit == TEMP_CELSIUS) + if options.get('units') is None: + if hass.config.temperature_unit is TEMP_CELSIUS: + options['units'] = 'metric' + elif hass.config.temperature_unit is TEMP_FAHRENHEIT: + options['units'] = 'imperial' + + travel_mode = config.get(CONF_TRAVEL_MODE) + mode = options.get(CONF_MODE) + + if travel_mode is not None: + wstr = ("Google Travel Time: travel_mode is deprecated, please add " + "mode to the options dictionary instead!") + _LOGGER.warning(wstr) + if mode is None: + options[CONF_MODE] = travel_mode + + titled_mode = options.get(CONF_MODE, 'driving').title() + formatted_name = "Google Travel Time - {}".format(titled_mode) + name = config.get(CONF_NAME, formatted_name) api_key = config.get(CONF_API_KEY) origin = config.get(CONF_ORIGIN) destination = config.get(CONF_DESTINATION) - travel_mode = config.get(CONF_TRAVEL_MODE) - sensor = GoogleTravelTimeSensor(api_key, origin, destination, - travel_mode, is_metric) + sensor = GoogleTravelTimeSensor(name, api_key, origin, destination, + options) if sensor.valid_api_connection: add_devices_callback([sensor]) +# pylint: disable=too-many-instance-attributes class GoogleTravelTimeSensor(Entity): """Representation of a tavel time sensor.""" # pylint: disable=too-many-arguments - def __init__(self, api_key, origin, destination, travel_mode, is_metric): + def __init__(self, name, api_key, origin, destination, options): """Initialize the sensor.""" - if is_metric: - self._unit = 'metric' - else: - self._unit = 'imperial' + self._name = name + self._options = options self._origin = origin self._destination = destination - self._travel_mode = travel_mode self._matrix = None self.valid_api_connection = True @@ -84,12 +141,13 @@ class GoogleTravelTimeSensor(Entity): @property def name(self): """Get the name of the sensor.""" - return "Google Travel time" + return self._name @property def device_state_attributes(self): """Return the state attributes.""" res = self._matrix.copy() + res.update(self._options) del res['rows'] _data = self._matrix['rows'][0]['elements'][0] if 'duration_in_traffic' in _data: @@ -108,10 +166,24 @@ class GoogleTravelTimeSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Google.""" - now = datetime.now() + options_copy = self._options.copy() + dtime = options_copy.get('departure_time') + atime = options_copy.get('arrival_time') + if dtime is not None and ':' in dtime: + options_copy['departure_time'] = convert_time_to_utc(dtime) + + if atime is not None and ':' in atime: + options_copy['arrival_time'] = convert_time_to_utc(atime) + + departure_time = options_copy.get('departure_time') + arrival_time = options_copy.get('arrival_time') + if departure_time is not None and arrival_time is not None: + wstr = ("Google Travel Time: You can not provide both arrival " + "and departure times! Deleting the arrival time...") + _LOGGER.warning(wstr) + del options_copy['arrival_time'] + del self._options['arrival_time'] + self._matrix = self._client.distance_matrix(self._origin, self._destination, - mode=self._travel_mode, - units=self._unit, - departure_time=now, - traffic_model="optimistic") + **options_copy)