Merge remote-tracking branch 'weather/devel'

This commit is contained in:
James Lu 2015-03-13 22:58:48 -07:00
commit 54350230d3
6 changed files with 269 additions and 489 deletions

View File

@ -1,6 +1,8 @@
language: python
python:
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- pypy
- pypy3

View File

@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2014 spline
Copyright (c) 2014-2015 James Lu
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -2,56 +2,33 @@
# Limnoria plugin for Weather Underground (GLolol's fork)
## Introduction
## Installation
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 deprecated at some point.
You will need a working Limnoria bot on Python 2.7/3.2+ for this to work.
## Install
You will need a working Limnoria bot on Python 2.7/3.4 for this to work.
Go into your Limnoria plugin dir, usually `~/supybot/plugins` and run:
1) Go into your Limnoria plugin dir, usually `~/supybot/plugins` and run:
```
git clone https://github.com/GLolol/Supybot-Weather
```
To install additional requirements, run:
Alternatively, you can fetch this plugin (albeit a slightly older version) via Limnoria's PluginDownloader using: `install GLolol Weather`.
```
pip install -r requirements.txt
```
or if you don't have or don't want to use root,
```
pip install -r requirements.txt --user
```
Next, load the plugin:
2) Load the plugin:
```
/msg bot load Weather
```
[Fetch an API key for Wunderground](http://www.wunderground.com/weather/api/) by signing up (free).
3) [Fetch an API key from Wunderground](http://www.wunderground.com/weather/api/) by signing up (free).
Once getting this key, you will need to set it on your bot before things will work.
Reload once you perform this operation to start using it.
```
/msg bot config plugins.Weather.apiKey <APIKEY>
/msg <yourbot> config plugins.Weather.apiKey <APIKEY>
```
Now, reload the bot and you should be good to go:
```
/msg bot reload Weather
```
*Optional:* There are some config variables that can be set for the bot. They mainly control output stuff.
4) *Optional:* There are some config variables that can be set for the bot. They mainly control output stuff.
```
/msg bot config search Weather
@ -59,9 +36,12 @@ Now, reload the bot and you should be good to go:
## Example Usage
When calling the `wunderground` command, you can use zip codes (10002), cities (New York, NY), etc. Weather Underground is pretty
intelligent here.
```
<spline> @wunderground 10002
<myybot> New York, NY :: Rain :: 52F | Visibility: 4.0mi | Saturday: Rain. High around 55F. ...
<myybot> New York, NY :: Rain :: 52F | Visibility: 4.0mi | Saturday: Rain. High around 55F...
```
## Features
@ -72,14 +52,8 @@ There are a ton of options to configure. You can find these via:
/msg bot config search Weather
```
Many of these are also available via --help when calling the wunderground command.
Users can also have their location remembered by the plugin's internal database so that
they will not have to continually type in their location. NOTE: It uses their nick only,
so if they are on a different nick, even with an identical hostmask, it will not match.
You can use zipcodes (10002), cities (New York, NY), etc. Weather Underground is pretty
intelligent here.
they will not have to continually type in their location.
```
<spline> @setweather 10002
@ -93,7 +67,7 @@ This now allows a user to type in the weather command w/o any arguments:
<myybot> Manchester, NH :: Rain :: 45F | Visibility: 10.0mi | Saturday: Occasional light rain. High 56F. ...
```
Users can also have the bot remember their preferred options, such as using Metric when displaying weather:
Users can also have the bot remember their preferred options, such as using metric units when displaying weather:
```
<spline> @setuser metric False

View File

@ -17,21 +17,27 @@ def configure(advanced):
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Weather', True)
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,'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?""")))
conf.registerGlobalValue(Weather,'almanac', registry.Boolean(False, ("""Display almanac by default?""")))
conf.registerGlobalValue(Weather,'astronomy', registry.Boolean(False, ("""Display astronomy by default?""")))
conf.registerGlobalValue(Weather,'showPressure', registry.Boolean(False, ("""Show pressure in output?""")))
conf.registerGlobalValue(Weather,'showWind', registry.Boolean(False, ("""Show wind in output?""")))
conf.registerGlobalValue(Weather,'showUpdated', registry.Boolean(False, ("""Show updated in output?""")))
conf.registerChannelValue(Weather,'showImperialAndMetric', registry.Boolean(True, ("""In channel, display output with Imperial and Metric?""")))
conf.registerGlobalValue(Weather,'lang', registry.String('EN', ("""language to use. See docs for available codes.""")))
conf.registerGlobalValue(Weather, 'apiKey',
registry.String('', ("""Sets the API key for the plugin. You can obtain an API key at http://www.wunderground.com/weather/api/."""), private=True))
conf.registerChannelValue(Weather, 'useImperial',
registry.Boolean(True, ("""Determines whether imperial units (Fahrenheit, etc.) will be used.""")))
conf.registerGlobalValue(Weather,'forecast',
registry.Boolean(True, ("""Determines whether forecasts will be displayed by default.""")))
conf.registerGlobalValue(Weather,'alerts',
registry.Boolean(False, ("""Determines whether forecasts will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'almanac',
registry.Boolean(False, ("""Determines whether almanac will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'astronomy',
registry.Boolean(False, ("""Determines whether astronomy will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'showPressure',
registry.Boolean(False, ("""Determines whether pressure will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'showWind',
registry.Boolean(False, ("""Determines whether winde will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'showUpdated',
registry.Boolean(False, ("""Determines whether the bot will show the data's "last updated" time by default.""")))
conf.registerGlobalValue(Weather, 'lang',
registry.String('EN', ("""Determines the language used by the plugin.""")))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250:

View File

@ -1,21 +1,38 @@
# -*- coding: utf-8 -*-
###
# Copyright (c) 2012-2014, spline
# Copyright (c) 2014-2015, James Lu
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
###
# my libs
from __future__ import unicode_literals
import json # json.
from math import floor # for wind.
import sqlite3 # userdb.
import json
from math import floor
import sqlite3
try:
from itertools import izip
except ImportError: # python3
except ImportError: # Python 3
izip = zip
# extra supybot libs
import supybot.conf as conf
import supybot.log as log
# supybot libs
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
@ -43,14 +60,13 @@ class WeatherDB():
def makeDb(self):
"""Create our DB."""
self.log.info("WeatherDB: Checking/Creating DB.")
self.log.info("Weather: 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,
alerts INTEGER DEFAULT 0,
almanac INTEGER DEFAULT 0,
astronomy INTEGER DEFAULT 0,
@ -67,7 +83,7 @@ class WeatherDB():
cursor = conn.cursor() # the old table is 4.
tablelength = len([l[1] for l in cursor.execute("pragma table_info('users')").fetchall()])
if tablelength == 4: # old table is 4: users, location, metric, colortemp.
self.log.info("Table length is 4. We need to upgrade.")
self.log.info("Weather: Upgrading database version.")
columns = ['alerts', 'almanac', 'astronomy', 'forecast', 'pressure', 'wind', 'uv', 'visibility', 'dewpoint', 'humidity', 'updated']
for column in columns:
try:
@ -129,19 +145,14 @@ class WeatherDB():
class Weather(callbacks.Plugin):
"""Add the help for "@plugin help Weather" here
This should describe *how* to use this plugin."""
"""This plugin provides access to information from Weather Underground."""
threaded = True
def __init__(self, irc):
self.__parent = super(Weather, self)
self.__parent.__init__(irc)
self.APIKEY = self.registryValue('apiKey')
self.db = WeatherDB()
def die(self):
self.__parent.die()
##############
# FORMATTING #
##############
@ -152,9 +163,6 @@ class Weather(callbacks.Plugin):
def _bu(self, string):
return ircutils.underline(ircutils.bold(string))
def _strip(self, string):
return ircutils.stripFormatting(string)
############################
# INTERNAL WEATHER HELPERS #
############################
@ -162,105 +170,64 @@ class Weather(callbacks.Plugin):
def _weatherSymbol(self, code):
"""Return a unicode symbol based on weather status."""
table = {'partlycloudy':'~☁',
'cloudy':'',
'tstorms':'',
'sunny':'',
'snow':'',
'sleet':'',
'rain':'',
'mostlysunny':'~☀',
'mostlycloudy':'~☁',
'hazy':'',
'fog':'',
'flurries':'',
'clear':'',
'chanceflurries':'?❄',
'chancerain':'?☔',
'chancesleet':'?❄',
'chancesnow':'?❄',
'chancetstorms':'?☔' }
table = {'partlycloudy': '~☁',
'cloudy': '',
'tstorms': '',
'sunny': '',
'snow': '',
'sleet': '',
'rain': '',
'mostlysunny': '~☀',
'mostlycloudy': '~☁',
'hazy': '',
'fog': '',
'flurries': '',
'clear': '',
'chanceflurries': '?❄',
'chancerain': '?☔',
'chancesleet': '?❄',
'chancesnow': '?❄',
'chancetstorms': '?☔'}
# return symbol from table.
try:
return table[code]
except KeyError:
return "unknown"
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:
symbol = "[ C ] (decreasing moon)"
elif phase < 0.30:
symbol = "[ C ] (half moon)"
elif phase < 0.45:
symbol = "[ ( ] (decreasing moon)"
elif phase < 0.65:
symbol = "[ ] (new moon)"
elif phase < 0.80:
symbol = "[ ) ] (waxing moon)"
elif phase < 0.80:
symbol = "[ D ] (half moon)"
else:
symbol = "[ D ] (waxing moon)"
# return.
return symbol
def _temp(self, x):
def _temp(self, f, c=None):
"""Returns a colored string based on the temperature."""
# lets be safe and wrap in a try/except because we can't always trust data purity.
try:
if x.startswith('NA'): # Wunderground sends a field that's not available
return x
# first, convert into F so we only have one table.
if x.endswith('C'): # c.
x = float(x[:-1]) * 9 / 5 + 32 # remove C + math into float(F).
unit = "C"
else: # f.
x = float(x[:-1]) # remove F. str->float.
unit = "F"
if str(f).startswith('NA'): # Wunderground sends a field that's not available
return f
f = int(f)
if not c:
c = int((f - 32) * 5/9)
# determine color.
if x < 10.0:
if f < 10.0:
color = 'light blue'
elif 10.0 <= x <= 32.0:
elif 10.0 <= f <= 32.0:
color = 'teal'
elif 32.1 <= x <= 50.0:
elif 32.1 <= f <= 50.0:
color = 'blue'
elif 50.1 <= x <= 60.0:
elif 50.1 <= f <= 60.0:
color = 'light green'
elif 60.1 <= x <= 70.0:
elif 60.1 <= f <= 70.0:
color = 'green'
elif 70.1 <= x <= 80.0:
elif 70.1 <= f <= 80.0:
color = 'yellow'
elif 80.1 <= x <= 90.0:
elif 80.1 <= f <= 90.0:
color = 'orange'
elif x > 90.0:
elif f > 90.0:
color = 'red'
else:
color = 'light grey'
# return.
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)
except Exception as e: # rutroh. something went wrong.
self.log.info("_temp: ERROR trying to convert temp: {0} message: {1}".format(x, e))
return x
def _tw(self, bol, x):
"""This is a convenience handle that wraps _temp."""
# make sure we have 'bol', which should come in from args['nocolortemp'].
# since the option is a negation, we assume NO.
if not bol: # COLOR IT.
x = self._temp(x)
return x
else:
return x
return ircutils.mircColor(("{0}F/{1}C".format(f, c)), color)
except ValueError as e:
self.log.info("Weather: ValueError trying to convert temp: {0} message: {1}".format(f, e))
return f
def _wind(self, angle, useSymbols=False):
"""Converts degrees to direction for wind. Can optionally return a symbol."""
@ -285,8 +252,8 @@ class Weather(callbacks.Plugin):
"""<setting> <True|False>
Sets a user's <setting> to True or False.
Settings: alerts, almanac, astronomy, forecast, pressure, wind, uv, visibility, dewpoint, humidity, updated
Ex: metric True or colortemp False
Valid settings include: alerts, almanac, astronomy, forecast, pressure,
wind, uv, visibility, dewpoint, humidity, and updated.
"""
# first, lower
@ -294,19 +261,18 @@ class Weather(callbacks.Plugin):
# grab a list of valid settings.
validset = self.db.getsettings()
if optset not in validset:
irc.error("'{0}' is an invalid setting. Must be one of: {1}".format(optset, " | ".join(sorted([i for i in validset]))), Raise=True)
return
# setting value True/False
irc.error(format("%r is an invalid setting. Must be one of: %L.", optset,
sorted(validset)), Raise=True)
if optbool: # True.
value = 1
else: # False.
value = 0
# check user first.
if not self.db.getuser(msg.nick.lower()): # user exists
irc.error("You're not in the database. You must setweather first.", Raise=True)
irc.error("You are not in the database; you must use 'setweather' first.", Raise=True)
else: # user is valid. perform the op.
self.db.setsetting(msg.nick.lower(), optset, value)
irc.reply("I have changed {0}'s {1} setting to {2}".format(msg.nick, optset, value))
irc.replySuccess()
setuser = wrap(setuser, [('somethingWithoutSpaces'), ('boolean')])
@ -317,10 +283,8 @@ class Weather(callbacks.Plugin):
Use your zip/postal code to keep it simple.
Ex: setweather 10012
"""
# 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))
irc.replySuccess()
setweather = wrap(setweather, [('text')])
@ -332,217 +296,135 @@ class Weather(callbacks.Plugin):
"""Internal helper to find a location via Wunderground's autocomplete API."""
url = 'http://autocomplete.wunderground.com/aq?query=%s' % utils.web.urlquote(q)
#self.log.info("WUAC URL: {0}".format(url))
# try and fetch.
self.log.debug("Autocomplete URL: %s", url)
try:
page = utils.web.getUrl(url)
except Exception as e: # something didn't work.
self.log.info("_wuac: ERROR: Trying to open {0} message: {1}".format(url, e))
except Exception as e:
self.log.info("Weather: (_wuac) Error trying to open {0} message: {1}".format(url, e))
return None
# now process json and return.
try:
data = json.loads(page.decode('utf-8'))
loc = data['RESULTS'][0]['zmw'] # find the first zmw.
loc = "zmw:%s" % loc # return w/zmw: attached.
# ZMW is in some ways a lot like Wunderground's location ID Codes, for when locations
# are too ambiguous. (e.g. looking up "France", which is a country with many different
# locations!)
for item in data['RESULTS']:
# Sometimes the autocomplete will lead us to more disambiguation pages...
# which cause lots of errors in processing!
if item['tz'] != 'MISSING':
loc = "zmw:%s" % item['zmw']
break
return loc
except Exception as e:
self.log.info("_wuac: ERROR processing json in {0} :: {1}".format(url, e))
self.log.info("Weather: (_wuac) Error processing JSON in {0} :: {1}".format(url, e))
return None
def _wunderjson(self, url, location):
"""Fetch wunderground JSON and return."""
# first, construct the url properly.
if url.endswith('/'): # cheap way to strip the tailing /
if url.endswith('/'): # cheap way to strip the trailing /
url = '%sq/%s.json' % (url, utils.web.urlquote(location))
else:
url = '%s/q/%s.json' % (url, utils.web.urlquote(location))
# now actually fetch the url.
try:
self.log.info("URL: {0}".format(url))
self.log.debug("Weather URL: {0}".format(url))
page = utils.web.getUrl(url)
return page
except Exception as e: # something didn't work.
self.log.info("_wunderjson: ERROR Trying to open {0} message: {1}".format(url, e))
self.log.info("Weather: (_wunderjson) Error trying to open {0} message: {1}".format(url, e))
return None
####################
# PUBLIC FUNCTIONS #
####################
def wunderground(self, irc, msg, args, optlist, optinput):
def wunderground(self, irc, msg, args, optinput):
"""[--options] <location>
Fetch weather and forcast information for <location>
Fetches weather and forecast information for <location>.
Location must be one of: US state/city (CA/San_Francisco), zipcode, country/city (Australia/Sydney), airport code (KJFK)
Use --help to list all options.
Location must be one of: US state/city (CA/San_Francisco), zip code, country/city (Australia/Sydney), or an airport code (KJFK).
Ex: 10021 or Sydney, Australia or KJFK
"""
# first, check if we have an API key. Useless w/o this.
if len(self.APIKEY) < 1 or not self.APIKEY or self.APIKEY == "Not set":
irc.error("Need a Wunderground API key. Set config plugins.Weather.apiKey and reload Weather.", Raise=True)
apikey = self.registryValue('apiKey')
if not apikey:
irc.error("No Wunderground API key was defined. Set 'config plugins.Weather.apiKey' and reload the plugin.",
Raise=True)
# urlargs will be used to build the url to query the API.
# besides lang, these are unmutable values that should not be changed.
urlArgs = {'features':['conditions', 'forecast'],
'lang':self.registryValue('lang'),
'bestfct':'1',
'pws':'0' }
# now, figure out the rest of the options for fetching and displaying weather.
# some of these are for the query and the others are for output.
# the order will always go global->channel (supybot config) -> user.
urlArgs = {'features': ['conditions', 'forecast'],
'lang': self.registryValue('lang'),
'bestfct': '1',
'pws': '0' }
loc = None
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'),
'pressure':self.registryValue('showPressure'),
'wind':self.registryValue('showWind'),
'updated':self.registryValue('showUpdated'),
'showImperialAndMetric':self.registryValue('showImperialAndMetric', msg.args[0]),
'forecast':False,
'humidity':False,
'strip':False,
'uv':False,
'visibility':False,
'dewpoint':False }
args = {'imperial': self.registryValue('useImperial', msg.args[0]),
'alerts': self.registryValue('alerts'),
'almanac': self.registryValue('almanac'),
'astronomy': self.registryValue('astronomy'),
'pressure': self.registryValue('showPressure'),
'wind': self.registryValue('showWind'),
'updated': self.registryValue('showUpdated'),
'forecast': False,
'humidity': False,
'uv': False,
'visibility': False,
'dewpoint': False}
# instead of doing optlist, we need to handle the location/options to set initially.
# first, check if there is a user so we can grab their settings.
usersetting = self.db.getweather(msg.nick.lower()) # check the db.
if usersetting: # user is found. lets grab their location and settings.
for (k, v) in usersetting.items(): # iterate over settings dict returned from getweather row.
# set specific settings based on keys that won't 1:1 match.
if k == 'location': # location. look down below this for how the logic is handled.
loc = v # copy over their location from the DB to loc.
elif k == 'metric': # metric
if v == 1: # true.
args['imperial'] = False
else: # 0 = false.
args['imperial'] = True
elif k == 'colortemp': # colortemp.
if v == 1: # true.
args['nocolortemp'] = False
else: # false. the 'nocolortemp' values are inverse.
args['nocolortemp'] = True
else: # rest of them are 1:1.
if v == 1: # if value is 1, or true.
args[k] = True
else: # argument is 0 or False.
args[k] = False
else: # user was not found.
usersetting = self.db.getweather(msg.nick.lower())
if usersetting:
for (k, v) in usersetting.items():
args[k] = v
loc = usersetting["location"]
args['imperial'] = (not usersetting["metric"])
else:
if not optinput: # location was also not specified, so we must bail.
irc.error("I did not find a preset location for you. Set via setweather <location>", Raise=True)
irc.error("I did not find a preset location for you. Set one via 'setweather <location>'.", Raise=True)
# handle optlist (getopts). this will manipulate output via args dict.
# we must do this after the dblookup for users as it would always override.
if optlist:
for (key, value) in optlist:
if key == "metric":
args['imperial'] = False
if key == 'alerts':
args['alerts'] = True
if key == 'forecast':
args['forecast'] = True
if key == 'almanac':
args['almanac'] = True
if key == 'pressure':
args['pressure'] = True
if key == 'humidity':
args['humidity'] = True
if key == 'wind':
args['wind'] = True
if key == 'uv':
args['uv'] = True
if key == 'visibility':
args['visibility'] = True
if key == 'dewpoint':
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 <location> (set user's location). setmetric True/False (set metric option) setcolortemp True/False (display color temp?")
return
# now that we're done with 'input things'
# we need to decide on how to handle the location.
# optinput = user specified location, regardless if they're known or not.
# loc = the location that can come back if a user is known and this is set.
# both of these might not be valid locations. however, if a user specifies a location, we should look it up.
if optinput: # if we have optinput, regardless if the user is known or not, autocomplete it.
if optinput:
wloc = self._wuac(optinput)
if not wloc: # error looking up the location.
if not wloc:
irc.error("I could not find a valid location for: {0}".format(optinput), Raise=True)
elif loc and not optinput: # user is known. location is set. no optinput.
wloc = loc # set wloc as their location. worst case, the user gets an error for setting it wrong.
elif loc: # user is known. location is set. no optinput.
wloc = loc
else: # no optinput. no location. error out. this should happen above but lets be redundant.
irc.error("You must specify a city to search for weather.", Raise=True)
# build url now. first, apikey. then, iterate over urlArgs and insert.
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.
url = 'http://api.wunderground.com/api/%s/' % (apikey)
for check in ['alerts', 'almanac', 'astronomy']:
if args[check]: # if args['value'] is True, either via config or getopts.
if args[check]:
urlArgs['features'].append(check) # append to dict->key (list)
# now, we use urlArgs dict to append to url.
for (key, value) in urlArgs.items():
if key == "features": # will always be at least conditions.
url += "".join([item + '/' for item in value]) # listcmp the features/
if key == "lang" or key == "bestfct" or key == "pws": # rest added with key:value
if key in ("lang", "bestfct", "pws"): # rest added with key:value
url += "{0}:{1}/".format(key, value)
# now that we're done, lets finally make our API call.
page = self._wunderjson(url, wloc)
if not page:
irc.error("Failed to load Wunderground API. Check the logs for more information.", Raise=True)
# process json.
try:
data = json.loads(page.decode('utf-8'))
except Exception as e:
self.log.error("ERROR: could not process JSON from: {0} :: {1}".format(url, e))
self.log.error("Weather: Error processing JSON from: {0} :: {1}".format(url, e))
irc.error("Could not process JSON from Weather Underground. Check the logs.", Raise=True)
# 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.error("I got an error searching '{0}'. ({1}: {2})".format(loc, errortype, errordesc), Raise=True)
# if there is more than one city matching (Ambiguous Results). we now go with the first (best?) match.
# this should no longer be the case with our autocomplete routine above but we'll keep this anyways.
if 'results' in data['response']: # we grab the first location's "ZMW" which then gets constructed as location.
first = 'zmw:%s' % data['response']['results'][0]['zmw'] # grab the "first" location and create the
# grab this first location and search again.
page = self._wunderjson(url, first)
if not page:
irc.error("Failed to load Wunderground API.", Raise=True)
# we're here if we got the second search (best?) now lets reload the json and continue.
data = json.loads(page.decode('utf-8'))
outdata = {'weather': data['current_observation']['weather'],
'location': data['current_observation']['display_location']['full'],
'humidity': data['current_observation']['relative_humidity'],
'uv': data['current_observation']['UV']}
# 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']
outdata['humidity'] = data['current_observation']['relative_humidity']
outdata['uv'] = data['current_observation']['UV']
# handle wind. check if there is none first.
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.
else:
if args['imperial']:
outdata['wind'] = "{0}@{1}mph".format(self._wind(data['current_observation']['wind_degrees']), data['current_observation']['wind_mph'])
if int(data['current_observation']['wind_gust_mph']) > 0: # gusts?
if int(data['current_observation']['wind_gust_mph']) > 0:
outdata['wind'] += " ({0}mph gusts)".format(data['current_observation']['wind_gust_mph'])
else: # handle metric units for wind.
else:
outdata['wind'] = "{0}@{1}kph".format(self._wind(data['current_observation']['wind_degrees']),data['current_observation']['wind_kph'])
if int(data['current_observation']['wind_gust_kph']) > 0: # gusts?
if int(data['current_observation']['wind_gust_kph']) > 0:
outdata['wind'] += " ({0}kph gusts)".format(data['current_observation']['wind_gust_kph'])
# handle the time. concept/method from WunderWeather plugin.
@ -565,118 +447,55 @@ class Weather(callbacks.Plugin):
outdata['observation'] = '1hr ago'
else:
outdata['observation'] = '{0}hrs ago'.format(s/3600)
# handle basics like temp/pressure/dewpoint. big conditional here
# as we can display Imperial + Metric, or one or the other.
if args['showImperialAndMetric']:
# lets put C and F into strings to make it easier.
tf = str(data['current_observation']['temp_f']) + 'F'
tc = str(data['current_observation']['temp_c']) + 'C'
outdata['temp'] = "{0}/{1}".format(self._tw(args['nocolortemp'], tf), self._tw(args['nocolortemp'], tc))
# now lets do pressure.
pin = str(data['current_observation']['pressure_in']) + 'in'
pmb = str(data['current_observation']['pressure_mb']) + 'mb'
outdata['pressure'] = "{0}/{1}".format(pin, pmb)
# dewpoint.
dpf = str(data['current_observation']['dewpoint_f']) + 'F'
dpc = str(data['current_observation']['dewpoint_c']) + 'C'
outdata['dewpoint'] = "{0}/{1}".format(self._tw(args['nocolortemp'], dpf), self._tw(args['nocolortemp'], dpc))
# heatindex.
hif = str(data['current_observation']['heat_index_f']) + 'F'
hic = str(data['current_observation']['heat_index_c']) + 'C'
outdata['heatindex'] = "{0}/{1}".format(self._tw(args['nocolortemp'], hif), self._tw(args['nocolortemp'], hic))
# windchill.
wcf = str(data['current_observation']['windchill_f']) + 'F'
wcc = str(data['current_observation']['windchill_c']) + 'C'
outdata['windchill'] = "{0}/{1}".format(self._tw(args['nocolortemp'], wcf), self._tw(args['nocolortemp'], wcc))
# feels like
flf = str(data['current_observation']['feelslike_f']) + 'F'
flc = str(data['current_observation']['feelslike_c']) + 'C'
outdata['feelslike'] = "{0}/{1}".format(self._tw(args['nocolortemp'], flf), self._tw(args['nocolortemp'], flc))
# visibility.
vmi = str(data['current_observation']['visibility_mi']) + 'mi'
vkm = str(data['current_observation']['visibility_km']) + 'km'
outdata['visibility'] = "{0}/{1}".format(vmi, vkm)
else: # don't display both (default)
if args['imperial']: # assigns the symbol based on metric.
outdata['temp'] = self._tw(args['nocolortemp'], str(data['current_observation']['temp_f']) + 'F')
outdata['pressure'] = str(data['current_observation']['pressure_in']) + 'in'
outdata['dewpoint'] = self._tw(args['nocolortemp'], str(data['current_observation']['dewpoint_f']) + 'F')
outdata['heatindex'] = self._tw(args['nocolortemp'], str(data['current_observation']['heat_index_f']) + 'F')
outdata['windchill'] = self._tw(args['nocolortemp'], str(data['current_observation']['windchill_f']) + 'F')
outdata['feelslike'] = self._tw(args['nocolortemp'], str(data['current_observation']['feelslike_f']) + 'F')
outdata['visibility'] = str(data['current_observation']['visibility_mi']) + 'mi'
else: # metric.
outdata['temp'] = self._tw(args['nocolortemp'], str(data['current_observation']['temp_c']) + 'C')
outdata['pressure'] = str(data['current_observation']['pressure_mb']) + 'mb'
outdata['dewpoint'] = self._tw(args['nocolortemp'], str(data['current_observation']['dewpoint_c']) + 'C')
outdata['heatindex'] = self._tw(args['nocolortemp'], str(data['current_observation']['heat_index_c']) + 'C')
outdata['windchill'] = self._tw(args['nocolortemp'], str(data['current_observation']['windchill_c']) + 'C')
outdata['feelslike'] = self._tw(args['nocolortemp'], str(data['current_observation']['feelslike_c']) + 'C')
outdata['visibility'] = str(data['current_observation']['visibility_km']) + 'km'
outdata['temp'] = self._temp(data['current_observation']['temp_f'])
# pressure.
pin = str(data['current_observation']['pressure_in']) + 'in'
pmb = str(data['current_observation']['pressure_mb']) + 'mb'
outdata['pressure'] = "{0}/{1}".format(pin, pmb)
# dewpoint.
outdata['dewpoint'] = self._temp(data['current_observation']['dewpoint_f'])
# heatindex.
outdata['heatindex'] = self._temp(data['current_observation']['heat_index_f'])
# windchill.
outdata['windchill'] = self._temp(data['current_observation']['windchill_f'])
# feels like
outdata['feelslike'] = self._temp(data['current_observation']['feelslike_f'])
# visibility.
vmi = str(data['current_observation']['visibility_mi']) + 'mi'
vkm = str(data['current_observation']['visibility_km']) + 'km'
outdata['visibility'] = "{0}/{1}".format(vmi, vkm)
# 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']: # imperial.
tmpdict['text'] = forecastday['fcttext']
else: # metric.
tmpdict['text'] = forecastday['fcttext_metric']
forecastdata[int(forecastday['period'])] = tmpdict
if args['imperial']:
text = forecastday['fcttext']
else:
text = forecastday['fcttext_metric']
forecastdata[int(forecastday['period'])] = {'day': forecastday['title'],
'text': text}
# now this is the --forecast part.
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']: # imperial.
tmpdict['high'] = forecastday['high']['fahrenheit'] + "F"
tmpdict['low'] = forecastday['low']['fahrenheit'] + "F"
else: # metric.
tmpdict['high'] = forecastday['high']['celsius'] + "C"
tmpdict['low'] = forecastday['low']['celsius'] + "C"
fullforecastdata[int(forecastday['period'])] = tmpdict
# handle almanac
if args['almanac']:
outdata['highyear'] = data['almanac']['temp_high'].get('recordyear', 'NA')
outdata['lowyear'] = data['almanac']['temp_low'].get('recordyear', 'NA')
if args['imperial']: # imperial.
outdata['highnormal'] = data['almanac']['temp_high']['normal']['F'] + "F"
outdata['lownormal'] = data['almanac']['temp_low']['normal']['F'] + "F"
if outdata['highyear'] != "NA" and outdata['lowyear'] != "NA":
outdata['highrecord'] = data['almanac']['temp_high']['record']['F']
outdata['lowrecord'] = data['almanac']['temp_low']['record']['F']
else:
outdata['highrecord'] = "NA"
outdata['lowrecord'] = "NA"
else: # metric.
outdata['highnormal'] = data['almanac']['temp_high']['normal']['C'] + "C"
outdata['lownormal'] = data['almanac']['temp_low']['normal']['C'] + "C"
if outdata['highyear'] != "NA" and outdata['lowyear'] != "NA":
outdata['highrecord'] = data['almanac']['temp_high']['record']['C']
outdata['lowrecord'] = data['almanac']['temp_low']['record']['C']
else:
outdata['highrecord'] = "NA"
outdata['lowrecord'] = "NA"
# handle astronomy
if args['astronomy']:
outdata['moonilluminated'] = data['moon_phase']['percentIlluminated']
outdata['moonage'] = data['moon_phase']['ageOfMoon']
sunriseh = data['moon_phase']['sunrise']['hour']
sunrisem = data['moon_phase']['sunrise']['minute']
sunseth = data['moon_phase']['sunset']['hour']
sunsetm = data['moon_phase']['sunset']['minute']
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((((int(sunseth)-int(sunriseh))+float((int(sunsetm)-int(sunrisem))/60.0))*60),60)
output = "{0} :: {1} ::".format(self._bold(outdata['location']), outdata['weather'])
output += " {0} ".format(outdata['temp'])
# humidity.
if args['humidity']:
output += " (Humidity: {0}) ".format(outdata['humidity'])
# windchill/heatindex are conditional on season but test with startswith to see what to include
if not outdata['windchill'].startswith("NA"):
output += "| {0} {1} ".format(self._bold('Wind Chill:'), outdata['windchill'])
if not outdata['heatindex'].startswith("NA"):
output += "| {0} {1} ".format(self._bold('Heat Index:'), outdata['heatindex'])
# now get into the args dict for what to include (extras)
for k in ('wind', 'visibility', 'uv', 'pressure', 'dewpoint'):
if args[k]:
output += "| {0}: {1} ".format(self._bold(k.title()), outdata[k])
# add in the first two forecast item in conditions + updated time.
output += "| {0}: {1}".format(self._bold(forecastdata[0]['day']), forecastdata[0]['text'])
output += " {0}: {1}".format(self._bold(forecastdata[1]['day']), forecastdata[1]['text'])
if args['updated']:
output += " | {0} {1}".format(self._bold('Updated:'), outdata['observation'])
# finally, output the basic weather.
irc.reply(output)
# handle alerts
if args['alerts']: # only look for alerts if there.
@ -686,86 +505,65 @@ class Weather(callbacks.Plugin):
outdata['alerts'] = utils.str.normalizeWhitespace(outdata['alerts']) # fix pesky double whitespacing.
else: # no alerts found (empty).
outdata['alerts'] = "No alerts."
irc.reply("{0} {1}".format(self._bu("Alerts:"), outdata['alerts']))
# OUTPUT.
# we go step-by-step to build the proper string. ° u" \u00B0C"
output = "{0} :: {1} ::".format(self._bold(outdata['location']), outdata['weather'])
# add in temperature.
output += " {0}".format(outdata['temp'])
# humidity.
if args['humidity']: # display humidity?
output += " (Humidity: {0}) ".format(outdata['humidity'])
else:
output += " "
# windchill/heatindex are conditional on season but test with startswith to see what to include
if not outdata['windchill'].startswith("NA"): # windchill.
output += "| {0} {1} ".format(self._bold('Wind Chill:'), outdata['windchill'])
if not outdata['heatindex'].startswith("NA"): # heatindex.
output += "| {0} {1} ".format(self._bold('Heat Index:'), 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, we add it.
output += "| {0}: {1} ".format(self._bold(k.title()), outdata[k])
# add in the first two forecast item in conditions + updated time.
output += "| {0}: {1}".format(self._bold(forecastdata[0]['day']), forecastdata[0]['text'])
output += " {0}: {1}".format(self._bold(forecastdata[1]['day']), forecastdata[1]['text'])
# show Updated?
if args['updated']:
output += " | {0} {1}".format(self._bold('Updated:'), outdata['observation'])
# finally, output the basic weather.
irc.reply(output)
# 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']))
# handle almanac if --almanac is given.
# handle almanac
if args['almanac']:
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: # 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'])
# now output to irc.
try:
outdata['highyear'] = data['almanac']['temp_high'].get('recordyear')
outdata['lowyear'] = data['almanac']['temp_low'].get('recordyear')
outdata['highaverage'] = self._temp(data['almanac']['temp_high']['average']['F'])
outdata['lowaverage'] = self._temp(data['almanac']['temp_low']['average']['F'])
if outdata['highyear'] != "NA" and outdata['lowyear'] != "NA":
outdata['highrecord'] = self._temp(data['almanac']['temp_high']['record']['F'])
outdata['lowrecord'] = self._temp(data['almanac']['temp_low']['record']['F'])
else:
outdata['highrecord'] = outdata['lowrecord'] = "NA"
except KeyError:
output = "%s Not available." % self._bu('Almanac:')
else:
output = ("{0} Average High: {1} (Record: {2} in {3}) | Average Low: {4} (Record: {5} in {6})".format(
self._bu('Almanac:'), outdata['highaverage'], outdata['highrecord'], outdata['highyear'],
outdata['lowaverage'], outdata['lowrecord'], outdata['lowyear']))
irc.reply(output)
# handle astronomy if --astronomy is given.
# handle 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'], outdata['moonage'],outdata['sunrise'],\
outdata['sunset'], outdata['lengthofday'])
# irc output now.
sunriseh = data['moon_phase']['sunrise']['hour']
sunrisem = data['moon_phase']['sunrise']['minute']
sunseth = data['moon_phase']['sunset']['hour']
sunsetm = data['moon_phase']['sunset']['minute']
sunrise = "{0}:{1}".format(sunriseh, sunrisem)
sunset = "{0}:{1}".format(sunseth, sunsetm)
# Oh god, this one-liner... -GLolol
lengthofday = "%dh%dm" % divmod((((int(sunseth)-int(sunriseh))+float((int(sunsetm)-int(sunrisem))/60.0))*60 ),60)
astronomy = {'Moon illum:': str(data['moon_phase']['percentIlluminated']) + "%",
'Moon age:': str(data['moon_phase']['ageOfMoon']) + "d",
'Sunrise:': sunrise,
'Sunset:': sunset,
'Length of Day:': lengthofday}
output = [format('%s %s', self._bold(k), v) for k, v in sorted(astronomy.items())]
output = format("%s %s", self._bu('Astronomy:'), " | ".join(output))
irc.reply(output)
# handle main forecast if --forecast is given.
# handle forecast
if args['forecast']:
fullforecastdata = {} # key = day (int), value = dict of forecast data.
for forecastday in data['forecast']['simpleforecast']['forecastday']:
high = self._temp(forecastday['high']['fahrenheit'])
low = self._temp(forecastday['low']['fahrenheit'])
tmpdict = {'day': forecastday['date']['weekday_short'],
'symbol': self._weatherSymbol(forecastday['icon']),
'text': forecastday['conditions'],
'low': low,
'high': high}
fullforecastdata[int(forecastday['period'])] = tmpdict
outforecast = [] # prep string for output.
for (k, v) in fullforecastdata.items(): # iterate through forecast data.
if args['nocolortemp']:
outforecast.append("{0}: {1} ({2}/{3})".format(self._bold(v['day']),\
outforecast.append("{0}: {1} (High: {2} Low: {3})".format(self._bold(v['day']),
v['text'], 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'])))
# construct our string to output.
output = "{0} :: {1}".format(self._bu('Forecast:'), " | ".join(outforecast))
# now output to irc.
output = "{0} {1}".format(self._bu('Forecast:'), " | ".join(outforecast))
irc.reply(output)
wunderground = wrap(wunderground, [getopts({'alerts':'',
'almanac':'',
'astronomy':'',
'forecast':'',
'pressure':'',
'wind':'',
'uv':'',
'visibility':'',
'dewpoint':'',
'humidity':'',
'metric':'',
'nocolortemp':'',
'help':''}), optional('text')])
wunderground = wrap(wunderground, [optional('text')])
Class = Weather

View File

@ -13,19 +13,18 @@ class WeatherTestCase(PluginTestCase):
def setUp(self):
PluginTestCase.setUp(self)
apiKey = os.environ.get('apiKey')
apiKey = os.environ.get('weather_apikey')
if not apiKey:
e = """The Wunderground API key has not been set.
please set this value correctly and try again:
'export apiKey=<key>' for bash users"""
please set this value correctly via the environment variable
"weather_apikey"."""
raise Exception(e)
conf.supybot.plugins.Weather.apiKey.setValue(apiKey)
def testWeather(self):
self.assertSnarfResponse('reload Weather', 'The operation succeeded.')
self.assertRegexp('wunderground 10002', 'New York, NY')
self.assertSnarfResponse('setweather 10002', "I have changed test's weather ID to 10002")
self.assertSnarfResponse('setuser metric True', "I have changed test's metric setting to 1")
self.assertNotError('setweather 10002')
self.assertNotError('setuser metric True')
self.assertRegexp('wunderground', 'New York, NY')
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: