GTFS and/or GTFS Realtime Not Working

Does anyone know if GTFS anything (realtime or not) is working?

When I try getting GTFS working I don’t get anything. No errors, no sensor, no nothing. It’s like it’s not even in my config.

Entry in my sensors.yaml file:

#TRANSIT INFORMATION
  - platform: gtfs
    origin: S09031
    destination: S00426
    data: kt.zip
    name: Route-502

When I try to implement gtfs_realtime I get the following error:

Platform error sensor.gtfs_realtime - Integration 'gtfs_realtime' not found.

My config in sensor.yaml file:

#REALTIME TRANSIT INFORMATION
  - platform: gtfs_realtime
#    trip_update_url: 'https://api.cityofkingston.ca/gtfs-realtime/tripupdates.pb'
    vehicle_position_url: 'https://api.cityofkingston.ca/gtfs-realtime'
    departures:
    - name: To Downtown
      route: 502
      stopid: 02037

and the gtfs_realtime.py file is located in /custom_components/sensor

I’ve double checked the spelling of the python file and the sensor config to make sure they were both the same and correct.

So I checked today and I am seeing the GTFS sensor but I am getting “No more departures today” in the info section.

As for the GTFS_realtime option, it’s dead. The python code is trying to load a requirement from https://github.com/google/gtfs-realtime-bindings which doesn’t exist anymore.

I had a use for this as well, so I took the old GTFS-RT integration and fixed it up to adhere to the modern requirements for custom_components. I’m using it here in Seattle, and seems to work pretty well. Thanks to the original authors who did the actual work, I just re-packaged/etc.

1 Like

@zacs Thanks for ‘just’ repackaging this. Followed your steps and presto I have a departure times. Well I hope they are the right departure times :slightly_smiling_face:

2 questions;
1st for the card you created, I assume you created 2 separate sensors for "Home to work’ and ‘home to airport’?
2nd when using the vehicle position, should the bus appear on the map automatically?

A big thank you for getting this working!!!

Glad it’s useful! Yep, you should be able to create multiple sensors for a card like that (that card is actually the original author’s so I can’t confirm, but I’ve got multiple sensors working in the same way). Totally unsure on vehicle position… I’ve never used it! In general, transit agency’s are a bit delayed on things that precise, so personally I wouldn’t trust it that much :slight_smile:

Thanks, I did some playing around and I was mostly able to get it to work. I discovered a problem that isn’t related to gtfs-rt but how my city publishes some data. Of course where I get on the bus I need is the origin point of the route. As such for some reason it doesn’t get a start time just a (-). So the next bus is always - away from arriving.

I need to do some more digging in the feed since the start and end stops are the same, maybe I can get the right time from the ‘end’ stop (if they have separate IDs)

@zacks I tried to add the repository but I’m getting this error message

20-06-22 19:45:31 INFO (MainThread) [supervisor.store.git] Clone add-on https://github.com/zacs/ha-gtfs-rt.git repository
20-06-22 19:45:33 ERROR (MainThread) [supervisor.utils.json] Can't read json from /data/addons/git/ef121c36/repository.json: [Errno 2] No such file or directory: '/data/addons/git/ef121c36/repository.json'
20-06-22 19:45:33 WARNING (MainThread) [supervisor.store.data] Can't read repository information from /data/addons/git/ef121c36/repository.json

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?