-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathmbta.py
345 lines (237 loc) · 10.3 KB
/
mbta.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# Handle MBTA API calls
from mycroft.util.parse import match_one
import requests
import re
class MBTA():
################ API DISCLAIMER ################
#
# The Massachusetts Department of Transportation (MassDOT)
# is the provider of MBTA Data.
# The author is not nn agent or partner of
# MassDOT or its agencies and authorities
def __init__(self, apiKey, trackCount):
self.routeInfo = None; # dictionary with info on all bus routes
# error flag valid after API is called
self.serverError = False
# save API key
self.apiKey = apiKey;
self.currentRoute = None # entry from routInfo for selected route
self.currentDirections = None # direction options for current route - list of tuples
self.currentDirection = "" # used in queries, index into current route directions as string
self.stopId = "" # internal ID of selected bus stop, used to get predicitons
self.stopName ="" # text name of bus stop
self.busStops = dict() # dictionary of stop names, ids in slected direction
self.predTimes = dict() # dictionary of trip ids, predictated arrival times
self.maxTrackCnt = int(trackCount) # max # of buses to track
self.lastTrack = "" # last trip to track - stop when no longer in predictions
self.savedrequet = ''
# settings have been changed on Home
def updateSettings(self, apiKey, trackCount):
self.apiKey = apiKey
self.maxTrackCnt = int(trackCount) # max # of buses to track
# reset class, call when stopping tracking
def reset(self):
self.currentRoute = None
self.currentDirection = ""
self.stopId = ""
# get data from MBTA API at given endpoint with passed arguments
def _getData(self,endPoint, args=None):
retVal = None;
# clear error flag
self.serverError = False
# base url
api_url = "https://api-v3.mbta.com/{}".format(endPoint)
# if we are using an api key and have args
if self.apiKey != None and args != None:
# url?key%args
api_url = "{}?api_key={}&{}".format(api_url,self.apiKey,args)
elif self.apiKey != None:
# url?key
api_url = "{}?api_key={}".format(api_url,self.apiKey)
elif args != None :
# url?args
api_url = "{}?{}".format(api_url,args)
try:
# get requested data
r = requests.get(api_url)
# check if we got any data before setting return value
retVal = r.json()['data'] if len(r.json()['data']) > 0 else None
except:
# set error flag
self.serverError = True
return retVal
# API calls die silently, return true if last call
# resulted in an error
def callError(self):
return self.serverError
# information on all MBTA bus routes is read from server
# not all information is relevant to skill, we build a
# dictionary with the info we need
# this API call is necessary before getting any predictions
# and will only be done once
def readRoutes(self):
# if route info has not been read yet
if( self.routeInfo == None ):
# create empty dicitonary
self.routeInfo = dict()
# get info on all bus routes - only called once
routes = self._getData('routes',"filter[type]=3&sort=sort_order")
if routes != None:
# build dictionary with the info we need on each route
# key is short name
for rt in routes:
self.routeInfo[rt['attributes']['short_name']] = {
'id': rt['id'], # id
'short_name' : rt['attributes']['short_name'],# short name
'long_name' : rt['attributes']['long_name'], # long name
'dirs' : rt['attributes']['direction_names'], # directions
'dest' : rt['attributes']['direction_destinations'] # terminus
}
# set current route based on passed name
# return route name or None
def setRoute(self, routeName):
self.currentRoute = None;
# make certain routes are loaded
self.readRoutes()
# look in the dictionary
rt = self.routeInfo.get(routeName)
# if we got a valid route
if rt != None:
self.currentRoute = self.routeInfo[routeName]
return(None if not rt else self.currentRoute['short_name'])
# return object that can be saved to settings
# to remember current route and direction
def getRouteSettings(self):
# current route basic info
currRoute = self.currentRoute
# add current direction and stop id
currRoute["direction"] = self.currentDirection
currRoute["stopid"] = self.stopId
currRoute["stopName"] = self.stopName
return(currRoute)
# restore a root saved in settings
# return route name
def restoreRoute(self, rt):
# current direction, stop id and stop name are not in route dict
self.currentDirection = rt.pop('direction', None)
self.stopId = rt.pop('stopid', None);
self.stopName = rt.pop('stopName')
# now route is reduced to proper contents of current route
self.currentRoute = rt
# fill current directions array
self.getDirections()
return self.currentRoute['short_name'];
# getters for info on restored route
def getStopName(self):
return self.stopName
def getDirDest(self):
return(self.currentDirections[self.currentDirection])
# return list of directions and destinations
# each element in list is direction, destination tuple
# directions are usually Inbound and Outbound
def getDirections(self):
self.currentDirections = []
# append tuple for each direction
for idx, d in enumerate(self.currentRoute["dirs"]):
self.currentDirections.append((self.currentRoute["dirs"][idx],self.currentRoute["dest"][idx]))
return(self.currentDirections)
# pass string for direction - could be inbound, outboud or terminus
# a tuple of (direction name, destination name) for best match is returned
def setDirection(self, str):
# build list of directions and destinations as strings
dirList = [' '.join(x).upper() for x in self.currentDirections]
# select one destinaiton
dirKey, dirConfidence = match_one(str.upper(), dirList)
# set direction id to index of selected destination
self.currentDirection = dirList.index(dirKey)
return(self.currentDirections[self.currentDirection])
# format stop name to (hopefully) match spoken version
def formatStopName(self, str):
# Street for St
str = re.sub(r'St ', 'Street ', str)
str = re.sub(r' St$', ' Street ', str)
# opposite for opp
str = re.sub(r'opp ', 'opposite ', str)
return(str)
# build dictionary of bus stops for current route in selected direction
# key is name, to compare with utterance
# value is id, to use for API calls
def getStops(self):
# ask API for stops
routeStops = self._getData('stops',
"filter[direction_id]={}&filter[route]={}"
.format(self.currentDirection, self.currentRoute["id"]))
# empty dictionary
self.busStops = dict()
# create entry in dictionary for each bus stop
for stop in routeStops:
stopKey = self.formatStopName(stop['attributes']['name'])
stopKey = stopKey.lower()
self.busStops[stopKey] = stop['id']
# a bus stop name is passed, match to stop on route and set stop id
# return name found
def setStop(self, stopName):
# build dictionay of stops if necessary
self.getStops()
# find closest match on route
theStop = key, confidence = match_one(stopName, list(self.busStops))
self.stopId = self.busStops.get(key);
# record stop name
self.stopName = theStop[0]
return(self.stopName)
# get arrival predictions for current route in the selected direcation at the chosen stop
# return (possibly empty) list of arrival time, trip id tuples
def getPredictions(self):
predList = []
# ask API for predictions
predictions = self._getData('predictions',
"filter[direction_id]={}&filter[route]={}&filter[stop]={}"
.format(self.currentDirection,self.currentRoute["id"],self.stopId))
# if we got valid predictions
if predictions != None:
# build list of arrival time, trip id tuples
predList = list(map(lambda x: (x['attributes']['arrival_time'],
x['relationships']['trip']['data']['id']), predictions))
# remove tuples where arrival time is none
predList = [p for p in predList if p[0] != None ]
return(predList)
# return arrival predictions as list
# predictions are datatime objects with time zone
def getArrivals(self):
# get predictions
self.predTimes = self.getPredictions()
# only return prediction times
return(None if len(self.predTimes) == 0 else [x[0] for x in self.predTimes])
# begin tracking buses, return list of arrival predictions
def startTracking(self):
# get predictions
self.predTimes = self.getPredictions()
# if we got any predictions
if len(self.predTimes) > 0:
# record last trip we will track
self.lastTrack = self.predTimes[min(len(self.predTimes),self.maxTrackCnt)-1][1]
# only return prediction times
# predictions are datatime objects with time zone
return(None if len(self.predTimes) == 0 else [x[0] for x in self.predTimes][:self.maxTrackCnt])
# call periodically for current predictions of bus being tracked
# startTracking must be called first to record the last trip tracked
# return list of arrival predictions
def updateTracking(self):
idx = -1 # assume we won't find the last tracked bus
# get predictions
self.predTimes = self.getPredictions()
if self.predTimes != None:
# get index of last tracked bus
try:
# build list with trip id of last tracked bus then find its index in prediction list
idx = self.predTimes.index([x for x in self.predTimes if x[1] == self.lastTrack][0])
except:
self.reset()
# return prediction times up to last tracked bus
# if we got valid predictions and last
# tracked trip is still in list
return(None if idx < 0
else [x[0] for x in self.predTimes][:idx+1])
# call when done tracking or stopped by voice command
def stopTracking(self):
self.reset()