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