Massive refactor. Moved internal db over to sqlite. MUST READ README IF USING OLDER VERSION.

This commit is contained in:
spline 2013-06-09 16:34:22 -04:00
parent f435bcc004
commit 194ae52481
3 changed files with 356 additions and 334 deletions

105
README.md
View File

@ -2,85 +2,94 @@ Supybot-Weather
=============== ===============
Overview Overview
This is a Supybot plugin for displaying Weather via Weather Underground (http://www.wunderground.com) This is a Supybot plugin for displaying Weather via Weather Underground (http://www.wunderground.com)
They've got a nice JSON api that is free to use when you register and grab an API key. You will need They've got a nice JSON api that is free to use when you register and grab an API key.
an API key to use this plugin. Configure it via:
/msg bot config plugin.Weather.apiKey <apiKey>
I made this plugin because quite a few Weather plugins didn't work well and WunderWeather, which uses I made this plugin because quite a few Weather plugins didn't work well and WunderWeather, which uses
this API, is on their older XML api that they don't have documented anymore and, one would assume, will this API, is on their older XML api that they don't have documented anymore and, one would assume, will
be depreciated at some point. be depreciated at some point.
There are a ton of options to configure. You can look through these via /msg <bot> config search Weather Besides a few of the ideas like having a user database, colorized temp, most of the code is mine.
Many of these are also available via --options when calling the wunderground command.
Instructions
NOTICE: If you were using the older version of this plugin, you _MUST_ delete the older Weather.db
file in the Supybot data directory. Normally, this is at <supybotdir>/data/Weather.db
The internal DB is not compatable and must be deleted before.
First, you will need to register for a free API key. Signup takes less than a minute at:
http://www.wunderground.com/weather/api/
You will need an API key to use this plugin. Configure it via:
/msg <bot> config plugin.Weather.apiKey <apiKey>
Now reload the plugin:
/msg <bot> reload Weather
You can now use the basic functionality by:
/msg <bot> wunderground 10012 (or your zipcode)
I suggest adding an alias to this command to make it easier. I suggest adding an alias to this command to make it easier.
/msg bot Alias add weather wunderground /msg <bot> Alias add weather wunderground
/msg <bot> Alias add w wunderground
Another feature that will make you and your users happy is an internal database that can remember your
location and setting for metric. I've seen this before with another bot and wanted to implement this. Options
There are a ton of options to configure. You can look through these via /msg <bot> config search Weather
Many of these are also available via --help when calling the wunderground command.
Another feature that will make you and your users happy is an internal database that can remember your
location, setting for metric, and color temperature.
Basically, instead of having to type wunderground 10152 (or wherever you are), you can just type in Basically, instead of having to type wunderground 10152 (or wherever you are), you can just type in
wunderground. This can be done via setting a location with the setweather command. wunderground. This can be done via setting a location with the setweather command.
/msg <bot> setweather 10152 /msg <bot> setweather 10152
/msg <bot> setmetric False (to use imperial units) /msg <bot> setmetric False (to use imperial units)
/msg <bot> setcolortemp False (or true)
The bot's db is very simple and only remembers a nick and setting. So, if you change nicks, it will not The bot's db is very simple and only remembers a nick and setting. So, if you change nicks, it will not
remember you unless you set it on this new nick. remember you unless you set it on this new nick.
Use:
/msg <bot> getweather
/msg <bot> getmetric
To check settings here. This is optional but a neat feature. This only works if you don't give it an input.
So, if you /msg bot wunderground --metric, it will display the weather you set in setweather but in --metric.
Options Options
This plugin has a bit of configuration that can be done with it. We'll start with the basics: This plugin has a bit of configuration that can be done with it. We'll start with the basics:
- useImperial: - useImperial:
We display using non-metric units. For the rest of the world who uses them, you may set this We display using non-metric units. For the rest of the world who uses them, you may set this
per channel or in the config via the: per channel or in the config via the:
/msg <bot> config plugins.Weather.useImperial configuration variable (True/False) /msg <bot> config plugins.Weather.useImperial configuration variable (True/False)
You may also use --metric when calling to get metric units. You may also use --metric when calling to get metric units.
- languages: - languages:
By default, it is set to English. Weather Underground has a variety of language support By default, it is set to English. Weather Underground has a variety of language support
documented here: http://api.wunderground.com/weather/api/d/docs?d=language-support documented here: http://api.wunderground.com/weather/api/d/docs?d=language-support
If you do not want to use English, you can set this via one of the codes above: If you do not want to use English, you can set this via one of the codes above:
/msg <bot> config plugins.Weather.lang EN (replace EN with the 2 letter language code) /msg <bot> config plugins.Weather.lang EN (replace EN with the 2 letter language code)
- disableANSI:
By default, ANSI is on. Color/bold on output makes things a bit easier to read.
If you do not want any color or bold in the output for a specific channel, you can:
/msg <bot> channel #channelname plugins.Weather.disableANSI True
or
/msg <bot> config plugins.Weather.disableANSI True
- disableColorTemp - disableColorTemp
On a similar note, I coded a neat "color" to temperature function that will color any temperature On a similar note, I coded a neat "color" to temperature function that will color any temperature
on a basis of what it is (works for metric, too). Think of how temperature maps are done where on a basis of what it is (works for metric, too). Think of how temperature maps are done where
you would see red/orange/yellow if it's "hot", green if "moderate", and blue if its "cold". you would see red/orange/yellow if it's "hot", green if "moderate", and blue if its "cold".
By default, I have this ON. You can turn it off like this: By default, I have this ON. This can also be personalized via /msg <bot> setcolortemp True/False
once a user is in the database. You can turn it off like this:
/msg <bot> config plugins.Weather.disableColorTemp True /msg <bot> config plugins.Weather.disableColorTemp True
Documentation Documentation
Some links: Some links:
- Main documentation: http://www.wunderground.com/weather/api/ # Main documentation: http://www.wunderground.com/weather/api/
# https://github.com/davidwilemski/Weather/blob/master/weather.py # https://github.com/davidwilemski/Weather/blob/master/weather.py
# https://bitbucket.org/rizon/pypsd/src/8f975a375ab4/modules/internets/api/weather.py # https://bitbucket.org/rizon/pypsd/src/8f975a375ab4/modules/internets/api/weather.py
# http://ronie.googlecode.com/svn-history/r283/trunk/weather.wunderground/default.py # http://ronie.googlecode.com/svn-history/r283/trunk/weather.wunderground/default.py
# http://www.wunderground.com/weather/api/

View File

@ -21,8 +21,7 @@ def configure(advanced):
Weather = conf.registerPlugin('Weather') Weather = conf.registerPlugin('Weather')
conf.registerGlobalValue(Weather,'apiKey', registry.String('', ("""Your wunderground.com API key."""), private=True)) conf.registerGlobalValue(Weather,'apiKey', registry.String('', ("""Your wunderground.com API key."""), private=True))
conf.registerChannelValue(Weather,'useImperial', registry.Boolean(True, ("""Use imperial units? Defaults to yes."""))) conf.registerChannelValue(Weather,'useImperial', registry.Boolean(True, ("""Use imperial units? Defaults to yes.""")))
conf.registerChannelValue(Weather,'disableANSI', registry.Boolean(False, """Do not display any ANSI (color/bold) for channel.""")) conf.registerChannelValue(Weather,'disableColoredTemp', registry.Boolean(False, """If True, this will disable coloring temperatures based on values."""))
conf.registerChannelValue(Weather,'disableColoredTemp', registry.Boolean(False, """If disableANSI is True, this will color temperatures based on values."""))
conf.registerChannelValue(Weather,'useWeatherSymbols', registry.Boolean(False, """Use unicode symbols with weather conditions and for wind direction.""")) conf.registerChannelValue(Weather,'useWeatherSymbols', registry.Boolean(False, """Use unicode symbols with weather conditions and for wind direction."""))
conf.registerGlobalValue(Weather,'forecast', registry.Boolean(True, ("""Display forecast in output by default?"""))) conf.registerGlobalValue(Weather,'forecast', registry.Boolean(True, ("""Display forecast in output by default?""")))
conf.registerGlobalValue(Weather,'alerts', registry.Boolean(False, ("""Display alerts by default?"""))) conf.registerGlobalValue(Weather,'alerts', registry.Boolean(False, ("""Display alerts by default?""")))

582
plugin.py
View File

@ -3,19 +3,14 @@
# Copyright (c) 2012-2013, spline # Copyright (c) 2012-2013, spline
# All rights reserved. # All rights reserved.
### ###
# my libs # my libs
import urllib2
import json import json
import re from math import floor # for wind.
from math import floor import sqlite3 # userdb.
from urllib import quote import os
# extra supybot libs # extra supybot libs
import supybot.conf as conf import supybot.conf as conf
import supybot.ircdb as ircdb import supybot.log as log
import supybot.world as world
# supybot libs # supybot libs
import supybot.utils as utils import supybot.utils as utils
from supybot.commands import * from supybot.commands import *
@ -25,77 +20,113 @@ import supybot.callbacks as callbacks
from supybot.i18n import PluginInternationalization, internationalizeDocstring from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('Weather') _ = PluginInternationalization('Weather')
# @internationalizeDocstring
@internationalizeDocstring class WeatherDB():
class WeatherDB(plugins.ChannelUserDB): """WeatherDB class to store our users and their settings."""
"""WeatherDB class to store our users locations and metric."""
def __init__(self, *args, **kwargs):
plugins.ChannelUserDB.__init__(self, *args, **kwargs)
def serialize(self, v): def __init__(self):
return list(v) self.filename = conf.supybot.directories.data.dirize("Weather.db")
self.log = log.getPluginLogger('Weather')
self._conn = sqlite3.connect(self.filename, check_same_thread=False)
self._conn.text_factory = str
self.makeDb()
def deserialize(self, channel, id, L): def makeDb(self):
(id, metric) = L """Create our DB."""
return (id, metric)
def getId(self, nick): self.log.info("WeatherDB: Checking/Creating DB.")
return self['x', nick.lower()][0] with self._conn as conn:
cursor = conn.cursor()
cursor.execute("""CREATE TABLE IF NOT EXISTS users (
nick TEXT PRIMARY KEY,
location TEXT NOT NULL,
metric INTEGER DEFAULT 0,
colortemp INTEGER DEFAULT 1)""")
self._conn.commit()
def getMetric(self, nick): def setweather(self, username, location):
return self['x', nick.lower()][1] """Stores or update a user's location. Adds user if not found."""
with self._conn as conn:
cursor = conn.cursor()
if self.getuser(username): # username exists.
cursor.execute("""UPDATE users SET location=? WHERE nick=?""", (location, username,))
else: # username does not exist so add it in.
cursor.execute("""INSERT OR REPLACE INTO users (nick, location) VALUES (?,?)""", (username, location,))
self._conn.commit() # commit.
def setId(self, nick, id): def setmetric(self, username, metric):
try: """Sets a user's metric value."""
metric = self['x', nick.lower()][1] with self._conn as conn:
except KeyError: cursor = conn.cursor()
metric = 'False' cursor.execute("""UPDATE users SET metric=? WHERE nick=?""", (metric, username,))
self['x', nick.lower()] = (id, metric,) self._conn.commit()
def setcolortemp(self, username, colortemp):
"""Sets a user's colortemp value."""
with self._conn as conn:
cursor = conn.cursor()
cursor.execute("""UPDATE users SET colortemp=? WHERE nick=?""", (colortemp, username))
self._conn.commit()
def getweather(self, user):
"""Return a dict of user's settings."""
self._conn.row_factory = sqlite3.Row
with self._conn as conn:
cursor = conn.cursor()
cursor.execute("""SELECT * from users where nick=?""", (user,))
row = cursor.fetchone()
if not row: # user does not exist.
return None
else: # user exists.
return row
def getuser(self, user):
"""Returns a boolean if a user exists."""
with self._conn as conn:
cursor = conn.cursor()
cursor.execute("""SELECT location from users where nick=?""", (user,))
row = cursor.fetchone()
if row:
return True
else:
return False
def setMetric(self, nick, metric):
try:
id = self['x', nick.lower()][0]
except:
id = '10121'
self['x', nick.lower()] = (id, metric,)
class Weather(callbacks.Plugin): class Weather(callbacks.Plugin):
"""Add the help for "@plugin help Weather" here """Add the help for "@plugin help Weather" here
This should describe *how* to use this plugin.""" This should describe *how* to use this plugin."""
threaded = True threaded = True
# BASICS/WeatherDB
def __init__(self, irc): def __init__(self, irc):
self.__parent = super(Weather, self) self.__parent = super(Weather, self)
self.__parent.__init__(irc) self.__parent.__init__(irc)
self.db = WeatherDB(conf.supybot.directories.data.dirize("Weather.db"))
self.APIKEY = self.registryValue('apiKey') self.APIKEY = self.registryValue('apiKey')
world.flushers.append(self.db.flush) self.db = WeatherDB()
def die(self): def die(self):
if self.db.flush in world.flushers:
world.flushers.remove(self.db.flush)
self.db.close()
self.__parent.die() self.__parent.die()
# COLORING ##############
# FORMATTING #
##############
def _bold(self, string): def _bold(self, string):
return ircutils.bold(string) return ircutils.bold(string)
def _bu(self, string): def _bu(self, string):
return ircutils.underline(ircutils.bold(string)) return ircutils.underline(ircutils.bold(string))
def _blue(self, string):
return ircutils.mircColor(string, 'blue')
def _red(self, string):
return ircutils.mircColor(string, 'red')
def _strip(self, string): def _strip(self, string):
return ircutils.stripFormatting(string) return ircutils.stripFormatting(string)
# WEATHER SYMBOLS ############################
# INTERNAL WEATHER HELPERS #
############################
def _weatherSymbol(self, code): def _weatherSymbol(self, code):
"""Return a unicode symbol based on weather status."""
table = {'partlycloudy':'~☁', table = {'partlycloudy':'~☁',
'cloudy':'', 'cloudy':'',
'tstorms':'', 'tstorms':'',
@ -114,15 +145,17 @@ class Weather(callbacks.Plugin):
'chancesleet':'?❄', 'chancesleet':'?❄',
'chancesnow':'?❄', 'chancesnow':'?❄',
'chancetstorms':'?☔', 'chancetstorms':'?☔',
'unknown':''} 'unknown':'unknown'}
# return symbol from table.
try: try:
return table[code] return table[code]
except KeyError: except KeyError:
return None return None
# MOON PHASE
def _moonphase(self, phase): def _moonphase(self, phase):
"""Returns a moon phase based on the %.""" """Returns a moon phase based on the %."""
# depending on the phase float, we have an ascii picture+text to represent it.
if phase < 0.05: if phase < 0.05:
symbol = "[ ( ) ] (fullmoon)" symbol = "[ ( ) ] (fullmoon)"
elif phase < 0.20: elif phase < 0.20:
@ -139,16 +172,18 @@ class Weather(callbacks.Plugin):
symbol = "[ D ] (half moon)" symbol = "[ D ] (half moon)"
else: else:
symbol = "[ D ] (waxing moon)" symbol = "[ D ] (waxing moon)"
# return.
return symbol return symbol
# COLOR TEMPERATURE
def _temp(self, x): def _temp(self, x):
"""Returns a colored string based on the temperature.""" """Returns a colored string based on the temperature."""
if x.endswith('C'):
x = float(str(x).replace('C','')) * 9 / 5 + 32 # first, convert into F so we only have one table.
if x.endswith('C'): # c.
x = float(str(x).replace('C', '')) * 9 / 5 + 32 # remove C + math into float(F).
unit = "C" unit = "C"
else: else: # f.
x = float(str(x).replace('F','')) x = float(str(x).replace('F', '')) # remove F. str->float.
unit = "F" unit = "F"
# determine color. # determine color.
if x < 10.0: if x < 10.0:
@ -170,122 +205,113 @@ class Weather(callbacks.Plugin):
else: else:
color = 'light grey' color = 'light grey'
# return. # return.
if unit == "F": if unit == "F": # no need to convert back.
return ircutils.mircColor(("{0:.0f}F".format(x)),color) return ircutils.mircColor(("{0:.0f}F".format(x)), color)
else: else: # temp is in F and we need to go back to C.
return ircutils.mircColor(("{0:.0f}C".format((x - 32) * 5 / 9)),color) return ircutils.mircColor(("{0:.0f}C".format((x - 32) * 5 / 9)),color)
# DEGREES TO DIRECTION (wind)
def _wind(self, angle, useSymbols=False): def _wind(self, angle, useSymbols=False):
if not useSymbols: """Converts degrees to direction for wind. Can optionally return a symbol."""
direction_names = ["N","NE","E","SE","S","SW","W","NW"]
else: if not useSymbols: # ordinal names.
direction_names = ['','','','','','','',''] direction_names = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
else: # symbols.
direction_names = ['', '', '', '', '', '', '', '']
# do math below to figure the angle->direction out.
directions_num = len(direction_names) directions_num = len(direction_names)
directions_step = 360./directions_num directions_step = 360./directions_num
index = int(round((angle/360. - floor(angle/360.)*360.)/directions_step)) index = int(round((angle/360. - floor(angle/360.)*360.)/directions_step))
index %= directions_num index %= directions_num
# return.
return direction_names[index] return direction_names[index]
# PUBLIC FUNCTIONS TO WORK WITH WEATHERDB. ##############################################
def weatherusers(self, irc, msg, args): # PUBLIC FUNCTIONS TO WORK WITH THE DATABASE #
""" ##############################################
Returns the amount of users we know about.
"""
output = str(len(self.db.keys()))
irc.reply("I know about {0} users in my weather database.".format(str(output)))
weatherusers = wrap(weatherusers)
def setmetric(self, irc, msg, args, optboolean): def setcolortemp(self, irc, msg, args, opttemp):
"""<True|False>
Sets the user's colortemp setting to True or False.
If True, will color temperature. If False, will not color.
"""
if opttemp: # handle opttemp
metric = 1
else: # False.
metric = 0
# check user first.
if self.db.getuser(msg.nick.lower()): # user exists
# perform op.
self.db.setcolortemp(msg.nick.lower(), metric)
irc.reply("I have changed {0}'s colortemp setting to {1}".format(msg.nick, metric))
else: # user is NOT In the database.
irc.reply("ERROR: You're not in the database. You must setweather first.")
setcolortemp = wrap(setcolortemp, [('boolean')])
def setmetric(self, irc, msg, args, optmetric):
"""<True|False> """<True|False>
Sets the user's use metric setting to True or False. Sets the user's use metric setting to True or False.
If True, will use netric. If False, will use imperial. If True, will use netric. If False, will use imperial.
""" """
# first, title case and cleanup as helper. Still must be True or False.
optboolean = optboolean.title().strip() # partial helpers.
if optboolean != "True" and optboolean != "False":
irc.reply("metric setting must be True or False")
return
# now, test if we have a username. setmetric for an unknown username = error
try:
self.db.getId(msg.nick)
except KeyError:
irc.reply("I have no user in the DB named {0}. Try setweather first.".format(msg.nick))
return
# now change it.
self.db.setMetric(msg.nick, optboolean)
irc.reply("I have changed {0}'s metric setting to {1}".format(msg.nick, optboolean))
setmetric = wrap(setmetric, [('somethingWithoutSpaces')])
def setweather(self, irc, msg, args, optid): if optmetric: # handle opttemp
metric = 1
else: # False.
metric = 0
# check user first.
if self.db.getuser(msg.nick.lower()): # user exists
# perform op.
self.db.setmetric(msg.nick.lower(), metric)
irc.reply("I have changed {0}'s metric setting to {1}".format(msg.nick, metric))
else: # user is NOT In the database.
irc.reply("ERROR: You're not in the database. You must setweather first.")
setmetric = wrap(setmetric, [('boolean')])
def setweather(self, irc, msg, args, optlocation):
"""<location code> """<location code>
Set's weather location code for your nick as <location code>. Set's weather location code for your nick as location code.
Use your zip/postal code to keep it simple. Ex: setweather 03062
Use your zip/postal code to keep it simple.
Ex: setweather 10012
""" """
# set the weather id based on nick.
optid = optid.replace(' ','') # set the weather id based on nick. This will update or set.
self.db.setId(msg.nick, optid) self.db.setweather(msg.nick.lower(), optlocation)
irc.reply("I have changed {0}'s weather ID to {1}".format(msg.nick, optid)) irc.reply("I have changed {0}'s weather ID to {1}".format(msg.nick.lower(), optlocation))
setweather = wrap(setweather, [('text')]) setweather = wrap(setweather, [('text')])
def getmetric(self, irc, msg, args, optnick):
"""[nick]
Get the metric setting of your or [nick].
"""
# allows us to specify the nick.
if not optnick:
optnick = msg.nick
# now try to fetch the metric. Tests if we have a username.
try:
irc.reply("The metric setting for {0} is {1}".format(optnick, self.db.getMetric(optnick)))
except KeyError:
irc.reply('I have no weather metric setting for {0}'.format(optnick))
getmetric = wrap(getmetric, [optional('somethingWithoutSpaces')])
def getweather(self, irc, msg, args, optnick):
"""[nick]
Get the weather ID of your or [nick].
"""
# allow us to specify the nick if we don't have it.
if not optnick:
optnick = msg.nick
# now try and fetch the metric setting. error if it's broken.
try:
irc.reply("The weather ID for {0} is {1}".format(optnick, self.db.getId(optnick)))
except KeyError:
irc.reply('I have no weather ID for %s.' % optnick)
getweather = wrap(getweather, [optional('somethingWithoutSpaces')])
# CHECK FOR API KEY. (NOT PUBLIC)
def keycheck(self, irc):
"""Check and make sure we have an API key."""
if len(self.APIKEY) < 1 or not self.APIKEY or self.APIKEY == "Not set":
irc.reply("ERROR: Need a Wunderground API key. Set config plugins.Weather.apiKey.")
return False
else:
return True
#################### ####################
# PUBLIC FUNCTIONS # # PUBLIC FUNCTIONS #
#################### ####################
def wunderground(self, irc, msg, args, optlist, optinput): def wunderground(self, irc, msg, args, optlist, optinput):
"""[--options] [location] """[--options] <location>
Fetch weather and forcast information for <location>
Location must be one of: US state/city (CA/San_Francisco), zipcode, country/city (Australia/Sydney), airport code (KJFK) Location must be one of: US state/city (CA/San_Francisco), zipcode, country/city (Australia/Sydney), airport code (KJFK)
For options: Use --help to list all options.
Ex: 10021 or Sydney, Australia or KJFK
""" """
# first, check if we have an API key. Useless w/o this. # first, check if we have an API key. Useless w/o this.
if not self.keycheck(irc): if len(self.APIKEY) < 1 or not self.APIKEY or self.APIKEY == "Not set":
return False irc.reply("ERROR: Need a Wunderground API key. Set config plugins.Weather.apiKey and reload Weather.")
return
# urlargs will be used to build the url to query the API. # urlargs will be used to build the url to query the API.
urlArgs = {'features':['conditions','forecast'], urlArgs = {'features':['conditions', 'forecast'],
'lang':self.registryValue('lang'), 'lang':self.registryValue('lang'),
'bestfct':'1', 'bestfct':'1',
'pws':'0' 'pws':'0' }
}
# now, start our dict for output formatting. # now, start our dict for output formatting.
args = {'imperial':self.registryValue('useImperial', msg.args[0]), args = {'imperial':self.registryValue('useImperial', msg.args[0]),
'nocolortemp':self.registryValue('disableColoredTemp', msg.args[0]),
'alerts':self.registryValue('alerts'), 'alerts':self.registryValue('alerts'),
'almanac':self.registryValue('almanac'), 'almanac':self.registryValue('almanac'),
'astronomy':self.registryValue('astronomy'), 'astronomy':self.registryValue('astronomy'),
@ -296,23 +322,7 @@ class Weather(callbacks.Plugin):
'strip':False, 'strip':False,
'uv':False, 'uv':False,
'visibility':False, 'visibility':False,
'dewpoint':False 'dewpoint':False }
}
# now check if we have a location. if no location, use the userdb. also set for metric variable.
# autoip.json?geo_ip=38.102.136.138
if not optinput:
try:
optinput = self.db.getId(msg.nick)
optmetric = self.db.getMetric(msg.nick) # set our imperial units here.
if optmetric == "True":
args['imperial'] = False
else:
args['imperial'] = True
except KeyError:
irc.reply("I did not find a preset location for you. Set via: setweather location or specify a location")
return
# handle optlist (getopts). this will manipulate output via args dict. # handle optlist (getopts). this will manipulate output via args dict.
if optlist: if optlist:
for (key, value) in optlist: for (key, value) in optlist:
@ -336,9 +346,33 @@ class Weather(callbacks.Plugin):
args['dewpoint'] = True args['dewpoint'] = True
if key == 'astronomy': if key == 'astronomy':
args['astronomy'] = True args['astronomy'] = True
if key == 'nocolortemp':
args['nocolortemp'] = True
if key == 'help': # make shift help because the docstring is overloaded above.
irc.reply("Options: --metric --alerts --forecast --almanac --pressure --wind --uv --visibility --dewpoint --astronomy --nocolortemp")
irc.reply("WeatherDB options: setweather <location> (set user's location). setmetric True/False (set metric option) setcolortemp True/False (display color temp?")
return
# now check if we have a location. if no location, use the WeatherDB.
if not optinput: # no location on input.
userloc = self.db.getweather(msg.nick.lower()) # check the db.
if userloc: # found a user so we change args w/info.
optinput = userloc['location'] # grab location.
# setmetric.
if userloc['metric'] == 0: # 0 = False for metric.
args['imperial'] = True # so we make sure we're using imperial.
elif userloc['metric'] == 1: # do the inverse.
args['imperial'] = False
# setcolortemp.
if userloc['colortemp'] == 0: # 0 = False for colortemp.
args['nocolortemp'] = True # disable
elif userloc['colortemp'] == 1: # do the inverse.
args['nocolortemp'] = False # show color temp.
else: # no user NOR optinput found. error msg.
irc.reply("ERROR: I did not find a preset location for you. Set via setweather <location>")
return
# build url now. first, apikey. then, iterate over urlArgs and insert. # build url now. first, apikey. then, iterate over urlArgs and insert.
# urlArgs['features'] also manipulated via what's in args.
url = 'http://api.wunderground.com/api/%s/' % (self.APIKEY) # first part of url, w/APIKEY url = 'http://api.wunderground.com/api/%s/' % (self.APIKEY) # first part of url, w/APIKEY
# now we need to set certain things for urlArgs based on args. # now we need to set certain things for urlArgs based on args.
for check in ['alerts','almanac','astronomy']: for check in ['alerts','almanac','astronomy']:
@ -351,42 +385,36 @@ class Weather(callbacks.Plugin):
if key == "lang" or key == "bestfct" or key == "pws": # rest added with key:value if key == "lang" or key == "bestfct" or key == "pws": # rest added with key:value
url += "{0}:{1}/".format(key, value) url += "{0}:{1}/".format(key, value)
# finally, attach the q/input. url is now done. # finally, attach the q/input. url is now done.
url += 'q/%s.json' % quote(optinput) url += 'q/%s.json' % utils.web.urlquote(optinput)
#self.log.info(url) # try and query url.
# try and query.
try: try:
request = urllib2.Request(url) page = utils.web.getUrl(url)
u = urllib2.urlopen(request) except utils.web.Error as e:
except Exception as e: self.log.error("ERROR: Trying to open {0} message: {1}".format(url, e))
self.log.info("Error loading {0} message {1}".format(url, e)) irc.reply("ERROR: Failed to load Weather Underground API: {0}".format(e))
irc.reply("Failed to load wunderground API: %s" % e)
return return
# process the json, check (in orders) for errors, multiple results, and one last # process json.
# sanity check. then we can process it. data = json.loads(page.decode('utf-8'))
data = json.load(u)
# check if we got errors and return. # now, a series of sanity checks before we process.
if 'error' in data['response']: if 'error' in data['response']: # check if there are errors.
errortype = data['response']['error']['type'] errortype = data['response']['error']['type'] # type. description is below.
errordesc = data['response']['error'].get('description', 'no description') errordesc = data['response']['error'].get('description', 'no description')
irc.reply("{0} I got an error searching for {1}. ({2}: {3})".format(self._red("ERROR:"), optinput, errortype, errordesc)) irc.reply("ERROR: I got an error searching for {0}. ({1}: {2})".format(optinput, errortype, errordesc))
return return
# if there is more than one city matching. # if there is more than one city matching.
if 'results' in data['response']: if 'results' in data['response']: # results only comes when location matches more than one.
output = [item['city'] + ", " + item['state'] + " (" + item['country_name'] + ")" for item in data['response']['results']] output = [i['city'] + ", " + i['state'] + " (" + i['country_name'] + ")" for i in data['response']['results']]
irc.reply("More than 1 city matched your query, try being more specific: {0}".format(" | ".join(output))) irc.reply("ERROR: More than 1 city matched your query, try being more specific: {0}".format(" | ".join(output)))
return return
# last sanity check # last sanity check
if not data.has_key('current_observation'): if not data.has_key('current_observation'):
irc.reply("{0} something went horribly wrong looking up weather for {1}. Contact the plugin owner.".format(self._red("ERROR:"), optinput)) irc.reply("ERROR: something went horribly wrong looking up weather for {0}. Contact the plugin owner.".format(optinput))
return return
# done with error checking. # no errors so we start the main part of processing.
# now, put everything into outdata dict for output later.
outdata = {} outdata = {}
outdata['weather'] = data['current_observation']['weather'] outdata['weather'] = data['current_observation']['weather']
outdata['location'] = data['current_observation']['display_location']['full'] outdata['location'] = data['current_observation']['display_location']['full']
@ -394,28 +422,26 @@ class Weather(callbacks.Plugin):
outdata['uv'] = data['current_observation']['UV'] outdata['uv'] = data['current_observation']['UV']
# handle wind. check if there is none first. # handle wind. check if there is none first.
if args['imperial']: if data['current_observation']['wind_mph'] < 1: # no wind.
if data['current_observation']['wind_mph'] < 1: # no wind. outdata['wind'] = "None"
outdata['wind'] = "None" else: # we do have wind. process differently.
else: if args['imperial']: # imperial units for wind.
outdata['wind'] = "{0}@{1}mph".format(self._wind(data['current_observation']['wind_degrees']),data['current_observation']['wind_mph']) outdata['wind'] = "{0}@{1}mph".format(self._wind(data['current_observation']['wind_degrees']), data['current_observation']['wind_mph'])
if data['current_observation']['wind_gust_mph'] > 0: if data['current_observation']['wind_gust_mph'] > 0: # gusts?
outdata['wind'] += " ({0}mph gusts)".format(data['current_observation']['wind_gust_mph']) outdata['wind'] += " ({0}mph gusts)".format(data['current_observation']['wind_gust_mph'])
else: else: # handle metric units for wind.
if data['current_observation']['wind_kph'] < 1: # no wind.
outdata['wind'] = "None"
else:
outdata['wind'] = "{0}@{1}kph".format(self._wind(data['current_observation']['wind_degrees']),data['current_observation']['wind_kph']) outdata['wind'] = "{0}@{1}kph".format(self._wind(data['current_observation']['wind_degrees']),data['current_observation']['wind_kph'])
if data['current_observation']['wind_gust_kph'] > 0: if data['current_observation']['wind_gust_kph'] > 0: # gusts?
outdata['wind'] += " ({0}kph gusts)".format(data['current_observation']['wind_gust_kph']) outdata['wind'] += " ({0}kph gusts)".format(data['current_observation']['wind_gust_kph'])
# handle the time. concept/method from WunderWeather plugin. # handle the time. concept/method from WunderWeather plugin.
observationTime = data['current_observation'].get('observation_epoch', None) observationTime = data['current_observation'].get('observation_epoch')
localTime = data['current_observation'].get('local_epoch', None) localTime = data['current_observation'].get('local_epoch')
if not observationTime or not localTime: # if we don't have the epoches from above, default to obs_time # if we don't have the epoches from above, default to obs_time
if not observationTime or not localTime:
outdata['observation'] = data.get('observation_time', 'unknown').lstrip('Last Updated on ') outdata['observation'] = data.get('observation_time', 'unknown').lstrip('Last Updated on ')
else: # format for relative time. else: # we do have so format for relative time.
s = int(localTime) - int(observationTime) s = int(localTime) - int(observationTime) # format into seconds.
if s <= 1: if s <= 1:
outdata['observation'] = 'just now' outdata['observation'] = 'just now'
elif s < 60: elif s < 60:
@ -429,8 +455,8 @@ class Weather(callbacks.Plugin):
else: else:
outdata['observation'] = '{0}hrs ago'.format(s/3600) outdata['observation'] = '{0}hrs ago'.format(s/3600)
# all conditionals for imperial/metric # handle basics like temp/pressure/dewpoint.
if args['imperial']: if args['imperial']: # assigns the symbol based on metric.
outdata['temp'] = str(data['current_observation']['temp_f']) + 'F' outdata['temp'] = str(data['current_observation']['temp_f']) + 'F'
outdata['pressure'] = data['current_observation']['pressure_in'] + 'in' outdata['pressure'] = data['current_observation']['pressure_in'] + 'in'
outdata['dewpoint'] = str(data['current_observation']['dewpoint_f']) + 'F' outdata['dewpoint'] = str(data['current_observation']['dewpoint_f']) + 'F'
@ -438,7 +464,7 @@ class Weather(callbacks.Plugin):
outdata['windchill'] = str(data['current_observation']['windchill_f']) + 'F' outdata['windchill'] = str(data['current_observation']['windchill_f']) + 'F'
outdata['feelslike'] = str(data['current_observation']['feelslike_f']) + 'F' outdata['feelslike'] = str(data['current_observation']['feelslike_f']) + 'F'
outdata['visibility'] = str(data['current_observation']['visibility_mi']) + 'mi' outdata['visibility'] = str(data['current_observation']['visibility_mi']) + 'mi'
else: else: # metric.
outdata['temp'] = str(data['current_observation']['temp_c']) + 'C' outdata['temp'] = str(data['current_observation']['temp_c']) + 'C'
outdata['pressure'] = data['current_observation']['pressure_mb'] + 'mb' outdata['pressure'] = data['current_observation']['pressure_mb'] + 'mb'
outdata['dewpoint'] = str(data['current_observation']['dewpoint_c']) + 'C' outdata['dewpoint'] = str(data['current_observation']['dewpoint_c']) + 'C'
@ -447,31 +473,30 @@ class Weather(callbacks.Plugin):
outdata['feelslike'] = str(data['current_observation']['feelslike_c']) + 'C' outdata['feelslike'] = str(data['current_observation']['feelslike_c']) + 'C'
outdata['visibility'] = str(data['current_observation']['visibility_km']) + 'km' outdata['visibility'] = str(data['current_observation']['visibility_km']) + 'km'
# handle forecast data part. output will be below. # handle forecast data part. output will be below. (not --forecast)
# this is not the --forecast part. forecastdata = {} # key = int(day), value = forecast dict.
forecastdata = {}
for forecastday in data['forecast']['txt_forecast']['forecastday']: for forecastday in data['forecast']['txt_forecast']['forecastday']:
tmpdict = {} tmpdict = {}
tmpdict['day'] = forecastday['title'] tmpdict['day'] = forecastday['title']
tmpdict['symbol'] = self._weatherSymbol(forecastday['icon']) tmpdict['symbol'] = self._weatherSymbol(forecastday['icon'])
if args['imperial']: if args['imperial']: # imperial.
tmpdict['text'] = forecastday['fcttext'] tmpdict['text'] = forecastday['fcttext']
else: else: # metric.
tmpdict['text'] = forecastday['fcttext_metric'] tmpdict['text'] = forecastday['fcttext_metric']
forecastdata[int(forecastday['period'])] = tmpdict forecastdata[int(forecastday['period'])] = tmpdict
# now this is the --forecast part. # now this is the --forecast part.
if args['forecast']: if args['forecast']: # only if we get this in getopts.
fullforecastdata = {} fullforecastdata = {} # key = day (int), value = dict of forecast data.
for forecastday in data['forecast']['simpleforecast']['forecastday']: for forecastday in data['forecast']['simpleforecast']['forecastday']:
tmpdict = {} tmpdict = {}
tmpdict['day'] = forecastday['date']['weekday_short'] tmpdict['day'] = forecastday['date']['weekday_short']
tmpdict['symbol'] = self._weatherSymbol(forecastday['icon']) tmpdict['symbol'] = self._weatherSymbol(forecastday['icon'])
tmpdict['text'] = forecastday['conditions'] tmpdict['text'] = forecastday['conditions']
if args['imperial']: if args['imperial']: # imperial.
tmpdict['high'] = forecastday['high']['fahrenheit'] + "F" tmpdict['high'] = forecastday['high']['fahrenheit'] + "F"
tmpdict['low'] = forecastday['low']['fahrenheit'] + "F" tmpdict['low'] = forecastday['low']['fahrenheit'] + "F"
else: else: # metric.
tmpdict['high'] = forecastday['high']['celsius'] + "C" tmpdict['high'] = forecastday['high']['celsius'] + "C"
tmpdict['low'] = forecastday['low']['celsius'] + "C" tmpdict['low'] = forecastday['low']['celsius'] + "C"
fullforecastdata[int(forecastday['period'])] = tmpdict fullforecastdata[int(forecastday['period'])] = tmpdict
@ -480,12 +505,12 @@ class Weather(callbacks.Plugin):
if args['almanac']: if args['almanac']:
outdata['highyear'] = data['almanac']['temp_high']['recordyear'] outdata['highyear'] = data['almanac']['temp_high']['recordyear']
outdata['lowyear'] = data['almanac']['temp_low']['recordyear'] outdata['lowyear'] = data['almanac']['temp_low']['recordyear']
if args['imperial']: if args['imperial']: # imperial.
outdata['highnormal'] = data['almanac']['temp_high']['normal']['F'] + "F" outdata['highnormal'] = data['almanac']['temp_high']['normal']['F'] + "F"
outdata['highrecord'] = data['almanac']['temp_high']['record']['F'] + "F" outdata['highrecord'] = data['almanac']['temp_high']['record']['F'] + "F"
outdata['lownormal'] = data['almanac']['temp_low']['normal']['F'] + "F" outdata['lownormal'] = data['almanac']['temp_low']['normal']['F'] + "F"
outdata['lowrecord'] = data['almanac']['temp_low']['record']['F'] + "F" outdata['lowrecord'] = data['almanac']['temp_low']['record']['F'] + "F"
else: else: # metric.
outdata['highnormal'] = data['almanac']['temp_high']['normal']['C'] + "C" outdata['highnormal'] = data['almanac']['temp_high']['normal']['C'] + "C"
outdata['highrecord'] = data['almanac']['temp_high']['record']['C'] + "C" outdata['highrecord'] = data['almanac']['temp_high']['record']['C'] + "C"
outdata['lownormal'] = data['almanac']['temp_low']['normal']['C'] + "C" outdata['lownormal'] = data['almanac']['temp_low']['normal']['C'] + "C"
@ -499,98 +524,85 @@ class Weather(callbacks.Plugin):
sunrisem = int(data['moon_phase']['sunrise']['minute']) sunrisem = int(data['moon_phase']['sunrise']['minute'])
sunseth = int(data['moon_phase']['sunset']['hour']) sunseth = int(data['moon_phase']['sunset']['hour'])
sunsetm = int(data['moon_phase']['sunset']['minute']) sunsetm = int(data['moon_phase']['sunset']['minute'])
outdata['sunrise'] = "{0}:{1}".format(sunriseh,sunrisem) outdata['sunrise'] = "{0}:{1}".format(sunriseh, sunrisem) # construct sunrise.
outdata['sunset'] = "{0}:{1}".format(sunseth,sunsetm) outdata['sunset'] = "{0}:{1}".format(sunseth, sunsetm) # construct sunset. calc "time of day" below.
outdata['lengthofday'] = "%dh%dm" % divmod((((sunseth-sunriseh)+float((sunsetm-sunrisem)/60.0))*60),60) outdata['lengthofday'] = "%dh%dm" % divmod((((sunseth-sunriseh)+float((sunsetm-sunrisem)/60.0))*60),60)
# handle alerts # handle alerts
if args['alerts']: if args['alerts']: # only look for alerts if there.
if data['alerts']: if data['alerts']: # alerts is a list. it can also be empty.
outdata['alerts'] = data['alerts'][:300] # alert limit to 300. outdata['alerts'] = data['alerts'][:300] # limit chars to 300.
else: else: # no alerts found (empty).
outdata['alerts'] = "No alerts." outdata['alerts'] = "No alerts."
# OUTPUT # OUTPUT.
# now, build output object with what to output. ° u" \u00B0C" # we go step-by-step to build the proper string. ° u" \u00B0C"
if self.registryValue('disableColoredTemp'): output = "Weather for {0} :: {1}".format(self._bold(outdata['location'].encode('utf-8')), outdata['weather'].encode('utf-8'))
output = "Weather for {0} :: {1} ({2})".format(self._bold(outdata['location']),\ if args['nocolortemp']: # don't color temp.
outdata['weather'],outdata['temp']) output += " {0}".format(outdata['temp'])
else: else: # colored temperature.
output = "Weather for {0} :: {1} ({2})".format(self._bold(outdata['location']),\ output += " {0}".format(self._temp(outdata['temp']))
outdata['weather'],self._temp(outdata['temp']))
# windchill/heatindex are conditional on season but test with startswith to see what to include # windchill/heatindex are conditional on season but test with startswith to see what to include
if not outdata['windchill'].startswith("NA"): if not outdata['windchill'].startswith("NA"): # windchill.
if self.registryValue('disableColoredTemp'): if args['nocolortemp']: # don't color windchill.
output += " | {0} {1}".format(self._bold('Wind Chill:'), outdata['windchill']) output += " | {0} {1}".format(self._bold('Wind Chill:'), outdata['windchill'])
else: else: # color wind chill.
output += " | {0} {1}".format(self._bold('Wind Chill:'), self._temp(outdata['windchill'])) output += " | {0} {1}".format(self._bold('Wind Chill:'), self._temp(outdata['windchill']))
if not outdata['heatindex'].startswith("NA"): if not outdata['heatindex'].startswith("NA"): # heatindex.
if self.registryValue('disableColoredTemp'): if args['nocolortemp']: # don't color heatindex.
output += " | {0} {1}".format(self._bold('Heat Index:'), outdata['heatindex']) output += " | {0} {1}".format(self._bold('Heat Index:'), outdata['heatindex'])
else: else: # color heat index.
output += " | {0} {1}".format(self._bold('Heat Index:'), self._temp(outdata['heatindex'])) output += " | {0} {1}".format(self._bold('Heat Index:'), self._temp(outdata['heatindex']))
# now get into the args dict for what to include (extras) # now get into the args dict for what to include (extras)
for (k,v) in args.items(): for (k, v) in args.items():
if k in ['wind','visibility','uv','pressure','dewpoint']: # if key is in extras if k in ['wind', 'visibility', 'uv', 'pressure', 'dewpoint']: # if key is in extras
if v: # if that key's value is True if v: # if that key's value is True, we add it.
output += " | {0}: {1}".format(self._bold(k.title()), outdata[k]) output += " | {0}: {1}".format(self._bold(k.title()), outdata[k].encode('utf-8'))
# add in the first forecast item in conditions + updated time. # add in the first two forecast item in conditions + updated time.
output += " | {0}: {1} {2}: {3}".format(self._bold(forecastdata[0]['day']),\ output += " | {0}: {1}".format(self._bold(forecastdata[0]['day'].encode('utf-8')), forecastdata[0]['text'].encode('utf-8'))
forecastdata[0]['text'],self._bold(forecastdata[1]['day']),forecastdata[1]['text']) output += " {0}: {1}".format(self._bold(forecastdata[1]['day'].encode('utf-8')), forecastdata[1]['text'].encode('utf-8'))
# show Updated? # show Updated?
if args['updated']: if args['updated']:
output += " | {0} {1}".format(self._bold('Updated:'), outdata['observation']) output += " | {0} {1}".format(self._bold('Updated:'), outdata['observation'].encode('utf-8'))
# output. # finally, output the basic weather.
if self.registryValue('disableANSI', msg.args[0]): irc.reply(output)
irc.reply(self._strip(output))
else:
irc.reply(output)
# next, for outputting, handle the extras like alerts, almanac, etc. # next, for outputting, handle the extras like alerts, almanac, astronomy, forecast.
if args['alerts']: if args['alerts']: # if --alerts issued.
output = "{0} :: {1}".format(self._bu("Alerts:"),outdata['alerts']) irc.reply("{0} :: {1}".format(self._bu("Alerts:"), outdata['alerts'].encode('utf-8')))
if self.registryValue('disableANSI', msg.args[0]): # handle almanac if --almanac is given.
irc.reply(self._strip(output))
else:
irc.reply(output)
# handle almanac
if args['almanac']: if args['almanac']:
if self.registryValue('disableColoredTemp'): if args['nocolortemp']: # disable colored temp?
output = "{0} :: Normal High: {1} (Record: {2} in {3}) | Normal Low: {4} (Record: {5} in {6})".format(\ output = "{0} :: Normal High: {1} (Record: {2} in {3}) | Normal Low: {4} (Record: {5} in {6})".format(\
self._bu('Almanac:'),outdata['highnormal'],outdata['highrecord'],\ self._bu('Almanac:'), outdata['highnormal'], outdata['highrecord'], outdata['highyear'],\
outdata['highyear'],outdata['lownormal'],outdata['lowrecord'],outdata['lowyear']) outdata['lownormal'], outdata['lowrecord'], outdata['lowyear'])
else: else: # colored temp.
output = "{0} :: Normal High: {1} (Record: {2} in {3}) | Normal Low: {4} (Record: {5} in {6})".format(\ output = "{0} :: Normal High: {1} (Record: {2} in {3}) | Normal Low: {4} (Record: {5} in {6})".format(\
self._bu('Almanac:'),self._temp(outdata['highnormal']),self._temp(outdata['highrecord']),\ self._bu('Almanac:'), self._temp(outdata['highnormal']), self._temp(outdata['highrecord']),\
outdata['highyear'],self._temp(outdata['lownormal']),self._temp(outdata['lowrecord']),outdata['lowyear']) outdata['highyear'], self._temp(outdata['lownormal']), self._temp(outdata['lowrecord']), outdata['lowyear'])
if self.registryValue('disableANSI', msg.args[0]): # now output to irc.
irc.reply(self._strip(output)) irc.reply(output)
else: # handle astronomy if --astronomy is given.
irc.reply(output)
# handle astronomy
if args['astronomy']: if args['astronomy']:
output = "{0} Moon illum: {1}% Moon age: {2}d Sunrise: {3} Sunset: {4} Length of Day: {5}".format(self._bu('Astronomy:'),outdata['moonilluminated'],\ output = "{0} Moon illum: {1}% Moon age: {2}d Sunrise: {3} Sunset: {4} Length of Day: {5}".format(\
outdata['moonage'],outdata['sunrise'],outdata['sunset'], outdata['lengthofday']) self._bu('Astronomy:'), outdata['moonilluminated'], outdata['moonage'],outdata['sunrise'],\
if self.registryValue('disableANSI', msg.args[0]): outdata['sunset'], outdata['lengthofday'])
irc.reply(self._strip(output)) # irc output now.
else: irc.reply(output)
irc.reply(output)
# handle main forecast if --forecast is given. # handle main forecast if --forecast is given.
if args['forecast']: if args['forecast']:
outforecast = [] # prep string for output. outforecast = [] # prep string for output.
for (k,v) in fullforecastdata.items(): # iterate through forecast data. for (k, v) in fullforecastdata.items(): # iterate through forecast data.
if self.registryValue('disableColoredTemp'): if args['nocolortemp']:
outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day']),v['text'],\ outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day'].encode('utf-8')),\
v['high'],v['low'])) v['text'].encode('utf-8'), v['high'], v['low']))
else: else:
outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day']),v['text'],\ outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day'].encode('utf-8')),\
self._temp(v['high']),self._temp(v['low']))) v['text'].encode('utf-8'), self._temp(v['high']), self._temp(v['low'])))
output = "{0} :: {1}".format(self._bu('Forecast:'), " | ".join(outforecast)) # string to output # construct our string to output.
if self.registryValue('disableANSI', msg.args[0]): output = "{0} :: {1}".format(self._bu('Forecast:'), " | ".join(outforecast))
irc.reply(self._strip(output)) # now output to irc.
else: irc.reply(output)
irc.reply(output)
wunderground = wrap(wunderground, [getopts({'alerts':'', wunderground = wrap(wunderground, [getopts({'alerts':'',
'almanac':'', 'almanac':'',
@ -601,7 +613,9 @@ class Weather(callbacks.Plugin):
'uv':'', 'uv':'',
'visibility':'', 'visibility':'',
'dewpoint':'', 'dewpoint':'',
'metric':''}), optional('text')]) 'metric':'',
'nocolortemp':'',
'help':''}), optional('text')])
Class = Weather Class = Weather