diff --git a/README.md b/README.md index ee86617..39634f7 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ Please note that this list may not always be up to date; your best bet is to act Most of these plugins also have their own READMEs in their folders; you can usually find a usage demonstration or further explanation of what they do. +##### AQI +- Retrieves [air quality index](https://en.wikipedia.org/wiki/Air_quality_index) info from the [World Air Quality Index project](https://aqicn.org). + ##### CtcpNext - Alternative to the official Ctcp plugin, with a database for configurable replies. @@ -104,10 +107,6 @@ Most of these plugins also have their own READMEs in their folders; you can usua ##### Voteserv - A plugin for storing and manipulating votes/polls. -##### [Weather](Weather/README.md) **[DEPRECATED]** -- My fork of [reticulatingspline's Weather](https://github.com/reticulatingspline/Weather) plugin, with rewritten output handling, explicit location search, and many other tweaks. -- **Update 2018012**: Weather Underground is shutting down free weather access, so this plugin will no longer be maintained. See the NuWeather plugin in this folder for an alternative using other backends. - ##### Wikifetch - Fork of [ProgVal's Wikipedia plugin](https://github.com/ProgVal/Supybot-plugins), with support for other wikis (via a `--site` option) and other improvements. - **Requires:** [lxml](https://lxml.de/installation.html) diff --git a/Weather/.gitignore b/Weather/.gitignore deleted file mode 100644 index d2d6f36..0000000 --- a/Weather/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox -nosetests.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject diff --git a/Weather/.travis.yml b/Weather/.travis.yml deleted file mode 100644 index 2eeba48..0000000 --- a/Weather/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -sudo: false -language: python -python: - - "2.7" - - "3.2" - - "3.3" - - "3.4" - - "3.5" - - "3.5-dev" - - pypy - - pypy3 -# command to install dependencies, -install: - - pip install -vr requirements.txt -# command to run tests, e.g. python setup.py test -script: - - cd .. && mv Supybot-Weather Weather - - supybot-test Weather -notifications: - email: false -matrix: - fast_finish: true - allow_failures: - - python: "pypy" - - python: "pypy3" diff --git a/Weather/LICENSE.txt b/Weather/LICENSE.txt deleted file mode 100644 index 83d7492..0000000 --- a/Weather/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -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 -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. diff --git a/Weather/README.md b/Weather/README.md index 3891fcb..1e03037 100644 --- a/Weather/README.md +++ b/Weather/README.md @@ -1,62 +1,3 @@ -# [DEPRECATED] Limnoria plugin for Weather Underground +The Weather plugin is no longer supported as Weather Underground has ceased providing free API access. -Update 201903: Weather Underground has shut down free weather access, so this plugin will no longer be maintained. See the [NuWeather](../NuWeather) plugin for an alternative using other backends. - -## Installation - -1) Download the plugin, either via Git or Limnoria's PluginDownloader (`install GLolol Weather`). - -2) Load the plugin: - -``` -/msg bot load Weather -``` - -3) [Fetch an API key from Wunderground](http://www.wunderground.com/weather/api/) by signing up (free). -Once you get this key, you will need to set it up on your bot: - -``` -/msg config plugins.Weather.apiKey -``` - -## Usage - -When calling the `weather` command, you can provide locations in one of many forms: -- City names (e.g. Vancouver) -- U.S. ZIP codes (e.g. 10002) -- Canadian, U.K. postal codes (e.g. V6C 3T4) -- City, country pairs (e.g. "Sydney, Australia", "Paris, France") -- City, state/province pairs (e.g. "Washington, D.C.", "Kitchener, Ontario") -- [ICAO airport codes](https://en.wikipedia.org/wiki/International_Civil_Aviation_Organization_airport_code) (e.g. KJFK) - -Example: - -``` - @weather 10002 - New York, NY :: Mostly Cloudy :: 55F/12C (Humidity: 53%) | Monday: Mostly cloudy. Low 11C. Monday Night: Cloudy. Slight chance of a rain shower. Low 11C. Winds ENE at 10 to 15 km/h. -``` - -### Saving locations - -Users can also have their location remembered by the bot so that they don't have to continually type in their location. - -``` - @setweather 10002 - Done. -``` - -This allows a user to use the `weather` command without any arguments: - -``` - @weather - New York, NY :: Clear :: 64F/17C | Wind: N@7kph | Thursday: Clear. Low 14C. Thursday Night: A clear sky. Low 14C. Winds SSE at 10 to 15 km/h. -``` - -### User options - -Users can also have the bot remember their preferred options, such as using metric units when displaying forecasts: - -``` - @setuser metric True - Done. -``` +Please consider using migrating to an alternative such as https://github.com/jlu5/SupyPlugins/tree/master/NuWeather diff --git a/Weather/__init__.py b/Weather/__init__.py index 6c5aa68..8f5c477 100644 --- a/Weather/__init__.py +++ b/Weather/__init__.py @@ -1,45 +1,5 @@ -### -# Copyright (c) 2012-2014, spline -# All rights reserved. -### +#!/usr/bin/env python3 +from supybot import callbacks -""" -Add a description of the plugin (to be presented to the user inside the wizard) -here. This should describe *what* the plugin does. -""" - -import supybot -import supybot.world as world - -# Use this for the version of this plugin. You may wish to put a CVS keyword -# in here if you're keeping the plugin in CVS or some similar system. -__version__ = "2019.03.03+git" - -# XXX Replace this with an appropriate author or supybot.Author instance. -__author__ = supybot.Author('James Lu', 'GLolol', 'GLolol@overdrivenetworks.com') - -# This is a dictionary mapping supybot.Author instances to lists of -# contributions. -__contributors__ = {} - -# This is a url where the most recent plugin package can be downloaded. -__url__ = 'https://github.com/GLolol/SupyPlugins/' - -from . import config -from . import plugin -from imp import reload -# In case we're being reloaded. -reload(config) -reload(plugin) - -# Add more reloads here if you add third-party modules and want them to be -# reloaded when this plugin is reloaded. Don't forget to import them as well! - -if world.testing: - from . import test - -Class = plugin.Class -configure = config.configure - - -# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: +raise callbacks.Error("The Weather plugin is no longer supported as Weather Underground has ceased providing free API access. " + "Please consider using migrating to an alternative such as https://github.com/jlu5/SupyPlugins/tree/master/NuWeather") diff --git a/Weather/config.py b/Weather/config.py deleted file mode 100644 index 2869178..0000000 --- a/Weather/config.py +++ /dev/null @@ -1,44 +0,0 @@ -### -# Copyright (c) 2012-2014, spline -# All rights reserved. -### - -import supybot.conf as conf -import supybot.registry as registry -from supybot.i18n import PluginInternationalization, internationalizeDocstring - -_ = PluginInternationalization('Weather') - -def configure(advanced): - # This will be called by supybot to configure this module. advanced is - # a bool that specifies whether the user identified himself as an advanced - # user or not. You should effect your configuration by manipulating the - # registry as appropriate. - from supybot.questions import expect, anything, something, yn - conf.registerPlugin('Weather', True) - -Weather = conf.registerPlugin('Weather') -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."""))) -conf.registerChannelValue(Weather, 'disableColoredTemp', - registry.Boolean(False, """If True, this will disable coloring temperatures based on values.""")) - -# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250: diff --git a/Weather/local/__init__.py b/Weather/local/__init__.py deleted file mode 100644 index e86e97b..0000000 --- a/Weather/local/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Stub so local is a module, used for third-party modules diff --git a/Weather/plugin.py b/Weather/plugin.py deleted file mode 100644 index 4244f98..0000000 --- a/Weather/plugin.py +++ /dev/null @@ -1,628 +0,0 @@ -# -*- coding: utf-8 -*- -### -# Copyright (c) 2012-2014, spline -# Copyright (c) 2014-2018, 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. -### - - -from __future__ import unicode_literals -import json -from math import floor -import sqlite3 -import string -try: - from itertools import izip -except ImportError: # Python 3 - izip = zip - -import supybot.conf as conf -import supybot.log as log -import supybot.utils as utils -from supybot.commands import * -import supybot.plugins as plugins -import supybot.ircutils as ircutils -import supybot.callbacks as callbacks -try: - from supybot.i18n import PluginInternationalization - _ = PluginInternationalization('Weather') -except ImportError: - # Placeholder that allows to run the plugin on a bot - # without the i18n module - _ = lambda x:x - - -class WeatherDB(): - """WeatherDB class to store our users and their settings.""" - - 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 makeDb(self): - """Create our 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, - alerts INTEGER DEFAULT 0, - almanac INTEGER DEFAULT 0, - astronomy INTEGER DEFAULT 0, - forecast INTEGER DEFAULT 0, - pressure INTEGER DEFAULT 0, - wind INTEGER DEFAULT 0, - uv INTEGER DEFAULT 0, - visibility INTEGER DEFAULT 0, - dewpoint INTEGER DEFAULT 0, - humidity INTEGER DEFAULT 0, - updated INTEGER DEFAULT 0)""") - self._conn.commit() # this fails silently if already there. - # next, we see if we need to upgrade the old table structure. - 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("Weather: Upgrading database version.") - columns = ['alerts', 'almanac', 'astronomy', 'forecast', 'pressure', 'wind', 'uv', 'visibility', 'dewpoint', 'humidity', 'updated'] - for column in columns: - try: - cursor.execute('ALTER TABLE users ADD COLUMN %s INTEGER DEFAULT 0' % column) - self._conn.commit() - except: # fail silently. - pass - - 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 setsetting(self, username, setting, value): - """Set one of the user settings.""" - - with self._conn as conn: - cursor = conn.cursor() - query = "UPDATE users SET %s=? WHERE nick=?" % setting - cursor.execute(query, (value, username,)) - self._conn.commit() - - def getsettings(self): - """Get all 'user' settings that can be set.""" - - with self._conn as conn: - cursor = conn.cursor() # below, we get all column names that are settings (INTEGERS) - settings = [str(l[1]) for l in cursor.execute("pragma table_info('users')").fetchall() if l[2] == "INTEGER"] - return settings - - 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. - rowdict = dict(izip(row.keys(), row)) - return rowdict - - 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 - - -class WeatherAPIError(RuntimeError): - pass - -class Weather(callbacks.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.db = WeatherDB() - - ############## - # FORMATTING # - ############## - - def _bold(self, string): - return ircutils.bold(string) - - def _bu(self, string): - return ircutils.underline(ircutils.bold(string)) - - ############################ - # INTERNAL WEATHER HELPERS # - ############################ - - def _temp(self, channel, 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 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) - s = "{0}F/{1}C".format(f, c) - # determine color. - if not self.registryValue('disableColoredTemp', channel): - if f < 10.0: - color = 'light blue' - elif 10.0 <= f <= 32.0: - color = 'teal' - elif 32.1 <= f <= 50.0: - color = 'blue' - elif 50.1 <= f <= 60.0: - color = 'light green' - elif 60.1 <= f <= 70.0: - color = 'green' - elif 70.1 <= f <= 80.0: - color = 'yellow' - elif 80.1 <= f <= 90.0: - color = 'orange' - elif f > 90.0: - color = 'red' - else: - color = 'light grey' - s = ircutils.mircColor(s, color) - # return. - return s - except (TypeError, ValueError) as e: - self.log.info("Weather: ValueError trying to convert temp: {0} message: {1}".format(f, e)) - return "N/A" - - def _wind(self, angle, useSymbols=False): - """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] - - @staticmethod - def _format_geolookup_name(result): - """Formats a place name from Wunderground Geolookup.""" - if result['state'] and not result['state'].isdigit(): - template = '{city}, {state}, {country_name}' - else: - template = '{city}, {country_name}' - return template.format(**result) - - ############################################## - # PUBLIC FUNCTIONS TO WORK WITH THE DATABASE # - ############################################## - - def setuser(self, irc, msg, args, optset, optbool): - """ - - Sets a user's to True or False. - Valid settings include: alerts, almanac, astronomy, forecast, pressure, - wind, uv, visibility, dewpoint, humidity, and updated. - """ - - # first, lower - optset = optset.lower() - # grab a list of valid settings. - validset = self.db.getsettings() - if optset not in validset: - 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 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.replySuccess() - - setuser = wrap(setuser, [('somethingWithoutSpaces'), ('boolean')]) - - def setweather(self, irc, msg, args, optlocation): - """ - - Sets the weather location for your nick. Location codes can be city names, "City, Country" - pairs, ICAO airport codes, US ZIP codes, or raw zmw codes as returned by the - 'locationsearch' command. - """ - self.db.setweather(msg.nick.lower(), optlocation) - irc.replySuccess() - - setweather = wrap(setweather, [('text')]) - - ########################## - # WUNDERGROUND API CALLS # - ########################## - - def _wuac(self, q, return_names=False): - """Internal helper to find locations via Wunderground's GeoLookup API. - Previous versions of this plugin used the Autocompete API instead.""" - - if q.startswith('zmw:'): - # If we're given a ZMW code, just return it as is. - return [q] - - apikey = self.registryValue('apiKey') - if not apikey: - raise callbacks.Error("No Wunderground API key was defined; set " - "the 'plugins.Weather.apiKey' config variable.") - - url = 'http://api.wunderground.com/api/%s/geolookup/q/%s.json' % (apikey, utils.web.urlquote(q)) - self.log.debug("Weather: GeoLookup URL %s", url) - page = utils.web.getUrl(url, timeout=5) - data = json.loads(page.decode('utf-8')) - - if data.get('location'): - # This form is used when there's only one result. - zmw = 'zmw:{zip}.{magic}.{wmo}'.format(**data['location']) - if return_names: - name = self._format_geolookup_name(data['location']) - return [(name, zmw)] - else: - return [zmw] - else: - if data['response'].get('error'): - errdata = data['response']['error'] - raise WeatherAPIError('Error in _wuac step: [%s] %s' % - (errdata.get('type', 'N/A'), - errdata.get('description', 'No message specified'))) - # This form of result is returned there are multiple places matching a query - results = data['response'].get('results') - if not results: - return [] - - if return_names: - results = [(self._format_geolookup_name(result), 'zmw:' + result['zmw']) for result in results] - else: - results = [('zmw:' + result['zmw']) for result in results] - return results - - - #################### - # PUBLIC FUNCTIONS # - #################### - - @wrap([getopts({'user': 'nick'}), optional('text')]) - def weather(self, irc, msg, args, optlist, location): - """[--user ] [] - - Fetches weather and forecast information for . can be left blank if you have a previously set location (via 'setweather'). - - If the --user option is specified, show weather for the saved location of that nick, instead of the caller. - - Location can take many forms, including a simple city name, US state/city (CA/San_Francisco), zip code, country/city (Australia/Sydney), or an airport code (KJFK). - Ex: 10021 or Sydney, Australia or KJFK - """ - apikey = self.registryValue('apiKey') - if not apikey: - irc.error("No Wunderground API key was defined; set the 'plugins.Weather.apiKey' config variable.", - Raise=True) - channel = msg.args[0] - - optlist = dict(optlist) - # Default to looking at the caller's saved info, but optionally they can look at someone else's weather too. - nick = optlist.get('user') or msg.nick - - # urlargs will be used to build the url to query the API. - # besides lang, these are preset values that should not be changed. - urlArgs = {'features': ['conditions', 'forecast'], - 'lang': self.registryValue('lang'), - 'bestfct': '1', - 'pws': '0' } - - loc = None - 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} - - usersetting = self.db.getweather(nick.lower()) - if usersetting: - for (k, v) in usersetting.items(): - args[k] = v - # Prefer the location given in the command, falling back to the one stored in the DB if not given. - location = location or usersetting["location"] - args['imperial'] = (not usersetting["metric"]) - # If both command line and DB locations aren't given, bail. - if not location: - if nick != msg.nick: - irc.error("I did not find a preset location for %s." % nick, Raise=True) - else: - irc.error("I did not find a preset location for you. Set one via 'setweather '.", Raise=True) - - loc = self._wuac(location) - if not loc: - irc.error("Failed to find a valid location for: %r" % location, Raise=True) - else: - # Use the first location. - loc = loc[0] - - for check in ['alerts', 'almanac', 'astronomy']: - if args[check]: - urlArgs['features'].append(check) # append to dict->key (list) - - baseurl = 'http://api.wunderground.com/api/%s/' % apikey - - # Prepare API options - for (key, value) in urlArgs.items(): - if key == "features": # will always be at least conditions. - # Join features directly to the URL - baseurl += "/".join(value) - baseurl += "/" - if key in ("lang", "bestfct", "pws"): - # Preset and configured (only lang) options, added with key:value - baseurl += "{0}:{1}/".format(key, value) - - url = '%s/q/%s.json' % (baseurl.rstrip('/'), loc) - self.log.debug("Weather URL: {0}".format(url)) - page = utils.web.getUrl(url, timeout=5) - data = json.loads(page.decode('utf-8')) - - if data['response'].get('error'): - errdata = data['response']['error'] - raise WeatherAPIError('Error in weather step: [%s] %s' % - (errdata.get('type', 'N/A'), - errdata.get('description', 'No message specified'))) - elif 'current_observation' not in data: - irc.error("Failed to fetch current conditions for %r." % loc, Raise=True) - - outdata = {'weather': data['current_observation']['weather'], - 'location': data['current_observation']['display_location']['full'], - 'humidity': data['current_observation']['relative_humidity'], - 'uv': data['current_observation']['UV']} - - if data['current_observation']['wind_mph'] < 1: # no wind. - outdata['wind'] = "None" - 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: - outdata['wind'] += " ({0}mph gusts)".format(data['current_observation']['wind_gust_mph']) - 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: - outdata['wind'] += " ({0}kph gusts)".format(data['current_observation']['wind_gust_kph']) - - # Show the last updated time if available. - observationTime = data['current_observation'].get('observation_epoch') - localTime = data['current_observation'].get('local_epoch') - - if not observationTime or not localTime: - outdata['observation'] = data.get('observation_time', 'unknown').lstrip('Last Updated on ') - else: # Prefer relative times, if available - s = int(localTime) - int(observationTime) # format into seconds. - if s <= 1: - outdata['observation'] = 'just now' - elif s < 60: - outdata['observation'] = '{0}s ago'.format(s) - elif s < 120: - outdata['observation'] = '1m ago' - elif s < 3600: - outdata['observation'] = '{0}m ago'.format(s/60) - elif s < 7200: - outdata['observation'] = '1hr ago' - else: - outdata['observation'] = '{0}hrs ago'.format(s/3600) - - outdata['temp'] = self._temp(channel, 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(channel, data['current_observation']['dewpoint_f']) - - # heatindex. - outdata['heatindex'] = self._temp(channel, data['current_observation']['heat_index_f']) - - # windchill. - outdata['windchill'] = self._temp(channel, data['current_observation']['windchill_f']) - - # feels like - outdata['feelslike'] = self._temp(channel, 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. This is internally stored as a dict with integer keys (days from now) - # with the forecast text as values. - forecastdata = {} - if 'forecast' in data: - for forecastday in data['forecast']['txt_forecast']['forecastday']: - # Slightly different wording and results (e.g. rainfall for X inches vs. X cm) are given - # depending on whether imperial or metric units are the same. - if args['imperial']: - text = forecastday['fcttext'] - else: - text = forecastday['fcttext_metric'] - forecastdata[int(forecastday['period'])] = {'day': forecastday['title'], - 'text': text} - - 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 - # NA means not available, so ignore those fields - 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']) - - # Iterate over the args dict for what extra data to include - for k in ('wind', 'visibility', 'uv', 'pressure', 'dewpoint'): - if args[k]: - output += "| {0}: {1} ".format(self._bold(k.title()), outdata[k]) - - if forecastdata: - # Add in the first two forecasts item in conditions + the "last 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']: - # Round updated time (given as a string) to the nearest unit. - # This is annoying because Wunderground sends these as raw strings, in the form - # "1hr ago" or "2.7666666666666666m ago" - tailstr = outdata['observation'].lstrip(string.digits + '.') - updated_time = outdata['observation'].rstrip(string.ascii_letters + ' ') - try: - updated_time = round(float(updated_time)) - except ValueError: - pass - output += " | Updated %s%s" % (ircutils.bold(updated_time), tailstr) - - # finally, output the basic weather. - irc.reply(output) - - # handle alerts - everything here and below sends as separate replies if enabled - if args['alerts'] and data['alerts']: # only look for alerts if enabled and present. - outdata['alerts'] = data['alerts'][0]['message'] # need to do some formatting below. - outdata['alerts'] = outdata['alerts'].replace('\n', ' ') - outdata['alerts'] = utils.str.normalizeWhitespace(outdata['alerts']) # fix pesky double whitespacing. - irc.reply("{0} {1}".format(self._bu("Alerts:"), outdata['alerts'])) - - # handle almanac - if args['almanac']: - try: - outdata['highyear'] = data['almanac']['temp_high'].get('recordyear') - outdata['lowyear'] = data['almanac']['temp_low'].get('recordyear') - outdata['highaverage'] = self._temp(channel, data['almanac']['temp_high']['normal']['F']) - outdata['lowaverage'] = self._temp(channel, data['almanac']['temp_low']['normal']['F']) - if outdata['highyear'] != "NA" and outdata['lowyear'] != "NA": - outdata['highrecord'] = self._temp(channel, data['almanac']['temp_high']['record']['F']) - outdata['lowrecord'] = self._temp(channel, 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 args['astronomy']: - 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 forecast - if args['forecast']: - fullforecastdata = {} # key = day (int), value = dict of forecast data. - for forecastday in data['forecast']['simpleforecast']['forecastday']: - high = self._temp(channel, forecastday['high']['fahrenheit']) - low = self._temp(channel, forecastday['low']['fahrenheit']) - tmpdict = {'day': forecastday['date']['weekday_short'], - '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. - outforecast.append("{0}: {1} (High: {2} Low: {3})".format(self._bold(v['day']), - v['text'], v['high'], v['low'])) - output = "{0} {1}".format(self._bu('Forecast:'), " | ".join(outforecast)) - irc.reply(output) - - @wrap(['text']) - def locationsearch(self, irc, msg, args, text): - """ - - Returns a list of raw Wunderground (ZMW) codes given the search query . This can be - helpful if Wunderground's autocomplete is not picking up the right place, as you can directly - look up weather using any ZMW codes returned here. - - Warning: ZMW codes are not fixed and are prone to sudden changes! - """ - apikey = self.registryValue('apiKey') - if not apikey: - irc.error("No Wunderground API key was defined; set 'config plugins.Weather.apiKey'.", - Raise=True) - - results = self._wuac(text, return_names=True) - if not results: - irc.error("No results found.") - else: - irc.reply(format('%L', ('\x02{0}\x02: {1}'.format(*result) for result in results))) - -Class = Weather - -# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=250: diff --git a/Weather/requirements.txt b/Weather/requirements.txt deleted file mode 100644 index 8ab45f1..0000000 --- a/Weather/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -git+https://github.com/ProgVal/Limnoria.git diff --git a/Weather/test.py b/Weather/test.py deleted file mode 100644 index 4ad35c2..0000000 --- a/Weather/test.py +++ /dev/null @@ -1,66 +0,0 @@ -### -# Copyright (c) 2012-2014, spline -# Copyright (c) 2018, 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 -# 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. -### - -from supybot.test import * -import os - -class WeatherTestCase(PluginTestCase): - plugins = ('Weather',) - - def setUp(self): - PluginTestCase.setUp(self) - apiKey = os.environ.get('weather_apikey') - if not apiKey: - e = """The Wunderground API key has not been set. - please set this value correctly via the environment variable - "weather_apikey".""" - raise Exception(e) - conf.supybot.plugins.Weather.apiKey.setValue(apiKey) - - def testWeatherBasic(self): - self.assertRegexp('weather New York City', 'New York, NY') - self.assertError('weather InvalidLocationTestCasePleaseIgnore') - - def testWeatherUSZIPCode(self): - self.assertRegexp('weather 10002', 'New York, NY') - - def testWeatherAmbiguous(self): - # Returns Albany, NY last time I checked (2017-01-28) - self.assertRegexp('weather New York', ', NY') - # Alturas, CA (2017-01-28) - self.assertRegexp('weather california', ', CA') - # I'll be very upset if this returns the wrong one ;) - self.assertRegexp('weather Vancouver', 'Vancouver, British Columbia') - - def testWeatherAirport(self): - # IATA codes (e.g. YVR, PEK, LAX for these 3) are unreliable and - # sometimes clash with other places - self.assertRegexp('weather CYVR', 'Vancouver International') - self.assertRegexp('weather ZBAA', 'Beijing Capital') - self.assertRegexp('weather KLAX', 'Los Angeles International') - - def testWeatherSavesLocation(self): - self.assertNotError('setweather 10002') - self.assertNotError('setuser metric True') - self.assertRegexp('weather', 'New York, NY') - -# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: