dr.botzo/ircbot/modules/Weather.py

297 lines
12 KiB
Python
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

# 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 <http://www.gnu.org/licenses/>.
"""
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'\\2', weather_str)
# vi:tabstop=4:expandtab:autoindent
# kate: indent-mode python;indent-width 4;replace-tabs on;