Skip to content

Commit

Permalink
ENH Generalize to other bike share networks
Browse files Browse the repository at this point in the history
Multiple bikeshare networks use the same API as Divvy. Move the Divvy-specific settings and language into the configuration file so that this skill can switch to any network.
  • Loading branch information
Stephen Hoover committed Oct 8, 2016
1 parent 1b03ee4 commit 4086e74
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 35 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# Chicago Bikeshare

A skill for Amazon Alexa<br>
http://www.alexaskillstore.com/

## Overview

Expand Down Expand Up @@ -65,7 +64,12 @@ in the environment, but only v1.3. This Skill needs v1.4.)
The skill requires an additional `config.py` file in the "divvy" folder.
This file should define the following attributes at global level:
- APP_ID : The unique ID of the Skill which uses the Lambda
- divvy_api : Web address of the Divvy API. As of September 2016, this is https://feeds.divvybikes.com/stations/stations.json.
- network_name : The name of the bike sharing network, e.g. "Divvy" or "CoGo"
- default_state : The two letter state code in which the network operates, e.g. "IL"
- default_city : The name of the city in which the network operates, e.g. "Chicago"
- time_zone : The local time zone, e.g. "US/Central" or "US/Eastern"
- sample_station : A valid station name for use in the help prompt
- divvy_api : Web address of the bike sharing network's API. As of September 2016, the Divvy network's API is https://feeds.divvybikes.com/stations/stations.json.
- maps_api : Web address of the Google Maps Geocoding API (used when users store addresses). As of September 2016, this is https://maps.googleapis.com/maps/api/geocode/.
- maps_api_key : Token which allows access to the Google Maps Geocoding API
- aws_region : Region in which you have your database
Expand Down
44 changes: 26 additions & 18 deletions divvy/handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,15 @@ def intent(req, session):
elif intent['name'] == 'AMAZON.HelpIntent':
return reply.build("You can ask me how many bikes or docks are "
"at a specific station, or else just ask the "
"status of a station. Use the Divvy station "
"name, such as "
"\"Milwaukee Avenue and Rockwell Street\". "
"status of a station. Use the %s station "
"name, such as \"%s\". "
"If you only remember one cross-street, you "
"can ask me to list all stations on a particular "
"street. If you've told me to \"add an address\", "
"I can remember that and use it when you "
"ask me to \"check my commute\". "
"What should I do?",
"What should I do?" %
(config.network_name, config.sample_station),
persist=session['attributes'],
is_end=False)
else:
Expand All @@ -170,8 +170,8 @@ def intent(req, session):


def _time_string():
"""Return a string representing local Chicago time"""
os.environ['TZ'] = 'US/Central' # Chicago!
"""Return a string representing local time"""
os.environ['TZ'] = config.time_zone
time.tzset()
return time.asctime()

Expand All @@ -185,12 +185,12 @@ def _station_from_intent(intent, stations):
JSON following the Alexa "IntentRequest"
schema with name "CheckBikeIntent"
stations : list of dict
JSON following the Divvy "stationBeanList" schema
JSON following the bikeshare "stationBeanList" schema
Returns
-------
dict
A single station information JSON from the Divvy API response
A single station information JSON from the bikeshare API response
Raises
------
Expand Down Expand Up @@ -341,7 +341,8 @@ def check_commute(intent, session):
nearest_st[1]['stationName']))

return reply.build(utter,
card_title="Your Divvy Commute Status",
card_title=("Your %s Commute Status" %
config.network_name),
card_text='\n'.join(card_text),
is_end=True)

Expand Down Expand Up @@ -478,22 +479,27 @@ def add_address(intent, session):
return add_address(intent, session)
elif sess_data['next_step'] == 'check_address':
if sess_data['zip_code']:
# Assume that Divvy subscribers are always interested
# in Illinois addresses, but not necessarily Chicago.
addr = '%s, IL, %s' % (sess_data['spoken_address'],
# Assume that network subscribers are always interested
# in in-state addresses, but not necessarily in the city.
addr = '%s, %s, %s' % (sess_data['spoken_address'],
config.default_state,
sess_data['zip_code'])
else:
# Without a zip code, assume Chicago
# Without a zip code, assume the network's home city
# to add necessary specificity.
addr = '%s, Chicago, IL' % sess_data['spoken_address']
addr = '%s, %s, %s' % (sess_data['spoken_address'],
config.default_city,
config.default_state)
lat, lon, full_address = geocoding.get_lat_lon(addr)
if full_address.endswith(", USA"):
# We don't need to keep the country name.
full_address = full_address[:-5]

if full_address.lower().startswith("chicago, il"):
if full_address.lower().startswith("%s, %s" %
(config.default_city.lower(),
config.default_state.lower())):
# If the geocoding fails to find a specific address,
# it will return a generic "Chicago" location.
# it will return a generic city location.
sess_data['next_step'] = 'num_and_name'
return reply.build("I'm sorry, I heard the address \"%s\", "
"but I can't figure out where that is. "
Expand Down Expand Up @@ -744,7 +750,8 @@ def list_stations(intent, session):
sta_name = location.text_to_speech(possible[0]['stationName'])
return reply.build("There's only one: the %s "
"station." % sta_name,
card_title="Divvy Stations on %s" % street_name,
card_title=("%s Stations on %s" %
(config.network_name, street_name)),
card_text=("One station on %s: %s" %
(street_name, possible[0]['stationName'])),
is_end=True)
Expand All @@ -759,6 +766,7 @@ def list_stations(intent, session):
(len(possible), street_name,
'\n'.join(p['stationName'] for p in possible)))
return reply.build(speech,
card_title="Divvy Stations on %s" % street_name,
card_title=("%s Stations on %s" %
(config.network_name, street_name)),
card_text=card_text,
is_end=True)
27 changes: 14 additions & 13 deletions divvy/location.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Match spoken station names and addresses to the
stored station information which comes back from
the Divvy API.
the bikeshare network's API.
"""
import difflib
import logging
Expand All @@ -10,7 +10,7 @@

# Create a couple of lookup tables to go
# between the name format given to us by
# Divvy and the transcription of spoken words.
# the API and the transcription of spoken words.
ABBREV = {' st ': ' street ',
' pl ': ' place ',
' ave ': ' avenue ',
Expand All @@ -20,7 +20,8 @@
' ln ': ' lane ',
' pkwy ': ' parkway ',
' ter ': ' terrace ',
' ct ': ' court '}
' ct ': ' court ',
' mt ': ' mount '}

DIRECTIONS = {' n ': ' north ',
' w ': ' west ',
Expand Down Expand Up @@ -57,14 +58,14 @@ def _check_possible(possible, first, second=None):


def matching_station_list(stations, first, second=None, exact=False):
"""Filter the Divvy station list based on locations
"""Filter the station list based on locations
May return multiple stations
Parameters
----------
stations : list of dict
The 'stationBeanList' from the Divvy API response
The 'stationBeanList' from the bikeshare API response
first : str
The first component of a station name or address,
e.g. "Larrabee" or "Larrabee Street".
Expand All @@ -79,7 +80,7 @@ def matching_station_list(stations, first, second=None, exact=False):
Returns
-------
list of dict
List of station status JSONs from the Divvy API response
List of station status JSONs from the bikeshare API response
"""
possible = []
first = speech_to_text(first)
Expand Down Expand Up @@ -165,12 +166,12 @@ def _fuzzy_match_two(first, second, stations):


def find_station(stations, first, second=None, exact=False):
"""Filter the Divvy station list to find a single station
"""Filter the station list to find a single station
Parameters
----------
stations : list of dict
The 'stationBeanList' from the Divvy API response
The 'stationBeanList' from the bikeshare API response
first : str
The first component of a station name or address,
e.g. "Larrabee" or "Larrabee Street".
Expand All @@ -184,7 +185,7 @@ def find_station(stations, first, second=None, exact=False):
Returns
-------
dict
A single station status JSON from the Divvy API response
A single station status JSON from the bikeshare API response
Raises
------
Expand All @@ -196,7 +197,7 @@ def find_station(stations, first, second=None, exact=False):


def speech_to_text(address):
"""Standardize speech input to look like Divvy station names
"""Standardize speech input to look like station names in the network
"""
# Add a space, since we look for spaces after abbreviations
address = address.lower() + ' '
Expand Down Expand Up @@ -224,9 +225,9 @@ def text_to_speech(address):
return address.strip()


def get_stations(divvy_api):
"""Query the Divvy API and return the station list"""
resp = requests.get(divvy_api)
def get_stations(bike_api):
"""Query the bikeshare API and return the station list"""
resp = requests.get(bike_api)
stations = resp.json()['stationBeanList']

return stations
6 changes: 4 additions & 2 deletions lambda_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ def lambda_handler(event, context):
if event['request']['type'] == "IntentRequest":
return handle.intent(event['request'], event['session'])
elif event['request']['type'] == "LaunchRequest":
return reply.build("Ask me a question about a Divvy station.",
return reply.build("Ask me a question about a "
"%s station." % config.network_name,
is_end=False)
elif event['request']['type'] == "SessionEndedRequest":
return reply.build("Bike safe!", is_end=True)
else:
# I don't think there's any other kinds of requests.
return reply.build("Ask me a question about a Divvy station.",
return reply.build("Ask me a question about a "
"%s station." % config.network_name,
is_end=False)
except Exception as err: # NOQA
log.exception('Unhandled exception for event\n%s\n' % str(event))
Expand Down

0 comments on commit 4086e74

Please sign in to comment.