diff --git a/dr_botzo/dr_botzo/settings.py b/dr_botzo/dr_botzo/settings.py index edb28b5..a89939e 100644 --- a/dr_botzo/dr_botzo/settings.py +++ b/dr_botzo/dr_botzo/settings.py @@ -127,6 +127,13 @@ IRCBOT_SSL = False IRCBOT_IPV6 = False +# IRC module stuff + +# weather + +WEATHER_WEATHER_UNDERGROUND_API_KEY = None + + # load local settings try: diff --git a/dr_botzo/weather/__init__.py b/dr_botzo/weather/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dr_botzo/weather/ircplugin.py b/dr_botzo/weather/ircplugin.py new file mode 100644 index 0000000..7255e45 --- /dev/null +++ b/dr_botzo/weather/ircplugin.py @@ -0,0 +1,57 @@ +import logging + +from ircbot.lib import Plugin + +from weather.lib import get_conditions_for_query, get_forecast_for_query + + +log = logging.getLogger('weather.ircplugin') + + +class Weather(Plugin): + + """Have IRC commands to do IRC things (join channels, quit, etc.).""" + + def start(self): + """Set up the handlers.""" + + self.connection.reactor.add_global_regex_handler('pubmsg', r'^!weather\s+(.*)$', + getattr(self, 'handle_weather'), -20) + self.connection.reactor.add_global_regex_handler('privmsg', r'^^!weather\s+(.*)$', + getattr(self, 'handle_weather'), -20) + + super(Weather, self).start() + + def stop(self): + """Tear down handlers.""" + + self.connection.reactor.remove_global_regex_handler('pubmsg', getattr(self, 'handle_weather')) + self.connection.reactor.remove_global_regex_handler('privmsg', getattr(self, 'handle_weather')) + + super(Weather, self).stop() + + def handle_weather(self, connection, event, match): + """Handle IRC input for a weather queries.""" + + # see if there was a specific command given in the query, first + query = match.group(1) + queryitems = query.split(" ") + if len(queryitems) <= 0: + return + + # search for commands + if queryitems[0] == 'conditions': + # current weather query + results = get_conditions_for_query(queryitems[1:]) + return self.bot.reply(event, results) + elif queryitems[0] == 'forecast': + # forecast query + results = get_forecast_for_query(queryitems[1:]) + return self.bot.reply(event, results) + else: + # assume they wanted current weather + results = get_conditions_for_query(queryitems) + return self.bot.reply(event, results) + + +plugin = Weather diff --git a/dr_botzo/weather/lib.py b/dr_botzo/weather/lib.py new file mode 100644 index 0000000..859a7f6 --- /dev/null +++ b/dr_botzo/weather/lib.py @@ -0,0 +1,199 @@ +# coding: utf-8 + +from __future__ import unicode_literals + +import json +import logging +import re +import urllib2 + +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(u"calling %s", url) + json_resp = urllib2.urlopen(url) + condition_data = json.load(json_resp) + 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(json.dumps(condition_data, sort_keys=True, indent=4)) + + 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)) + json_resp = urllib2.urlopen(url) + forecast_data = json.load(json_resp) + 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(json.dumps(forecast_data, sort_keys=True, indent=4)) + + 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) diff --git a/dr_botzo/weather/migrations/__init__.py b/dr_botzo/weather/migrations/__init__.py new file mode 100644 index 0000000..e69de29