GTFS and/or GTFS Realtime Not Working

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