# coding: utf-8 """ Weather - query weather underground for info Copyright (C) 2010 Brian S. Stephan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import json import MySQLdb as mdb import re import urllib2 from Module import Module class Weather(Module): """Provide weather lookup services to the bot.""" def __init__(self, irc, config): """Set up regexes and wunderground API.""" Module.__init__(self, irc, config) self.weatherre = re.compile("^!weather\s+(.*)$") # get the API key from config api_key = self._get_api_key_from_db() if api_key is None: raise Exception("weather underground API key not found!") self.wu_base = 'http://api.wunderground.com/api/{0:s}/'.format(api_key) def db_init(self): """Initialize the tiny table that stores the API key.""" version = self.db_module_registered(self.__class__.__name__) if version == None: db = self.get_db() try: version = 1 cur = db.cursor(mdb.cursors.DictCursor) cur.execute(''' CREATE TABLE weather_settings ( api_key VARCHAR(256) NOT NULL ) ''') db.commit() self.db_register_module_version(self.__class__.__name__, version) except mdb.Error as e: db.rollback() self.log.error("database error trying to create tables") self.log.exception(e) raise finally: cur.close() def do(self, connection, event, nick, userhost, what, admin_unlocked): """Handle IRC input for a weather queries.""" match = self.weatherre.search(what) if match: # 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 = self.get_conditions_for_query(queryitems[1:]) return self.irc.reply(event, results) elif queryitems[0] == "forecast": # forecast query results = self.get_forecast_for_query(queryitems[1:]) return self.irc.reply(event, results) else: # assume they wanted current weather results = self.get_conditions_for_query(queryitems) return self.irc.reply(event, results) def get_conditions_for_query(self, queryitems): """Make a wunderground conditions call, return as string.""" # recombine the query into a string query = ' '.join(queryitems) query = query.replace(' ', '_') try: url = self.wu_base + ('{0:s}/q/{1:s}.json'.format('conditions', query)) json_resp = urllib2.urlopen(url) condition_data = json.load(json_resp) except IOError as e: self.log.error("error while making conditions query") self.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 self.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 self.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 self.log.error("error or bad query in conditions lookup") self.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 self._prettify_weather_strings(reply.rstrip()) except KeyError as e: self.log.error("error or unexpected results in conditions reply") self.log.exception(e) return "Error parsing results." def get_forecast_for_query(self, queryitems): """Make a wunderground forecast call, return as string.""" # recombine the query into a string query = ' '.join(queryitems) query = query.replace(' ', '_') try: url = self.wu_base + ('{0:s}/q/{1:s}.json'.format('forecast', query)) json_resp = urllib2.urlopen(url) forecast_data = json.load(json_resp) except IOError as e: self.log.error("error while making forecast query") self.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 self.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 self.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 self.log.error("error or bad query in forecast lookup") self.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 self._prettify_weather_strings(reply.rstrip()) except KeyError as e: self.log.error("error or unexpected results in forecast reply") self.log.exception(e) return "Error parsing results." def _get_api_key_from_db(self): """Get the API key string from the database, or None if unset.""" api_key = None db = self.get_db() try: cur = db.cursor(mdb.cursors.DictCursor) query = '''SELECT api_key FROM weather_settings''' cur.execute(query) value = cur.fetchone() if (value != None): api_key = value['api_key'] except mdb.Error as e: self.log.error("database error during api key retrieval") self.log.exception(e) finally: cur.close() return api_key def _prettify_weather_strings(self, 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) # vi:tabstop=4:expandtab:autoindent # kate: indent-mode python;indent-width 4;replace-tabs on;