GTFS and/or GTFS Realtime Not Working

Tried using @zacs new version of gtfs_rt, but for whatever reason I keep getting stuck with an estimated next bus arrival time of the start of the Unix Epoch. So it just displays -26638474 min, decrementing by 1 min each min, right now. Not sure if I’ve done something wrong with the way I’ve set it up, or if it’s a component issue, or if it’s my city somehow messing up their GTFS implementation. Tried out the Seattle example on the github page and it worked fine.

sensor:
  - platform: gtfs_rt
    trip_update_url: 'http://gtfs.edmonton.ca/TMGTFSRealTimeWebService/TripUpdate/TripUpdates.pb'
    vehicle_position_url: 'http://gtfs.edmonton.ca/TMGTFSRealTimeWebService/Vehicle/VehiclePositions.pb'
    departures:
    - name: "Southgate"
      route: 9
      stopid: 1425

Didn’t seem like the Edmonton Transit Service used any special naming conventions for the stops or route IDs. Did a bit of research but wasn’t able ot find anything that helped, any help on this would be appreciated. :slight_smile:

Hi All i can’t setup my sensor geting error: 2020-10-08 21:27:31 ERROR (SyncWorker_16) [custom_components.gtfs_rt.sensor] updating vehicle positions got 401:b’{ “statusCode”: 401, “message”: “Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API.” }’.
My setup

- platform: gtfs_rt
    trip_update_url: 'https://gtfsr.transportforireland.ie/v1'
    vehicle_position_url: 'https://gtfsr.transportforireland.ie/v1 '
    api_key: xxxxxxxxxxxxxxx
    departures:
    - name: "Eagle Valley To City Centre"
      route: 10-214-e20-1
      stopid: 8380B2433701

any help would be appreciated.

Hi,

I have been playing around with this for the past week. To get the custom sensor working, you need to go into the code and change that api key as below - last if statement. The main problem I’m finding is that the data feed is incomplete / patchy. Most of the arrival times for stops are missing or set to 0. This will show up as a very large negative number on the sensor because it calculates the difference between current time and 0 posix time which is 1970. GTFS for Ireland only went live at the end of Sept so maybe they haven’t worked it all out yet. If you have any info on it, let me know. My local stops (in Dublin) are not showing up at all.

Jerry

class PublicTransportData(object):
    """The Class for handling the data retrieval."""

    def __init__(self, trip_update_url, vehicle_position_url=None, api_key=None):
        """Initialize the info object."""
        self._trip_update_url = trip_update_url
        self._vehicle_position_url = vehicle_position_url
        if api_key is not None:
            self._headers = {'x-api-key': api_key}

Hi,
Thanks for tip custom sensor show up. Unfortunately not working, GTFS for Cork as well, looks like this api not working properly.

Marcin

Hi Marcin,

I set up a number of sample stops. I found one that worked for a while and then stopped. I think you are correct, the API is not working. Maybe keep an eye on it over the next few weeks,

Jerry

This comment only applies to the IRISH GTFS-R implementation. I looked into it in more detail and it looks like the API from NTA Ireland only sends arrival/departure delay data. This means you need the scheduled data for the whole network as a static reference. This is available via the NTA’s website and it can be installed by following the instructions for the GTFS sensor. It takes a while for the database to be loaded (over an hour in my RPi3 with a fast SD card). It will also have to refreshed, probably yearly. After that you need to register with the NTA to get the api key for the realtime feed. I have taken the old Dublin Bus sensor code and some bits from @zacs and made a new version which works with the static scheduled data and the realtime feed combined. I’m new to Python so my code might not be perfect but if anyone is interested, let me know and I’ll share it.

Nice catch. Are you loading the static GTFS data (not the GTFS-RT feed) to do this? In general that should work just fine. I can’t claim any knowledge for this plugin since I primarily just re-wrapped it to fit the current plugin structure for HA.

I also run into that epoch bug @Aaron_Lloyd mentioned occasionally. I believe it’s an overflow bug for when the bus is arriving sooner than anticipated and the plugin goes into negative time. I haven’t looked at the code at all, that’s just a suspicion. Sadly I’m not using this plugin any more due to WFH. If anyone would like to push changes to my github repo please feel free to submit a PR!

I’m using the static data combined with the realtime feed, still under test. My version can give negative arrival times in some circumstances but that’s ok.

Yah the classic problem with GTFS and GTFS-RT is that there’s no ID to join a trip between the static and GTFS-RT. With an every-30-minute bus, if you get a RT update for the 15th minute, it’s impossible to tell if that bus was a 15 minutes early next bus or 15 minutes late last bus.

hi @zacs, I don’t think I have the problem you describe. I can always match up the scheduled trip to the delay data. It is just that if the bus is early the net arrival time might be say, -1 or -2 min. If you would like a look at the code see below. If you plan to use it you need an extra index on the stop times table over and above the indexes created by the GTFS sensor.

createIndex = "CREATE INDEX index_stop_times_1 ON  stop_times(trip_id, stop_id )"

Code for sensor:

import datetime
import logging
import requests
import time
import sqlite3

import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE)
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)

REQUIREMENTS = [
    'gtfs-realtime-bindings==0.0.5',
    'protobuf==3.6.1'
]

ATTR_STOP_ID = "Stop ID"
ATTR_ROUTE = "Route"
ATTR_DUE_IN = "Due in"
ATTR_DUE_AT = "Due at"
ATTR_NEXT_UP = "next_bus"
ATTR_HITS = "Hits"

CONF_API_KEY = 'api_key'
CONF_STOP_ID = 'stopid'
CONF_ROUTE = 'route'
CONF_DEPARTURES = 'departures'
CONF_OPERATOR = 'operator'
CONF_TRIP_UPDATE_URL = 'trip_update_url'
CONF_VEHICLE_POSITION_URL = 'vehicle_position_url'

DEFAULT_NAME = 'Next Bus'
ICON = 'mdi:bus'

MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=60)
TIME_STR_FORMAT = "%H:%M"


PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_TRIP_UPDATE_URL): cv.string,
    vol.Optional(CONF_API_KEY): cv.string,
    vol.Optional(CONF_VEHICLE_POSITION_URL): cv.string,
    vol.Optional(CONF_DEPARTURES): [{
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
        vol.Required(CONF_STOP_ID): cv.string,
        vol.Required(CONF_ROUTE): cv.string,
        vol.Required(CONF_OPERATOR): cv.string
    }]
})

def get_times(route_stops):
  """Get the next X departure times today for the stop """ 

  ## put in your own database file and location here

  conn = sqlite3.connect('/home/homeassistant/.homeassistant/gtfs/ireland.sqlite')
  curr = conn.cursor()
  cstops = conn.cursor()
  cserv = conn.cursor()
  croutes = conn.cursor()
  cexcp = conn.cursor()

  date_format = '%Y-%m-%d %H:%M:%S.%f'
  pattern = '%Y-%m-%d %H:%M:%S.%f'
  pattern1 = '1970-01-01 %H:%M:%S.%f'
  pattern2 = '%Y-%m-%d'

  def validate_service(agency_id, service_id):
       """is service id valid for today and are there any exceptions""" 

       result = False    
       today = datetime.datetime.today().weekday()
       cserv.execute("SELECT * from calendar WHERE service_id=:service", {"service": service_id})
       dows = cserv.fetchone()
       today_flag = list(dows)[today+2]

       today_date = datetime.datetime.today()
       today_date1 = str(today_date)
       today_date2 = datetime.datetime.strftime(today_date, pattern2)
       dt = int(time.mktime(time.strptime(today_date1, date_format)))
       from_date  = list(dows)[9]
       to_date = list(dows)[10]
       dt1 = int(time.mktime(time.strptime(from_date, pattern2)))
       dt2 =  int(time.mktime(time.strptime(to_date, pattern2)))

       validity = True  if dt >= dt1 and dt <= dt2  else  False

       if today_flag == 1 and validity:
          cexcp.execute("SELECT * from calendar_dates WHERE service_id=:service and date=:date", 
                                                   {"service": service_id, "date":today_date2})
          exception_date = cexcp.fetchone()
          if exception_date is not None:
             validity = False
          else:
             result = True
       return result

  all_stop_times = []

  for rs in route_stops:
     stop_times = []
     req_route = rs[0]
     req_stop = rs[1]
     req_operator = rs[2]
     req_agent = " "
  
     croutes.execute("SELECT agency_id from routes WHERE route_id=:route", {"route": req_route})
   
     ## find all the trips passing the required routes and stops

     curr.execute("SELECT trip_id, service_id from trips WHERE route_id=:route", {"route": req_route})

     ## for each trip found find the scheduled arrival time at the required stop, sorted by arrival time

     for trip_id, service_id in curr.fetchall():

       req_trip = trip_id
       cstops.execute("SELECT arrival_time, departure_time, stop_id FROM stop_times WHERE trip_id=:trip AND stop_id=:stop", 
             {"trip": req_trip, "stop": req_stop})

       departure = cstops.fetchone()

       if departure is not None:
          dep_time_str = list(departure)[1]

          epoch_dep = int(time.mktime(time.strptime(dep_time_str, pattern)))

          now = datetime.datetime.now()
          curr_time_str = now.strftime(pattern1)
          epoch_now = int(time.mktime(time.strptime(curr_time_str, pattern)))
 
          if epoch_dep >= epoch_now:
            diff = epoch_dep - epoch_now
            agent = croutes.fetchone()
            if agent is not None:
               req_agent = list(agent)[0]

            stop_times.append((req_agent, service_id, req_trip,  req_route, req_stop, req_operator, int(diff/60)))

       stop_times = sorted(stop_times, key=lambda x: x[6])
       stop_times = stop_times[0 : 15]

       ## change the array size from 15 as necessary

     ## check that the arrival times found are valid for today before adding to the result
 
     if stop_times is not None:
       for stop_time in stop_times:
          if req_operator == stop_time[0] and req_route == stop_time[3]:
            if validate_service(stop_time[0], stop_time[1]):
               all_stop_times.append(stop_time)

  curr.close()
  cstops.close()
  cserv.close()
  croutes.close()
  cexcp.close()

  return all_stop_times


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Get the Dublin public transport sensor."""
    trip_url         = config.get(CONF_TRIP_UPDATE_URL)
    vehicle_pos_url  = config.get(CONF_VEHICLE_POSITION_URL)
    api_key          = config.get(CONF_API_KEY)

    route_deps = []
    for departure in config.get(CONF_DEPARTURES):
        name     = departure.get(CONF_NAME)
        stop     = departure.get(CONF_STOP_ID)
        route    = departure.get(CONF_ROUTE)
        operator = departure.get(CONF_OPERATOR)
        route_deps.append((route, stop, operator, name))

    data = PublicTransportData(trip_url, route_deps, vehicle_pos_url, api_key)

    sensors = []

    for departure in config.get(CONF_DEPARTURES):
        name    = departure.get(CONF_NAME)
        stop    = departure.get(CONF_STOP_ID)
        route   = departure.get(CONF_ROUTE)
        operator= departure.get(CONF_OPERATOR)
        sensors.append(PublicTransportSensor(data, stop, route, name))

    add_devices(sensors)


class PublicTransportSensor(Entity):
    """Implementation of a public transport sensor."""

    def __init__(self, data, stop, route, name):
        """Initialize the sensor."""
        self.data = data
        self._name = name
        self._stop = stop
        self._route = route
        self.update()

    @property
    def name(self):
        return self._name

    def _get_next_buses(self):
        return self.data.info.get(self._route, {}).get(self._stop, [])

    @property
    def state(self):
        """Return the state of the sensor."""
        next_buses = self._get_next_buses()
        return next_buses[0].arrival_time if len(next_buses) > 0 else '-'
		
    @property
    def device_state_attributes(self):
        """Return the state attributes."""
        next_buses = self._get_next_buses()
        no_hits = str(len(next_buses))
        next_bus = "-"
        attrs = {
            ATTR_DUE_IN: self.state,
            ATTR_STOP_ID: self._stop,
            ATTR_ROUTE: self._route,
            ATTR_NEXT_UP: next_bus,
            ATTR_HITS: no_hits
        }
        if len(next_buses) > 0:
           attrs[ATTR_DUE_AT] = next_buses[0].arrival_time if len(next_buses) > 0 else '-'

           if next_buses[0].position:
                attrs[ATTR_LATITUDE] = next_buses[0].position.latitude
                attrs[ATTR_LONGITUDE] = next_buses[0].position.longitude
        if len(next_buses) > 1:
            attrs[ATTR_NEXT_UP] = next_buses[1].arrival_time if len(next_buses) > 1 else '-'
        return attrs

    @property
    def unit_of_measurement(self):
        """Return the unit this state is expressed in."""
        return "min"

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

    def update(self):
        """Get the latest data from opendata.ch and update the states."""
        self.data.update()


class PublicTransportData(object):
    """The Class for handling the data retrieval."""

    def __init__(self, trip_url, route_deps, vehicle_position_url=None, api_key=None):
        """Initialize the info object."""
        self._trip_update_url = trip_url
        self._route_deps = route_deps
        self._vehicle_position_url = vehicle_position_url
  
        if api_key is not None:
            self._headers = {'x-api-key': api_key}
        else:
            self._headers = None
        self.info = {}

    @Throttle(MIN_TIME_BETWEEN_UPDATES)

    def update(self):
        positions = self._get_vehicle_positions() if self._vehicle_position_url else {}
        self._update_route_statuses(positions)

    def _update_route_statuses(self, vehicle_positions):
        """Get the latest data."""
        from google.transit import gtfs_realtime_pb2

        class StopDetails:
            def __init__(self, arrival_time, position):
                self.arrival_time = arrival_time
                self.position = position

        next_times = get_times(self._route_deps)

        feed = gtfs_realtime_pb2.FeedMessage()
        response = requests.get(self._trip_update_url, headers=self._headers)
        if response.status_code != 200:
            _LOGGER.error("updating route status got {}:{}".format(response.status_code,response.content))
        feed.ParseFromString(response.content)

        departure_times = {}

        ## check if any of the arrival times need to have a delay added or subtracted from GTFS feed

        for arrival_time in next_times:
          agent_no = arrival_time[0]
          service_no = arrival_time[1]
          trip_no  = arrival_time[2]
          route_no = arrival_time[3]
          stop_no  = arrival_time[4]
          operator_no = arrival_time[5]
          modified_time = int(arrival_time[6])

          vehicle_position = 0
          for entity in feed.entity:
             if entity.HasField('trip_update'):               
                 if entity.trip_update.trip.trip_id == trip_no:
                    for stop in entity.trip_update.stop_time_update:
                       if stop.HasField('arrival'):
                         if stop.stop_id == stop_no:
                           modified_time = modified_time + int(stop.arrival.delay / 60)
                 vehicle_position =  vehicle_positions.get(entity.trip_update.vehicle.id)

          ## build up the array of departure time objects

          if route_no not in departure_times:
              departure_times[route_no] = {}
          if not departure_times[route_no].get(stop_no):
              departure_times[route_no][stop_no] = []
          details = StopDetails(modified_time, vehicle_position)
          departure_times[route_no][stop_no].append(details)

        # Sort by arrival time

        for route in departure_times:
            for stop in departure_times[route]:
                departure_times[route][stop].sort(key=lambda t: t.arrival_time)

        self.info = departure_times

    def _get_vehicle_positions(self):
        ## not working for now
        from google.transit import gtfs_realtime_pb2
        feed = gtfs_realtime_pb2.FeedMessage()
        response = requests.get(self._vehicle_position_url, headers=self._headers)
        if response.status_code != 200:
            _LOGGER.error("updating vehicle positions got {}:{}.".format(response.status_code, response.content))
        feed.ParseFromString(response.content)
        positions = {}

        for entity in feed.entity:
            vehicle = entity.vehicle

            if not vehicle.trip.route_id:
                # Vehicle is not in service
                continue

            positions[vehicle.vehicle.id] = vehicle.position

        return positions

Ah, interesting. Back when I was more involved in GTFS-RT I seem to remember there not being a trip_id present but clearly there is now, cool!

Hi @zacs, are you still maintaining the GTFS-RT integration? I’ve encountered some issues and could use some help but understand if you’re no longer working on this.

I am (or will be), but I’ve had it disabled for myself for about a year due to COVID work from home policies. What issues are you running into?

Two issues. One relatively easy, one that’s left me a little perplexed.

  1. (Easy) Washington Metro (WMATA) uses the header api_key to pass its API key. I was able to modify sensor.py in my installation, but wanted to let you know.
  2. (Perplexing) Installing GTFS RT breaks the Nest integration and causes other interesting errors (like the inability to restart the server from the GUI). I’m assuming you’d like me to submit the logs and report the issue through GitHub?

Hi,

I tried using @zacs great gtfs realtime integration but there were a few issues with the data feed from my provider, the most notable was that every combination of route and calendar had its own route id. Consequently the route id changes often (sometimes daily). So, I have made a few updates to @zacs integration and published it on guthub.

The changes I made were:

  • Added a “route_delimiter” configuration variable to spit route ids (my provider has a format of - so needed to remove the calendar reference
  • Added “icon” configuration variable for each service so different icons can be used for trains, ferries and buses etc.
  • Added “service_type” configuration variable to set the “Next Bus”, “Next Train” etc attribute on the sensor in HA
  • Now uses trip id to search for vehicle position rather than vehicle id (my provider didn’t always include the vehicle id)
  • Ignores stop times that are in the past (my transport provider has an annoying habit of including services that finished hours ago in the realtime feed :frowning:)
  • Added some extra logging.

The integration is installable via HACS.

3 Likes

I can’t get seem to get it to work with this data set.

trip_update_url: 'https://data.calgary.ca/api/views/gs4m-mdc2/files/be59ec25-b97a-46ca-9297-4591fc0c2816?filename=tripupdates.pb'
vehicle_position_url: 'https://data.calgary.ca/api/views/am7c-qe3u/files/90adc2ee-dd09-4efd-90af-7c38d89466c9?filename=vehiclepositions.pb'
route_delimiter: '-'
departures:
- name: "test stop"
  route: '3'
  stopid: '6662'

It always returns - as the output. anyone help?

DEBUG:__main__:...Feed Route Id 3 changed to 3
DEBUG:__main__:......Stop: 6662 Stop Sequence: 1 Stop Time: 1674686100
DEBUG:__main__:......Stop: 5154 Stop Sequence: 2 Stop Time: 1674686170
DEBUG:__main__:......Stop: 8746 Stop Sequence: 3 Stop Time: 1674686234
DEBUG:__main__:......Stop: 5155 Stop Sequence: 4 Stop Time: 1674686293
DEBUG:__main__:......Stop: 8747 Stop Sequence: 5 Stop Time: 1674686347
DEBUG:__main__:......Stop: 5156 Stop Sequence: 6 Stop Time: 1674686409
DEBUG:__main__:......Stop: 7535 Stop Sequence: 7 Stop Time: 1674686474
DEBUG:__main__:......Stop: 7536 Stop Sequence: 8 Stop Time: 1674686540
DEBUG:__main__:......Stop: 7537 Stop Sequence: 9 Stop Time: 1674686600
DEBUG:__main__:......Stop: 5157 Stop Sequence: 10 Stop Time: 1674686647
DEBUG:__main__:......Stop: 5579 Stop Sequence: 11 Stop Time: 1674686700
DEBUG:__main__:......Stop: 5806 Stop Sequence: 12 Stop Time: 1674686756
DEBUG:__main__:......Stop: 5159 Stop Sequence: 13 Stop Time: 1674686847
DEBUG:__main__:......Stop: 8619 Stop Sequence: 14 Stop Time: 1674686895
DEBUG:__main__:......Stop: 8620 Stop Sequence: 15 Stop Time: 1674686948
DEBUG:__main__:......Stop: 8621 Stop Sequence: 16 Stop Time: 1674687002
DEBUG:__main__:......Stop: 5160 Stop Sequence: 17 Stop Time: 1674687060
INFO:__main__:Sensor Update:
INFO:__main__:...Name: 16W Ave N WB
INFO:__main__:...Route: 3
INFO:__main__:...Stop ID: 6662
INFO:__main__:...Direction ID: 0
INFO:__main__:...Icon: mdi:bus
INFO:__main__:...Service Type: Service
INFO:__main__:...unit_of_measurement: min
INFO:__main__:...Due in: -
INFO:__main__:...Due at not defined
INFO:__main__:...Latitude not defined
INFO:__main__:...Longitude not defined
INFO:__main__:...Next Service not defined

I couldn’t get any of the GTFS integrations to work so I used the generic REST sensor to get the data from my local public transportation’s API.

Let me know if you need help putting this together for whatever public transportation system you use!

Were you ever able to get this to work for WMATA? I am hoping to get some sensors set up in my instance.

I never was. I ended up using NodeRed to access the WMATA (and Fairfax Connector) APIs directly to get accurate arrival & departure information.

Care to test my GTFS2 integration?
Or provide me the links, with BART it does not work for realtime as it still assumes another format (that most other us)
vingerha/gtfs2: Support GTFS in Home Assistant GUI-only (github.com)

1 Like