Waze travel time update


#41

@petro Using your code as custom_component works like a charm. Thanks.


#42

No problem!


#43

Yes, I’m using your code too (and have been for a while) it is perfect, never had an issue with it so thanks from me too.
I don’t know how these things work so I’m speaking from a position of ignorance but it I don’t understand why the built-in version isn’t as good as the unofficial version!


#44

Something in the component doesn’t meet HA standards I’m guessing.

It’s failing 1/9 build checks. Not sure how to fix it.


#45

On the pull request on github it looks like someone was working on it too and offered to help. Would that help get this great addition into HA?


#46

Yes, but another PR got added and now I need to account for those parameters. This might have been a first come first serve thing because I believe that PR was entered before mine. So the delay may have been due to that the whole time.


#47

So here is the current rev. I’m going to submit another PR. This version has the current implementation using the include/exclude routes options.

EDIT: It also includes all suggestions thus far in the thread. I.E better sorting and so forth.

"""
Support for Waze travel time sensor.

For more details about this platform, please refer to the documentation at
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, 
    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

REQUIREMENTS = ['WazeRouteCalculator==0.5']

_LOGGER = logging.getLogger(__name__)

ATTR_DISTANCE = 'distance'
ATTR_ROUTE = 'route'

CONF_ATTRIBUTION = "Data provided by the Waze.com"
CONF_DESTINATION = 'destination'
CONF_ORIGIN = 'origin'
CONF_INCL_FILTER = 'incl_filter'
CONF_EXCL_FILTER = 'excl_filter'

DEFAULT_NAME = 'Waze Travel Time'

ICON = 'mdi:car'

REGIONS = ['US', 'NA', 'EU', 'IL']

SCAN_INTERVAL = timedelta(minutes=5)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_ORIGIN): cv.string,
    vol.Required(CONF_DESTINATION): cv.string,
    vol.Required(CONF_REGION): vol.In(REGIONS),
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_INCL_FILTER): cv.string,
    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."""
    def run_setup(event):
        destination = config.get(CONF_DESTINATION)
        name = config.get(CONF_NAME)
        origin = config.get(CONF_ORIGIN)
        region = config.get(CONF_REGION)
        incl_filter = config.get(CONF_INCL_FILTER)
        excl_filter = config.get(CONF_EXCL_FILTER)

        sensor = WazeTravelTime(hass, name, origin, destination, region, incl_filter, excl_filter)
        add_devices([sensor])

    hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup)

class WazeTravelTime(Entity):
    """Representation of a Waze travel time sensor."""

    def __init__(self, hass, name, origin, destination, region, incl_filter, excl_filter):
        """Initialize the Waze travel time sensor."""
        self._hass = hass
        self._name = name
        self._region = region
        self._incl_filter = incl_filter
        self._excl_filter = excl_filter
        self._state = 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):
        """Return the name of the sensor."""
        return self._name

    @property
    def state(self):
        """Return the state of the sensor."""
        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):
        """Return the unit of measurement."""
        return 'min'

    @property
    def icon(self):
        """Icon to use in the frontend, if any."""
        return ICON

    @property
    def device_state_attributes(self):
        """Return the state attributes of the last update."""
        res = {ATTR_ATTRIBUTION:CONF_ATTRIBUTION}
        if 'duration' in self._state:
            res['duration'] = self._state['duration']
        if 'distance' in self._state:
            res['distance'] = self._state['distance']
        if 'route' in self._state:
            res['route'] = self._state['route']            
        return res

    def _get_location_from_entity(self, entity_id):
        """Get the location from the entity state or attributes."""
        entity = self._hass.states.get(entity_id)

        if entity is None:
            _LOGGER.error("Unable to find entity %s", entity_id)
            return None

        # Check if the entity has location attributes (zone)
        if location.has_location(entity):
            return self._get_location_from_attributes(entity)

        # Check if device is in a zone (device_tracker)
        zone_entity = self._hass.states.get("zone.%s" % entity.state)
        if location.has_location(zone_entity):
            _LOGGER.debug(
                "%s is in %s, getting zone location",
                entity_id, zone_entity.entity_id
            )
            return self._get_location_from_attributes(zone_entity)

        # If zone was not found in state then use the state as the location
        if entity_id.startswith("sensor."):
            return entity.state

        # When everything fails just return nothing
        return None

    @staticmethod
    def _get_location_from_attributes(entity):
        """Get the lat/long string from an entities attributes."""
        attr = entity.attributes
        return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))

    def _resolve_zone(self, friendly_name):
        """Get a lat/long from a zones friendly_name"""
        entities = self._hass.states.all()
        for entity in entities:
            if entity.domain == 'zone' and entity.name == friendly_name:
                return self._get_location_from_attributes(entity)

        return friendly_name

    @Throttle(SCAN_INTERVAL)
    def update(self):
        """Fetch new state data for the sensor."""
        import WazeRouteCalculator

        if hasattr(self, '_origin_entity_id'):
            self._origin = self._get_location_from_entity(
                self._origin_entity_id
            )

        if hasattr(self, '_destination_entity_id'):
            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

#48

@pbavinck, @klogg, @Timelike, @rodon, @Amir974, @masterkenobi, @svaguilarv, @PostSven

PR got merged this week, expect this in the next build with all updates contained in this thread.


#49

Did this happen? I just got back from 2 weeks away, upgraded to 0.72.0 and removed the custom component but it seems it didn’t.

Or have I missed something stupid?


#50

The process for PR’s is new to me. The changes got merged into patch-5. I do not know how long it takes patch-5 to get into a official release.


#51

Actually, it will be in 0.73. The process takes a bit. So it got merged when 0.72 was in beta. So it is now in the beta and should be released next. So… 2 weeks


#52

Thats great! Thanks


#53

Hi @petro

with the following config:

  - platform: waze_travel_time
    name: "Amir to work"
    origin: device_tracker.myphone
    destination: zone.work
    region: 'IL'

My log is getting full with the following errors since upgrading,

Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/homeassistant/helpers/entity.py", line 197, in async_update_ha_state
    yield from self.async_device_update()
  File "/usr/lib/python3.6/site-packages/homeassistant/helpers/entity.py", line 320, in async_device_update
    yield from self.hass.async_add_job(self.update)
  File "/usr/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/lib/python3.6/site-packages/homeassistant/util/__init__.py", line 319, in wrapper
    result = method(*args, **kwargs)
  File "/config/custom_components/sensor/waze_travel_time.py", line 204, in update
    routes = params.calc_all_routes_info()
  File "/usr/lib/python3.6/site-packages/WazeRouteCalculator/WazeRouteCalculator.py", line 142, in calc_all_routes_info
    routes = self.get_route(npaths, time_delta)
  File "/usr/lib/python3.6/site-packages/WazeRouteCalculator/WazeRouteCalculator.py", line 92, in get_route
    response_json = response.json()
  File "/usr/lib/python3.6/site-packages/requests/models.py", line 892, in json
    return complexjson.loads(self.text, **kwargs)
  File "/usr/lib/python3.6/site-packages/simplejson/__init__.py", line 518, in loads
    return _default_decoder.decode(s)
  File "/usr/lib/python3.6/site-packages/simplejson/decoder.py", line 370, in decode
    obj, end = self.raw_decode(s)
  File "/usr/lib/python3.6/site-packages/simplejson/decoder.py", line 400, in raw_decode
    return self.scan_once(s, idx=_w(s, idx).end())
simplejson.errors.JSONDecodeError: Expecting value: line 1 column 1 (char 0) 

This config used to work with your previous version (from a few weeks ago…)

Please let me know what I can do about it, hopefully asap - before the next version / PR

thanks!


#54

I have that exact same error message with the “standard” component.


#55

@eifinger, @Amir974, the error message is coming from the resource library. I don’t actually have control over that library and the library hasn’t been updated in years. I’m not entirely sure why the original waze_travel_time component used it. It’s something that I don’t think I have the ability to change. It’s in my log too. I may be able to catch the error and suppress it, but at this point we need to wait til it’s in the release before I can make another PR.

EDIT: I’ll look into it deeper. Maybe one of the input’s is incorrectly formatted. If that’s not the case, then i’ll catch the error and suppress it.

EDIT: I think it occurs when one of the trackers/location is bad.


#56

I thought there is a fast track for patch in case a component is loosing basic functionality like that -the sensor is not producing data since 0.72…

Any way if you figure this out and can suggest a fix in custom_component folder or something like that - it would be awesome!

Edit:

BTW - this is still happening after I revert to my 0.71 backup, using the exact same code you provided earlier on - to the custom_component…

is it possible an update was made to a utility you are referencing or something else that got pulled in the past couple of days that’s causing it to fail ?

The backup version I restored too worked perfectly…

Edit:

P.S it’s happening with same tracker and zones - both seem fine with normal lon/lat showing in dev tools - states


#57

Could it be that the waze api changed?


#58

Hi,

Actually, the WazeRouteCalculator is giving back a 403 error. That’s why the json decoder is returning an error in HA.
So you’re right, it’s probably due to some changes in Waze API.
I’m trying to solve that problem but I’m not sure if this is a temporary issue or not on Waze side.


#59

no update was made to the utilty that the waze app is referencing. It’s been the same old reference for 2+ years without updates. This issue has been in there since day 1, but there may be ways to avoid it.


#60

That is possible