import os import logging import datetime import math import re from six.moves.urllib.request import urlopen from six.moves.urllib.parse import urlencode import aniso8601 from flask import Flask, json, render_template from flask_ask import Ask, request, session, question, statement ENDPOINT = "http://tidesandcurrents.noaa.gov/api/datagetter" SESSION_CITY = "city" SESSION_DATE = "date" # NOAA station codes STATION_CODE_SEATTLE = "9447130" STATION_CODE_SAN_FRANCISCO = "9414290" STATION_CODE_MONTEREY = "9413450" STATION_CODE_LOS_ANGELES = "9410660" STATION_CODE_SAN_DIEGO = "9410170" STATION_CODE_BOSTON = "8443970" STATION_CODE_NEW_YORK = "8518750" STATION_CODE_VIRGINIA_BEACH = "8638863" STATION_CODE_WILMINGTON = "8658163" STATION_CODE_CHARLESTON = "8665530" STATION_CODE_BEAUFORT = "8656483" STATION_CODE_MYRTLE_BEACH = "8661070" STATION_CODE_MIAMI = "8723214" STATION_CODE_TAMPA = "8726667" STATION_CODE_NEW_ORLEANS = "8761927" STATION_CODE_GALVESTON = "8771341" STATIONS = {} STATIONS["seattle"] = STATION_CODE_SEATTLE STATIONS["san francisco"] = STATION_CODE_SAN_FRANCISCO STATIONS["monterey"] = STATION_CODE_MONTEREY STATIONS["los angeles"] = STATION_CODE_LOS_ANGELES STATIONS["san diego"] = STATION_CODE_SAN_DIEGO STATIONS["boston"] = STATION_CODE_BOSTON STATIONS["new york"] = STATION_CODE_NEW_YORK STATIONS["virginia beach"] = STATION_CODE_VIRGINIA_BEACH STATIONS["wilmington"] = STATION_CODE_WILMINGTON STATIONS["charleston"] = STATION_CODE_CHARLESTON STATIONS["beaufort"] = STATION_CODE_BEAUFORT STATIONS["myrtle beach"] = STATION_CODE_MYRTLE_BEACH STATIONS["miami"] = STATION_CODE_MIAMI STATIONS["tampa"] = STATION_CODE_TAMPA STATIONS["new orleans"] = STATION_CODE_NEW_ORLEANS STATIONS["galveston"] = STATION_CODE_GALVESTON app = Flask(__name__) ask = Ask(app, "/") logging.getLogger('flask_ask').setLevel(logging.DEBUG) class TideInfo(object): def __init__(self): self.first_high_tide_time = None self.first_high_tide_height = None self.low_tide_time = None self.low_tide_height = None self.second_high_tide_time = None self.second_high_tide_height = None @ask.launch def launch(): welcome_text = render_template('welcome') help_text = render_template('help') return question(welcome_text).reprompt(help_text) @ask.intent('OneshotTideIntent', mapping={'city': 'City', 'date': 'Date'}, convert={'date': 'date'}, default={'city': 'seattle', 'date': datetime.date.today }) def one_shot_tide(city, date): if city.lower() not in STATIONS: return supported_cities() return _make_tide_request(city, date) @ask.intent('DialogTideIntent', mapping={'city': 'City', 'date': 'Date'}, convert={'date': 'date'}) def dialog_tide(city, date): if city is not None: if city.lower() not in STATIONS: return supported_cities() if SESSION_DATE not in session.attributes: session.attributes[SESSION_CITY] = city return _dialog_date(city) date = aniso8601.parse_date(session.attributes[SESSION_DATE]) return _make_tide_request(city, date) elif date is not None: if SESSION_CITY not in session.attributes: session.attributes[SESSION_DATE] = date.isoformat() return _dialog_city(date) city = session.attributes[SESSION_CITY] return _make_tide_request(city, date) else: return _dialog_no_slot() @ask.intent('SupportedCitiesIntent') def supported_cities(): cities = ", ".join(sorted(STATIONS.keys())) list_cities_text = render_template('list_cities', cities=cities) list_cities_reprompt_text = render_template('list_cities_reprompt') return question(list_cities_text).reprompt(list_cities_reprompt_text) @ask.intent('AMAZON.HelpIntent') def help(): help_text = render_template('help') list_cities_reprompt_text = render_template('list_cities_reprompt') return question(help_text).reprompt(list_cities_reprompt_text) @ask.intent('AMAZON.StopIntent') def stop(): bye_text = render_template('bye') return statement(bye_text) @ask.intent('AMAZON.CancelIntent') def cancel(): bye_text = render_template('bye') return statement(bye_text) @ask.session_ended def session_ended(): return "{}", 200 @app.template_filter() def humanize_date(dt): # http://stackoverflow.com/a/20007730/1163855 ordinal = lambda n: "%d%s" % (n,"tsnrhtdd"[(n/10%10!=1)*(n%10<4)*n%10::4]) month_and_day_of_week = dt.strftime('%A %B') day_of_month = ordinal(dt.day) year = dt.year if dt.year != datetime.datetime.now().year else "" formatted_date = "{} {} {}".format(month_and_day_of_week, day_of_month, year) formatted_date = re.sub('\s+', ' ', formatted_date) return formatted_date @app.template_filter() def humanize_time(dt): morning_threshold = 12 afternoon_threshold = 17 evening_threshold = 20 hour_24 = dt.hour if hour_24 < morning_threshold: period_of_day = "in the morning" elif hour_24 < afternoon_threshold: period_of_day = "in the afternoon" elif hour_24 < evening_threshold: period_of_day = "in the evening" else: period_of_day = " at night" the_time = dt.strftime('%I:%M') formatted_time = "{} {}".format(the_time, period_of_day) return formatted_time @app.template_filter() def humanize_height(height): round_down_threshold = 0.25 round_to_half_threshold = 0.75 is_negative = False if height < 0: height = abs(height) is_negative = True remainder = height % 1 if remainder < round_down_threshold: remainder_text = "" feet = int(math.floor(height)) elif remainder < round_to_half_threshold: remainder_text = "and a half" feet = int(math.floor(height)) else: remainder_text = "" feet = int(math.floor(height)) if is_negative: feet *= -1 formatted_height = "{} {} feet".format(feet, remainder_text) formatted_height = re.sub('\s+', ' ', formatted_height) return formatted_height def _dialog_no_slot(): if SESSION_CITY in session.attributes: date_dialog2_text = render_template('date_dialog2') return question(date_dialog2_text).reprompt(date_dialog2_text) else: return supported_cities() def _dialog_date(city): date_dialog_text = render_template('date_dialog', city=city) date_dialog_reprompt_text = render_template('date_dialog_reprompt') return question(date_dialog_text).reprompt(date_dialog_reprompt_text) def _dialog_city(date): session.attributes[SESSION_DATE] = date session.attributes_encoder = _json_date_handler city_dialog_text = render_template('city_dialog', date=date) city_dialog_reprompt_text = render_template('city_dialog_reprompt') return question(city_dialog_text).reprompt(city_dialog_reprompt_text) def _json_date_handler(obj): if isinstance(obj, datetime.date): return obj.isoformat() def _make_tide_request(city, date): station = STATIONS.get(city.lower()) noaa_api_params = { 'station': station, 'product': 'predictions', 'datum': 'MLLW', 'units': 'english', 'time_zone': 'lst_ldt', 'format': 'json' } if date == datetime.date.today(): noaa_api_params['date'] = 'today' else: noaa_api_params['begin_date'] = date.strftime('%Y%m%d') noaa_api_params['range'] = 24 url = ENDPOINT + "?" + urlencode(noaa_api_params) resp_body = urlopen(url).read() if len(resp_body) == 0: statement_text = render_template('noaa_problem') else: noaa_response_obj = json.loads(resp_body) predictions = noaa_response_obj['predictions'] tideinfo = _find_tide_info(predictions) statement_text = render_template('tide_info', date=date, city=city, tideinfo=tideinfo) return statement(statement_text).simple_card("Tide Pooler", statement_text) def _find_tide_info(predictions): """ Algorithm to find the 2 high tides for the day, the first of which is smaller and occurs mid-day, the second of which is larger and typically in the evening. """ last_prediction = None first_high_tide = None second_high_tide = None low_tide = None first_tide_done = False for prediction in predictions: if last_prediction is None: last_prediction = prediction continue if last_prediction['v'] < prediction['v']: if not first_tide_done: first_high_tide = prediction else: second_high_tide = prediction else: # we're decreasing if not first_tide_done and first_high_tide is not None: first_tide_done = True elif second_high_tide is not None: break # we're decreasing after having found the 2nd tide. We're done. if first_tide_done: low_tide = prediction last_prediction = prediction fmt = '%Y-%m-%d %H:%M' parse = datetime.datetime.strptime tideinfo = TideInfo() tideinfo.first_high_tide_time = parse(first_high_tide['t'], fmt) tideinfo.first_high_tide_height = float(first_high_tide['v']) tideinfo.second_high_tide_time = parse(second_high_tide['t'], fmt) tideinfo.second_high_tide_height = float(second_high_tide['v']) tideinfo.low_tide_time = parse(low_tide['t'], fmt) tideinfo.low_tide_height = float(low_tide['v']) return tideinfo if __name__ == '__main__': if 'ASK_VERIFY_REQUESTS' in os.environ: verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() if verify == 'false': app.config['ASK_VERIFY_REQUESTS'] = False app.run(debug=True)