Add wunderground module

This module tries to use as much of the same variable naming conventions
that the ``weather`` module uses as possible, to make transitioning
easier in the future in case we decide to make a base class for all
modules which provide weather data.

An API key is required to use this module, information on obtaining one
can be found in the docstring.
This commit is contained in:
Erik Johnson 2016-03-14 01:35:31 -05:00
parent 9e3e7a6bc6
commit 23747d8181

219
i3pystatus/wunderground.py Normal file
View File

@ -0,0 +1,219 @@
from i3pystatus import IntervalModule
from i3pystatus.core.util import user_open, internet, require
from datetime import datetime
from urllib.request import urlopen
import json
import re
GEOLOOKUP_URL = 'http://api.wunderground.com/api/%s/geolookup%s/q/%s.json'
STATION_LOOKUP_URL = 'http://api.wunderground.com/api/%s/conditions/q/%s.json'
class Wunderground(IntervalModule):
'''
This module retrieves weather from the Weather Underground API.
.. note::
A Weather Underground API key is required to use this module, you can
sign up for one for a developer API key free at
https://www.wunderground.com/weather/api/
A developer API key is allowed 500 queries per day.
Valid values for ``location_code`` include:
* **State/City_Name** - CA/San_Francisco
* **Country/City** - France/Paris
* **Geolocation by IP** - autoip
* **Zip or Postal Code** - 60616
* **ICAO Airport Code** - icao:LAX
* **Latitude/Longitude** - 41.8301943,-87.6342619
* **Personal Weather Station (PWS)** - pws:KILCHICA30
When not using a PWS, the location will be queried, and the closest
station will be used. When possible, it is recommended to use a PWS
location, as this will result in fewer API calls.
.. rubric:: Available formatters
* `{city}` Location of weather observation
* `{conditon}` Current condition (Rain, Snow, Overcast, etc.)
* `{observation_time}` Time of weather observation (supports strftime format flags)
* `{current_temp}` Current temperature, excluding unit
* `{degrees}` ``°C`` if ``units`` is set to ``metric``, otherwise ``°F``
* `{feelslike}` Wunderground "Feels Like" temperature, excluding unit
* `{current_wind}` Wind speed in mph/kph, excluding unit
* `{current_wind_direction}` Wind direction
* `{current_wind_gust}` Speed of wind gusts in mph/kph, excluding unit
* `{pressure_in}` Barometric pressure (in inches), excluding unit
* `{pressure_mb}` Barometric pressure (in millibars), excluding unit
* `{pressure_trend}` ``+`` (rising) or ``-`` (falling)
* `{visibility}` Visibility in mi/km, excluding unit
* `{humidity}` Current humidity, excluding percentage symbol
* `{dewpoint}` Dewpoint temperature, excluding unit
* `{uv_index}` UV Index
'''
interval = 300
settings = (
('api_key', 'Weather Underground API key'),
('location_code', 'Location code from www.weather.com'),
('units', 'Celsius (metric) or Fahrenheit (imperial)'),
('use_pws', 'Set to False to use only airport stations'),
('error_log', 'If set, tracebacks will be logged to this file'),
'format',
)
required = ('api_key', 'location_code')
api_key = None
location_code = None
units = "metric"
format = "{current_temp}{degrees}"
use_pws = True
error_log = None
station_id = None
forecast_url = None
on_leftclick = 'open_wunderground'
def open_wunderground(self):
'''
Open the forecast URL, if one was retrieved
'''
if self.forecast_url and self.forecast_url != 'N/A':
user_open(self.forecast_url)
def api_request(self, url):
'''
Execute an HTTP POST to the specified URL and return the content
'''
with urlopen(url) as content:
try:
content_type = dict(content.getheaders())['Content-Type']
charset = re.search(r'charset=(.*)', content_type).group(1)
except AttributeError:
charset = 'utf-8'
response = json.loads(content.read().decode(charset))
try:
raise Exception(response['response']['error']['description'])
except KeyError:
pass
return response
def geolookup(self):
'''
Use the location_code to perform a geolookup and find the closest
station. If the location is a pws or icao station ID, no lookup will be
peformed.
'''
if self.station_id is None:
try:
for no_lookup in ('pws', 'icao'):
sid = self.location_code.partition(no_lookup + ':')[-1]
if sid:
self.station_id = self.location_code
return
except AttributeError:
# Numeric or some other type, either way we'll just stringify
# it below and perform a lookup.
pass
extra_opts = '/pws:0' if not self.use_pws else ''
api_url = GEOLOOKUP_URL % (self.api_key,
extra_opts,
self.location_code)
response = self.api_request(api_url)
station_type = 'pws' if self.use_pws else 'airport'
try:
stations = response['location']['nearby_weather_stations']
nearest = stations[station_type]['station'][0]
except (KeyError, IndexError):
raise Exception('No locations matched location_code %s'
% self.location_code)
if self.use_pws:
nearest_pws = nearest.get('id', '')
if not nearest_pws:
raise Exception('No id entry for station')
self.station_id = 'pws:%s' % nearest_pws
else:
nearest_airport = nearest.get('icao', '')
if not nearest_airport:
raise Exception('No icao entry for station')
self.station_id = 'icao:%s' % nearest_airport
def query_station(self):
'''
Query a specific station
'''
# If necessary, do a geolookup to set the station_id
self.geolookup()
query_url = STATION_LOOKUP_URL % (self.api_key, self.station_id)
try:
response = self.api_request(query_url)['current_observation']
self.forecast_url = response.pop('forecast_url', None)
except KeyError:
raise Exception('No weather data found for %s' % self.station_id)
def _find(key, data=None):
data = data or response
return data.get(key, 'N/A')
if self.units == 'metric':
temp_unit = 'c'
speed_unit = 'kph'
distance_unit = 'km'
else:
temp_unit = 'f'
speed_unit = 'mph'
distance_unit = 'mi'
try:
observation_time = int(_find('observation_epoch'))
except TypeError:
observation_time = 0
return dict(
forecast_url=_find('forecast_url'),
city=_find('city', response['observation_location']),
condition=_find('weather'),
observation_time=datetime.fromtimestamp(observation_time),
current_temp=_find('temp_' + temp_unit),
feelslike=_find('feelslike_' + temp_unit),
current_wind=_find('wind_' + speed_unit),
current_wind_direction=_find('wind_dir'),
current_wind_gust=_find('wind_gust_' + speed_unit),
pressure_in=_find('pressure_in'),
pressure_mb=_find('pressure_mb'),
pressure_trend=_find('pressure_trend'),
visibility=_find('visibility_' + distance_unit),
humidity=_find('relative_humidity').rstrip('%'),
dewpoint=_find('dewpoint_' + temp_unit),
uv_index=_find('uv'),
)
@require(internet)
def run(self):
try:
result = self.query_station()
except Exception as exc:
if self.error_log:
import traceback
with open(self.error_log, 'a') as f:
f.write('%s : An exception was raised:\n' %
datetime.isoformat(datetime.now()))
f.write(''.join(traceback.format_exc()))
f.write(80 * '-' + '\n')
raise
result['degrees'] = '°%s' % ('C' if self.units == 'metric' else 'F')
self.output = {
"full_text": self.format.format(**result),
# "color": self.color # TODO: add some sort of color effect
}