From addca54118fb7f73812d6b51a8d6dfb5d0b9d524 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Sun, 17 Jun 2018 01:13:47 -0400 Subject: [PATCH] Add entity support to Waze Travel Time (#14934) Current version only supports latitude and longitude or an address for the origin and destination fields. This update allows those fields to use entity IDs of device_tracker, zone, and sensor. --- .../components/sensor/waze_travel_time.py | 185 ++++++++++++------ 1 file changed, 130 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index dbcfcb9cc27..fc40d17d0af 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -7,12 +7,14 @@ https://home-assistant.io/components/sensor.waze_travel_time/ from datetime import timedelta import logging -import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START, + ATTR_LATITUDE, ATTR_LONGITUDE) import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.location as location from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -20,6 +22,7 @@ REQUIREMENTS = ['WazeRouteCalculator==0.5'] _LOGGER = logging.getLogger(__name__) +ATTR_DURATION = 'duration' ATTR_DISTANCE = 'distance' ATTR_ROUTE = 'route' @@ -46,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_EXCL_FILTER): cv.string, }) +TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Waze travel time sensor platform.""" @@ -56,24 +61,46 @@ def setup_platform(hass, config, add_devices, discovery_info=None): incl_filter = config.get(CONF_INCL_FILTER) excl_filter = config.get(CONF_EXCL_FILTER) - try: - waze_data = WazeRouteData( - origin, destination, region, incl_filter, excl_filter) - except requests.exceptions.HTTPError as error: - _LOGGER.error("%s", error) - return + sensor = WazeTravelTime(name, origin, destination, region, + incl_filter, excl_filter) - add_devices([WazeTravelTime(waze_data, name)], True) + add_devices([sensor], True) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, sensor.update) + + +def _get_location_from_attributes(state): + """Get the lat/long string from an states attributes.""" + attr = state.attributes + return '{},{}'.format( + attr.get(ATTR_LATITUDE), + attr.get(ATTR_LONGITUDE) + ) class WazeTravelTime(Entity): """Representation of a Waze travel time sensor.""" - def __init__(self, waze_data, name): + def __init__(self, name, origin, destination, region, + incl_filter, excl_filter): """Initialize the Waze travel time sensor.""" self._name = name + self._region = region + self._incl_filter = incl_filter + self._excl_filter = excl_filter self._state = None - self.waze_data = waze_data + self._origin_entity_id = None + self._destination_entity_id = None + + if origin.split('.', 1)[0] in TRACKABLE_DOMAINS: + self._origin_entity_id = origin + else: + self._origin = origin + + if destination.split('.', 1)[0] in TRACKABLE_DOMAINS: + self._destination_entity_id = destination + else: + self._destination = destination @property def name(self): @@ -83,7 +110,12 @@ class WazeTravelTime(Entity): @property def state(self): """Return the state of the sensor.""" - return round(self._state['duration']) + if self._state is None: + return None + + if 'duration' in self._state: + return round(self._state['duration']) + return None @property def unit_of_measurement(self): @@ -98,54 +130,97 @@ class WazeTravelTime(Entity): @property def device_state_attributes(self): """Return the state attributes of the last update.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_DISTANCE: round(self._state['distance']), - ATTR_ROUTE: self._state['route'], - } + if self._state is None: + return None - def update(self): - """Fetch new state data for the sensor.""" - try: - self.waze_data.update() - self._state = self.waze_data.data - except KeyError: - _LOGGER.error("Error retrieving data from server") + res = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + if 'duration' in self._state: + res[ATTR_DURATION] = self._state['duration'] + if 'distance' in self._state: + res[ATTR_DISTANCE] = self._state['distance'] + if 'route' in self._state: + res[ATTR_ROUTE] = self._state['route'] + return res + def _get_location_from_entity(self, entity_id): + """Get the location from the entity_id.""" + state = self.hass.states.get(entity_id) -class WazeRouteData(object): - """Get data from Waze.""" + if state is None: + _LOGGER.error("Unable to find entity %s", entity_id) + return None - def __init__(self, origin, destination, region, incl_filter, excl_filter): - """Initialize the data object.""" - self._destination = destination - self._origin = origin - self._region = region - self._incl_filter = incl_filter - self._excl_filter = excl_filter - self.data = {} + # Check if the entity has location attributes (zone) + if location.has_location(state): + return _get_location_from_attributes(state) + + # Check if device is in a zone (device_tracker) + zone_state = self.hass.states.get('zone.{}'.format(state.state)) + if location.has_location(zone_state): + _LOGGER.debug( + "%s is in %s, getting zone location", + entity_id, zone_state.entity_id + ) + return _get_location_from_attributes(zone_state) + + # If zone was not found in state then use the state as the location + if entity_id.startswith('sensor.'): + return state.state + + # When everything fails just return nothing + return None + + def _resolve_zone(self, friendly_name): + """Get a lat/long from a zones friendly_name.""" + states = self.hass.states.all() + for state in states: + if state.domain == 'zone' and state.name == friendly_name: + return _get_location_from_attributes(state) + + return friendly_name @Throttle(SCAN_INTERVAL) def update(self): - """Fetch latest data from Waze.""" + """Fetch new state data for the sensor.""" import WazeRouteCalculator - _LOGGER.debug("Update in progress...") - try: - params = WazeRouteCalculator.WazeRouteCalculator( - self._origin, self._destination, self._region, None) - results = params.calc_all_routes_info() - if self._incl_filter is not None: - results = {k: v for k, v in results.items() if - self._incl_filter.lower() in k.lower()} - if self._excl_filter is not None: - results = {k: v for k, v in results.items() if - self._excl_filter.lower() not in k.lower()} - best_route = next(iter(results)) - (duration, distance) = results[best_route] - best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8') - self.data['duration'] = duration - self.data['distance'] = distance - self.data['route'] = best_route_str - except WazeRouteCalculator.WRCError as exp: - _LOGGER.error("Error on retrieving data: %s", exp) - return + + if self._origin_entity_id is not None: + self._origin = self._get_location_from_entity( + self._origin_entity_id + ) + + if self._destination_entity_id is not None: + self._destination = self._get_location_from_entity( + self._destination_entity_id + ) + + self._destination = self._resolve_zone(self._destination) + self._origin = self._resolve_zone(self._origin) + + if self._destination is not None and self._origin is not None: + try: + params = WazeRouteCalculator.WazeRouteCalculator( + self._origin, self._destination, self._region) + routes = params.calc_all_routes_info() + + if self._incl_filter is not None: + routes = {k: v for k, v in routes.items() if + self._incl_filter.lower() in k.lower()} + + if self._excl_filter is not None: + routes = {k: v for k, v in routes.items() if + self._excl_filter.lower() not in k.lower()} + + route = sorted(routes, key=(lambda key: routes[key][0]))[0] + duration, distance = routes[route] + route = bytes(route, 'ISO-8859-1').decode('UTF-8') + self._state = { + 'duration': duration, + 'distance': distance, + 'route': route} + except WazeRouteCalculator.WRCError as exp: + _LOGGER.error("Error on retrieving data: %s", exp) + return + except KeyError: + _LOGGER.error("Error retrieving data from server") + return