diff --git a/hitomi/settings.py b/hitomi/settings.py index ac1aaa7..32fc98b 100644 --- a/hitomi/settings.py +++ b/hitomi/settings.py @@ -39,6 +39,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'bot', 'dice', + 'weather', ] MIDDLEWARE = [ @@ -125,6 +126,7 @@ STATIC_URL = '/static/' # default bot settings DISCORD_BOT_TOKEN = 'token' +WEATHER_WEATHER_UNDERGROUND_API_KEY = 'key' # get local settings diff --git a/weather/__init__.py b/weather/__init__.py new file mode 100644 index 0000000..ea2d8fc --- /dev/null +++ b/weather/__init__.py @@ -0,0 +1,8 @@ +"""Provide weather information over Discord.""" +from bot import hitomi +from weather import lib + + +@hitomi.command() +async def weather(query: str): + await hitomi.say(lib.get_conditions_for_query([query])) diff --git a/weather/lib.py b/weather/lib.py new file mode 100644 index 0000000..699689b --- /dev/null +++ b/weather/lib.py @@ -0,0 +1,196 @@ +# coding: utf-8 +"""Library methods for looking up and presenting weather.""" +import logging +import re +import requests + +from django.conf import settings + + +wu_base_url = 'http://api.wunderground.com/api/{0:s}/'.format(settings.WEATHER_WEATHER_UNDERGROUND_API_KEY) +log = logging.getLogger('weather.lib') + + +def get_conditions_for_query(queryitems): + """Make a wunderground conditions call, return as string.""" + + # recombine the query into a string + query = ' '.join(queryitems) + query = query.replace(' ', '_') + + try: + url = wu_base_url + ('{0:s}/q/{1:s}.json'.format('conditions', query)) + log.debug("calling %s", url) + resp = requests.get(url) + condition_data = resp.json() + except IOError as e: + log.error("error while making conditions query") + log.exception(e) + raise + + # condition data is loaded. the rest of this is obviously specific to + # http://www.wunderground.com/weather/api/d/docs?d=data/conditions + log.debug(condition_data) + + try: + # just see if we have current_observation data + current = condition_data['current_observation'] + except KeyError as e: + # ok, try to see if the ambiguous results stuff will help + log.debug(e) + log.debug("potentially ambiguous results, checking") + try: + results = condition_data['response']['results'] + reply = "Multiple results, try one of the following zmw codes:" + for res in results[:-1]: + q = res['l'].strip('/q/') + reply += " {0:s} ({1:s}, {2:s}),".format(q, res['name'], res['country_name']) + q = results[-1]['l'].strip('/q/') + reply += " or {0:s} ({1:s}, {2:s}).".format(q, results[-1]['name'], results[-1]['country_name']) + return reply + except KeyError as e: + # now we really know something is wrong + log.error("error or bad query in conditions lookup") + log.exception(e) + return "No results." + else: + try: + location = current['display_location']['full'] + reply = "Conditions for {0:s}: ".format(location) + + weather_str = current['weather'] + if weather_str != '': + reply += "{0:s}, ".format(weather_str) + + temp_f = current['temp_f'] + temp_c = current['temp_c'] + temp_str = current['temperature_string'] + if temp_f != '' and temp_c != '': + reply += "{0:.1f}°F ({1:.1f}°C)".format(temp_f, temp_c) + elif temp_str != '': + reply += "{0:s}".format(temp_str) + + # append feels like if we have it + feelslike_f = current['feelslike_f'] + feelslike_c = current['feelslike_c'] + feelslike_str = current['feelslike_string'] + if feelslike_f != '' and feelslike_c != '': + reply += ", feels like {0:s}°F ({1:s}°C)".format(feelslike_f, feelslike_c) + elif feelslike_str != '': + reply += ", feels like {0:s}".format(feelslike_str) + + # whether this is current or current + feelslike, terminate sentence + reply += ". " + + humidity_str = current['relative_humidity'] + if humidity_str != '': + reply += "Humidity: {0:s}. ".format(humidity_str) + + wind_str = current['wind_string'] + if wind_str != '': + reply += "Wind: {0:s}. ".format(wind_str) + + pressure_in = current['pressure_in'] + pressure_trend = current['pressure_trend'] + if pressure_in != '': + reply += "Pressure: {0:s}\"".format(pressure_in) + if pressure_trend != '': + reply += " and {0:s}".format("dropping" if pressure_trend == '-' else "rising") + reply += ". " + + heat_index_str = current['heat_index_string'] + if heat_index_str != '' and heat_index_str != 'NA': + reply += "Heat index: {0:s}. ".format(heat_index_str) + + windchill_str = current['windchill_string'] + if windchill_str != '' and windchill_str != 'NA': + reply += "Wind chill: {0:s}. ".format(windchill_str) + + visibility_mi = current['visibility_mi'] + if visibility_mi != '': + reply += "Visibility: {0:s} miles. ".format(visibility_mi) + + precip_in = current['precip_today_in'] + if precip_in != '': + reply += "Precipitation today: {0:s}\". ".format(precip_in) + + observation_time = current['observation_time'] + if observation_time != '': + reply += "{0:s}. ".format(observation_time) + + return _prettify_weather_strings(reply.rstrip()) + except KeyError as e: + log.error("error or unexpected results in conditions reply") + log.exception(e) + return "Error parsing results." + + +def get_forecast_for_query(queryitems): + """Make a wunderground forecast call, return as string.""" + + # recombine the query into a string + query = ' '.join(queryitems) + query = query.replace(' ', '_') + + try: + url = wu_base_url + ('{0:s}/q/{1:s}.json'.format('forecast', query)) + resp = requests.get(url) + forecast_data = resp.json() + except IOError as e: + log.error("error while making forecast query") + log.exception(e) + raise + + # forecast data is loaded. the rest of this is obviously specific to + # http://www.wunderground.com/weather/api/d/docs?d=data/forecast + log.debug(forecast_data) + + try: + # just see if we have forecast data + forecasts = forecast_data['forecast']['txt_forecast'] + except KeyError as e: + # ok, try to see if the ambiguous results stuff will help + log.debug(e) + log.debug("potentially ambiguous results, checking") + try: + results = forecast_data['response']['results'] + reply = "Multiple results, try one of the following zmw codes:" + for res in results[:-1]: + q = res['l'].strip('/q/') + reply += " {0:s} ({1:s}, {2:s}),".format(q, res['name'], + res['country_name']) + q = results[-1]['l'].strip('/q/') + reply += " or {0:s} ({1:s}, {2:s}).".format(q, results[-1]['name'], + results[-1]['country_name']) + return reply + except KeyError as e: + # now we really know something is wrong + log.error("error or bad query in forecast lookup") + log.exception(e) + return "No results." + else: + try: + reply = "Forecast: " + for forecast in forecasts['forecastday'][0:5]: + reply += "{0:s}: {1:s} ".format(forecast['title'], + forecast['fcttext']) + + return _prettify_weather_strings(reply.rstrip()) + except KeyError as e: + log.error("error or unexpected results in forecast reply") + log.exception(e) + return "Error parsing results." + + +def _prettify_weather_strings(weather_str): + """ + Clean up output strings. + + For example, turn 32F into 32°F in input string. + + Input: + weather_str --- the string to clean up + + """ + + return re.sub(r'(\d+)\s*([FC])', r'\1°\2', weather_str)