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)