diff --git a/README.md b/README.md index b80b903..55f734b 100644 --- a/README.md +++ b/README.md @@ -2,85 +2,94 @@ Supybot-Weather =============== Overview - + 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 - an API key to use this plugin. Configure it via: - - /msg bot config plugin.Weather.apiKey - + They've got a nice JSON api that is free to use when you register and grab an API key. + 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 - be depreciated at some point. - - There are a ton of options to configure. You can look through these via /msg config search Weather - Many of these are also available via --options when calling the wunderground command. - + be depreciated at some point. + + Besides a few of the ideas like having a user database, colorized temp, most of the code is mine. + +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 /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 config plugin.Weather.apiKey + + Now reload the plugin: + + /msg reload Weather + + You can now use the basic functionality by: + + /msg wunderground 10012 (or your zipcode) + I suggest adding an alias to this command to make it easier. - - /msg bot Alias add weather 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. + + /msg Alias add weather wunderground + /msg Alias add w wunderground + +Options + + There are a ton of options to configure. You can look through these via /msg 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 wunderground. This can be done via setting a location with the setweather command. - + /msg setweather 10152 /msg setmetric False (to use imperial units) - + /msg 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 - remember you unless you set it on this new nick. - - Use: - /msg getweather - /msg 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. + remember you unless you set it on this new nick. Options This plugin has a bit of configuration that can be done with it. We'll start with the basics: - + - useImperial: 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: - + /msg config plugins.Weather.useImperial configuration variable (True/False) - + You may also use --metric when calling to get metric units. - + - languages: 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 If you do not want to use English, you can set this via one of the codes above: - + /msg 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 channel #channelname plugins.Weather.disableANSI True - - or - - /msg config plugins.Weather.disableANSI True - + + - disableColorTemp 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 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 setcolortemp True/False + once a user is in the database. You can turn it off like this: + /msg config plugins.Weather.disableColorTemp True - + Documentation 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://bitbucket.org/rizon/pypsd/src/8f975a375ab4/modules/internets/api/weather.py # http://ronie.googlecode.com/svn-history/r283/trunk/weather.wunderground/default.py - # http://www.wunderground.com/weather/api/ - diff --git a/config.py b/config.py index ac8d81d..8fb6854 100644 --- a/config.py +++ b/config.py @@ -21,8 +21,7 @@ def configure(advanced): Weather = conf.registerPlugin('Weather') 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,'disableANSI', registry.Boolean(False, """Do not display any ANSI (color/bold) for channel.""")) -conf.registerChannelValue(Weather,'disableColoredTemp', registry.Boolean(False, """If disableANSI is True, this will color temperatures based on values.""")) +conf.registerChannelValue(Weather,'disableColoredTemp', registry.Boolean(False, """If True, this will disable coloring temperatures based on values.""")) 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,'alerts', registry.Boolean(False, ("""Display alerts by default?"""))) diff --git a/plugin.py b/plugin.py index ad62335..a16a1e8 100644 --- a/plugin.py +++ b/plugin.py @@ -3,19 +3,14 @@ # Copyright (c) 2012-2013, spline # All rights reserved. ### - # my libs -import urllib2 import json -import re -from math import floor -from urllib import quote - +from math import floor # for wind. +import sqlite3 # userdb. +import os # extra supybot libs import supybot.conf as conf -import supybot.ircdb as ircdb -import supybot.world as world - +import supybot.log as log # supybot libs import supybot.utils as utils from supybot.commands import * @@ -25,77 +20,113 @@ import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Weather') +# @internationalizeDocstring -@internationalizeDocstring -class WeatherDB(plugins.ChannelUserDB): - """WeatherDB class to store our users locations and metric.""" - def __init__(self, *args, **kwargs): - plugins.ChannelUserDB.__init__(self, *args, **kwargs) +class WeatherDB(): + """WeatherDB class to store our users and their settings.""" - def serialize(self, v): - return list(v) + def __init__(self): + 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): - (id, metric) = L - return (id, metric) + def makeDb(self): + """Create our DB.""" - def getId(self, nick): - return self['x', nick.lower()][0] + self.log.info("WeatherDB: Checking/Creating DB.") + 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): - return self['x', nick.lower()][1] + def setweather(self, username, location): + """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): - try: - metric = self['x', nick.lower()][1] - except KeyError: - metric = 'False' - self['x', nick.lower()] = (id, metric,) + def setmetric(self, username, metric): + """Sets a user's metric value.""" + with self._conn as conn: + cursor = conn.cursor() + cursor.execute("""UPDATE users SET metric=? WHERE nick=?""", (metric, username,)) + 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): """Add the help for "@plugin help Weather" here This should describe *how* to use this plugin.""" threaded = True - # BASICS/WeatherDB def __init__(self, irc): self.__parent = super(Weather, self) self.__parent.__init__(irc) - self.db = WeatherDB(conf.supybot.directories.data.dirize("Weather.db")) self.APIKEY = self.registryValue('apiKey') - world.flushers.append(self.db.flush) + self.db = WeatherDB() def die(self): - if self.db.flush in world.flushers: - world.flushers.remove(self.db.flush) - self.db.close() self.__parent.die() - # COLORING + ############## + # FORMATTING # + ############## + def _bold(self, string): return ircutils.bold(string) def _bu(self, 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): return ircutils.stripFormatting(string) - # WEATHER SYMBOLS + ############################ + # INTERNAL WEATHER HELPERS # + ############################ + def _weatherSymbol(self, code): + """Return a unicode symbol based on weather status.""" + table = {'partlycloudy':'~☁', 'cloudy':'☁', 'tstorms':'⚡', @@ -114,15 +145,17 @@ class Weather(callbacks.Plugin): 'chancesleet':'?❄', 'chancesnow':'?❄', 'chancetstorms':'?☔', - 'unknown':''} + 'unknown':'unknown'} + # return symbol from table. try: return table[code] except KeyError: return None - # MOON PHASE def _moonphase(self, phase): """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: symbol = "[ ( ) ] (fullmoon)" elif phase < 0.20: @@ -139,16 +172,18 @@ class Weather(callbacks.Plugin): symbol = "[ D ] (half moon)" else: symbol = "[ D ] (waxing moon)" + # return. return symbol - # COLOR TEMPERATURE def _temp(self, x): """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" - else: - x = float(str(x).replace('F','')) + else: # f. + x = float(str(x).replace('F', '')) # remove F. str->float. unit = "F" # determine color. if x < 10.0: @@ -170,122 +205,113 @@ class Weather(callbacks.Plugin): else: color = 'light grey' # return. - if unit == "F": - return ircutils.mircColor(("{0:.0f}F".format(x)),color) - else: + if unit == "F": # no need to convert back. + return ircutils.mircColor(("{0:.0f}F".format(x)), color) + 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) - # DEGREES TO DIRECTION (wind) def _wind(self, angle, useSymbols=False): - if not useSymbols: - direction_names = ["N","NE","E","SE","S","SW","W","NW"] - else: - direction_names = ['↑','↗','→','↘','↓','↙','←','↖'] + """Converts degrees to direction for wind. Can optionally return a symbol.""" + + if not useSymbols: # ordinal 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_step = 360./directions_num index = int(round((angle/360. - floor(angle/360.)*360.)/directions_step)) index %= directions_num + # return. return direction_names[index] - # PUBLIC FUNCTIONS TO WORK WITH WEATHERDB. - def weatherusers(self, irc, msg, args): - """ - 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) + ############################################## + # PUBLIC FUNCTIONS TO WORK WITH THE DATABASE # + ############################################## - def setmetric(self, irc, msg, args, optboolean): + def setcolortemp(self, irc, msg, args, opttemp): + """ + 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): """ Sets the user's use metric setting to True or False. 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): """ - Set's weather location code for your nick as . - Use your zip/postal code to keep it simple. Ex: setweather 03062 + Set's weather location code for your nick as location code. + + Use your zip/postal code to keep it simple. + Ex: setweather 10012 """ - # set the weather id based on nick. - optid = optid.replace(' ','') - self.db.setId(msg.nick, optid) - irc.reply("I have changed {0}'s weather ID to {1}".format(msg.nick, optid)) + + # set the weather id based on nick. This will update or set. + self.db.setweather(msg.nick.lower(), optlocation) + irc.reply("I have changed {0}'s weather ID to {1}".format(msg.nick.lower(), optlocation)) + 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 # #################### def wunderground(self, irc, msg, args, optlist, optinput): - """[--options] [location] + """[--options] + + Fetch weather and forcast information for + 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. - if not self.keycheck(irc): - return False + 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 and reload Weather.") + return # urlargs will be used to build the url to query the API. - urlArgs = {'features':['conditions','forecast'], + urlArgs = {'features':['conditions', 'forecast'], 'lang':self.registryValue('lang'), 'bestfct':'1', - 'pws':'0' - } + 'pws':'0' } # now, start our dict for output formatting. args = {'imperial':self.registryValue('useImperial', msg.args[0]), + 'nocolortemp':self.registryValue('disableColoredTemp', msg.args[0]), 'alerts':self.registryValue('alerts'), 'almanac':self.registryValue('almanac'), 'astronomy':self.registryValue('astronomy'), @@ -296,23 +322,7 @@ class Weather(callbacks.Plugin): 'strip':False, 'uv':False, 'visibility':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 - + 'dewpoint':False } # handle optlist (getopts). this will manipulate output via args dict. if optlist: for (key, value) in optlist: @@ -336,9 +346,33 @@ class Weather(callbacks.Plugin): args['dewpoint'] = True if key == 'astronomy': 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 (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 ") + return # 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 # now we need to set certain things for urlArgs based on args. 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 url += "{0}:{1}/".format(key, value) # 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. + # try and query url. try: - request = urllib2.Request(url) - u = urllib2.urlopen(request) - except Exception as e: - self.log.info("Error loading {0} message {1}".format(url, e)) - irc.reply("Failed to load wunderground API: %s" % e) + page = utils.web.getUrl(url) + except utils.web.Error as e: + self.log.error("ERROR: Trying to open {0} message: {1}".format(url, e)) + irc.reply("ERROR: Failed to load Weather Underground API: {0}".format(e)) return - # process the json, check (in orders) for errors, multiple results, and one last - # sanity check. then we can process it. - data = json.load(u) + # process json. + data = json.loads(page.decode('utf-8')) - # check if we got errors and return. - if 'error' in data['response']: - errortype = data['response']['error']['type'] + # now, a series of sanity checks before we process. + if 'error' in data['response']: # check if there are errors. + errortype = data['response']['error']['type'] # type. description is below. 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 - # if there is more than one city matching. - if 'results' in data['response']: - output = [item['city'] + ", " + item['state'] + " (" + item['country_name'] + ")" for item in data['response']['results']] - irc.reply("More than 1 city matched your query, try being more specific: {0}".format(" | ".join(output))) + if 'results' in data['response']: # results only comes when location matches more than one. + output = [i['city'] + ", " + i['state'] + " (" + i['country_name'] + ")" for i in data['response']['results']] + irc.reply("ERROR: More than 1 city matched your query, try being more specific: {0}".format(" | ".join(output))) return - # last sanity check 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 - # done with error checking. - # now, put everything into outdata dict for output later. + # no errors so we start the main part of processing. outdata = {} outdata['weather'] = data['current_observation']['weather'] outdata['location'] = data['current_observation']['display_location']['full'] @@ -394,28 +422,26 @@ class Weather(callbacks.Plugin): outdata['uv'] = data['current_observation']['UV'] # handle wind. check if there is none first. - if args['imperial']: - if data['current_observation']['wind_mph'] < 1: # no wind. - outdata['wind'] = "None" - else: - 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: - outdata['wind'] += " ({0}mph gusts)".format(data['current_observation']['wind_gust_mph']) - else: - if data['current_observation']['wind_kph'] < 1: # no wind. - outdata['wind'] = "None" - else: + if data['current_observation']['wind_mph'] < 1: # no wind. + outdata['wind'] = "None" + else: # we do have wind. process differently. + 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']) + if data['current_observation']['wind_gust_mph'] > 0: # gusts? + outdata['wind'] += " ({0}mph gusts)".format(data['current_observation']['wind_gust_mph']) + else: # handle metric units for wind. 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: - outdata['wind'] += " ({0}kph gusts)".format(data['current_observation']['wind_gust_kph']) + if data['current_observation']['wind_gust_kph'] > 0: # gusts? + outdata['wind'] += " ({0}kph gusts)".format(data['current_observation']['wind_gust_kph']) # handle the time. concept/method from WunderWeather plugin. - observationTime = data['current_observation'].get('observation_epoch', None) - localTime = data['current_observation'].get('local_epoch', None) - if not observationTime or not localTime: # if we don't have the epoches from above, default to obs_time + observationTime = data['current_observation'].get('observation_epoch') + localTime = data['current_observation'].get('local_epoch') + # 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 ') - else: # format for relative time. - s = int(localTime) - int(observationTime) + else: # we do have so format for relative time. + s = int(localTime) - int(observationTime) # format into seconds. if s <= 1: outdata['observation'] = 'just now' elif s < 60: @@ -429,8 +455,8 @@ class Weather(callbacks.Plugin): else: outdata['observation'] = '{0}hrs ago'.format(s/3600) - # all conditionals for imperial/metric - if args['imperial']: + # handle basics like temp/pressure/dewpoint. + if args['imperial']: # assigns the symbol based on metric. outdata['temp'] = str(data['current_observation']['temp_f']) + 'F' outdata['pressure'] = data['current_observation']['pressure_in'] + 'in' 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['feelslike'] = str(data['current_observation']['feelslike_f']) + 'F' outdata['visibility'] = str(data['current_observation']['visibility_mi']) + 'mi' - else: + else: # metric. outdata['temp'] = str(data['current_observation']['temp_c']) + 'C' outdata['pressure'] = data['current_observation']['pressure_mb'] + 'mb' 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['visibility'] = str(data['current_observation']['visibility_km']) + 'km' - # handle forecast data part. output will be below. - # this is not the --forecast part. - forecastdata = {} + # handle forecast data part. output will be below. (not --forecast) + forecastdata = {} # key = int(day), value = forecast dict. for forecastday in data['forecast']['txt_forecast']['forecastday']: tmpdict = {} tmpdict['day'] = forecastday['title'] tmpdict['symbol'] = self._weatherSymbol(forecastday['icon']) - if args['imperial']: + if args['imperial']: # imperial. tmpdict['text'] = forecastday['fcttext'] - else: + else: # metric. tmpdict['text'] = forecastday['fcttext_metric'] forecastdata[int(forecastday['period'])] = tmpdict # now this is the --forecast part. - if args['forecast']: - fullforecastdata = {} + if args['forecast']: # only if we get this in getopts. + fullforecastdata = {} # key = day (int), value = dict of forecast data. for forecastday in data['forecast']['simpleforecast']['forecastday']: tmpdict = {} tmpdict['day'] = forecastday['date']['weekday_short'] tmpdict['symbol'] = self._weatherSymbol(forecastday['icon']) tmpdict['text'] = forecastday['conditions'] - if args['imperial']: + if args['imperial']: # imperial. tmpdict['high'] = forecastday['high']['fahrenheit'] + "F" tmpdict['low'] = forecastday['low']['fahrenheit'] + "F" - else: + else: # metric. tmpdict['high'] = forecastday['high']['celsius'] + "C" tmpdict['low'] = forecastday['low']['celsius'] + "C" fullforecastdata[int(forecastday['period'])] = tmpdict @@ -480,12 +505,12 @@ class Weather(callbacks.Plugin): if args['almanac']: outdata['highyear'] = data['almanac']['temp_high']['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['highrecord'] = data['almanac']['temp_high']['record']['F'] + "F" outdata['lownormal'] = data['almanac']['temp_low']['normal']['F'] + "F" outdata['lowrecord'] = data['almanac']['temp_low']['record']['F'] + "F" - else: + else: # metric. outdata['highnormal'] = data['almanac']['temp_high']['normal']['C'] + "C" outdata['highrecord'] = data['almanac']['temp_high']['record']['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']) sunseth = int(data['moon_phase']['sunset']['hour']) sunsetm = int(data['moon_phase']['sunset']['minute']) - outdata['sunrise'] = "{0}:{1}".format(sunriseh,sunrisem) - outdata['sunset'] = "{0}:{1}".format(sunseth,sunsetm) + outdata['sunrise'] = "{0}:{1}".format(sunriseh, sunrisem) # construct sunrise. + 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) # handle alerts - if args['alerts']: - if data['alerts']: - outdata['alerts'] = data['alerts'][:300] # alert limit to 300. - else: + if args['alerts']: # only look for alerts if there. + if data['alerts']: # alerts is a list. it can also be empty. + outdata['alerts'] = data['alerts'][:300] # limit chars to 300. + else: # no alerts found (empty). outdata['alerts'] = "No alerts." - # OUTPUT - # now, build output object with what to output. ° u" \u00B0C" - if self.registryValue('disableColoredTemp'): - output = "Weather for {0} :: {1} ({2})".format(self._bold(outdata['location']),\ - outdata['weather'],outdata['temp']) - else: - output = "Weather for {0} :: {1} ({2})".format(self._bold(outdata['location']),\ - outdata['weather'],self._temp(outdata['temp'])) + # OUTPUT. + # we go step-by-step to build the proper string. ° u" \u00B0C" + output = "Weather for {0} :: {1}".format(self._bold(outdata['location'].encode('utf-8')), outdata['weather'].encode('utf-8')) + if args['nocolortemp']: # don't color temp. + output += " {0}".format(outdata['temp']) + else: # colored temperature. + output += " {0}".format(self._temp(outdata['temp'])) # windchill/heatindex are conditional on season but test with startswith to see what to include - if not outdata['windchill'].startswith("NA"): - if self.registryValue('disableColoredTemp'): + if not outdata['windchill'].startswith("NA"): # windchill. + if args['nocolortemp']: # don't color 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'])) - if not outdata['heatindex'].startswith("NA"): - if self.registryValue('disableColoredTemp'): + if not outdata['heatindex'].startswith("NA"): # heatindex. + if args['nocolortemp']: # don't color 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'])) # now get into the args dict for what to include (extras) - for (k,v) in args.items(): - if k in ['wind','visibility','uv','pressure','dewpoint']: # if key is in extras - if v: # if that key's value is True - output += " | {0}: {1}".format(self._bold(k.title()), outdata[k]) - # add in the first forecast item in conditions + updated time. - output += " | {0}: {1} {2}: {3}".format(self._bold(forecastdata[0]['day']),\ - forecastdata[0]['text'],self._bold(forecastdata[1]['day']),forecastdata[1]['text']) - # show Updated? + for (k, v) in args.items(): + if k in ['wind', 'visibility', 'uv', 'pressure', 'dewpoint']: # if key is in extras + if v: # if that key's value is True, we add it. + output += " | {0}: {1}".format(self._bold(k.title()), outdata[k].encode('utf-8')) + # add in the first two forecast item in conditions + updated time. + output += " | {0}: {1}".format(self._bold(forecastdata[0]['day'].encode('utf-8')), forecastdata[0]['text'].encode('utf-8')) + output += " {0}: {1}".format(self._bold(forecastdata[1]['day'].encode('utf-8')), forecastdata[1]['text'].encode('utf-8')) + # show Updated? if args['updated']: - output += " | {0} {1}".format(self._bold('Updated:'), outdata['observation']) - # output. - if self.registryValue('disableANSI', msg.args[0]): - irc.reply(self._strip(output)) - else: - irc.reply(output) + output += " | {0} {1}".format(self._bold('Updated:'), outdata['observation'].encode('utf-8')) + # finally, output the basic weather. + irc.reply(output) - # next, for outputting, handle the extras like alerts, almanac, etc. - if args['alerts']: - output = "{0} :: {1}".format(self._bu("Alerts:"),outdata['alerts']) - if self.registryValue('disableANSI', msg.args[0]): - irc.reply(self._strip(output)) - else: - irc.reply(output) - # handle almanac + # next, for outputting, handle the extras like alerts, almanac, astronomy, forecast. + if args['alerts']: # if --alerts issued. + irc.reply("{0} :: {1}".format(self._bu("Alerts:"), outdata['alerts'].encode('utf-8'))) + # handle almanac if --almanac is given. 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(\ - self._bu('Almanac:'),outdata['highnormal'],outdata['highrecord'],\ - outdata['highyear'],outdata['lownormal'],outdata['lowrecord'],outdata['lowyear']) - else: + self._bu('Almanac:'), outdata['highnormal'], outdata['highrecord'], outdata['highyear'],\ + outdata['lownormal'], outdata['lowrecord'], outdata['lowyear']) + else: # colored temp. 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']),\ - outdata['highyear'],self._temp(outdata['lownormal']),self._temp(outdata['lowrecord']),outdata['lowyear']) - if self.registryValue('disableANSI', msg.args[0]): - irc.reply(self._strip(output)) - else: - irc.reply(output) - # handle astronomy + self._bu('Almanac:'), self._temp(outdata['highnormal']), self._temp(outdata['highrecord']),\ + outdata['highyear'], self._temp(outdata['lownormal']), self._temp(outdata['lowrecord']), outdata['lowyear']) + # now output to irc. + irc.reply(output) + # handle astronomy if --astronomy is given. 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'],\ - outdata['moonage'],outdata['sunrise'],outdata['sunset'], outdata['lengthofday']) - if self.registryValue('disableANSI', msg.args[0]): - irc.reply(self._strip(output)) - else: - irc.reply(output) + output = "{0} Moon illum: {1}% Moon age: {2}d Sunrise: {3} Sunset: {4} Length of Day: {5}".format(\ + self._bu('Astronomy:'), outdata['moonilluminated'], outdata['moonage'],outdata['sunrise'],\ + outdata['sunset'], outdata['lengthofday']) + # irc output now. + irc.reply(output) # handle main forecast if --forecast is given. if args['forecast']: outforecast = [] # prep string for output. - for (k,v) in fullforecastdata.items(): # iterate through forecast data. - if self.registryValue('disableColoredTemp'): - outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day']),v['text'],\ - v['high'],v['low'])) + for (k, v) in fullforecastdata.items(): # iterate through forecast data. + if args['nocolortemp']: + outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day'].encode('utf-8')),\ + v['text'].encode('utf-8'), v['high'], v['low'])) else: - outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day']),v['text'],\ - self._temp(v['high']),self._temp(v['low']))) - output = "{0} :: {1}".format(self._bu('Forecast:'), " | ".join(outforecast)) # string to output - if self.registryValue('disableANSI', msg.args[0]): - irc.reply(self._strip(output)) - else: - irc.reply(output) - + outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day'].encode('utf-8')),\ + v['text'].encode('utf-8'), self._temp(v['high']), self._temp(v['low']))) + # construct our string to output. + output = "{0} :: {1}".format(self._bu('Forecast:'), " | ".join(outforecast)) + # now output to irc. + irc.reply(output) wunderground = wrap(wunderground, [getopts({'alerts':'', 'almanac':'', @@ -601,7 +613,9 @@ class Weather(callbacks.Plugin): 'uv':'', 'visibility':'', 'dewpoint':'', - 'metric':''}), optional('text')]) + 'metric':'', + 'nocolortemp':'', + 'help':''}), optional('text')]) Class = Weather