Rework wunderground module as a backend of i3pystatus.weather

This commit is contained in:
Erik Johnson 2016-03-27 22:14:04 -05:00
parent abf5b6ad1c
commit f3f2b59c5b

View File

@ -1,5 +1,5 @@
from i3pystatus import IntervalModule from i3pystatus import IntervalModule
from i3pystatus.core.util import user_open, internet, require from i3pystatus.core.util import internet, require
from datetime import datetime from datetime import datetime
from urllib.request import urlopen from urllib.request import urlopen
@ -7,19 +7,21 @@ import json
import re import re
GEOLOOKUP_URL = 'http://api.wunderground.com/api/%s/geolookup%s/q/%s.json' 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' STATION_QUERY_URL = 'http://api.wunderground.com/api/%s/%s/q/%s.json'
class Wunderground(IntervalModule): class Wunderground(IntervalModule):
''' '''
This module retrieves weather from the Weather Underground API. This module retrieves weather data using the Weather Underground API.
.. note:: .. note::
A Weather Underground API key is required to use this module, you can A Weather Underground API key is required to use this module, you can
sign up for one for a developer API key free at sign up for a developer API key free at
https://www.wunderground.com/weather/api/ https://www.wunderground.com/weather/api/
A developer API key is allowed 500 queries per day. A developer API key is allowed 500 queries per day, and no more than 10
in a given minute. Therefore, it is recommended to be conservative when
setting the update interval.
Valid values for ``location_code`` include: Valid values for ``location_code`` include:
@ -37,58 +39,60 @@ class Wunderground(IntervalModule):
http://www.wunderground.com/weatherstation/ListStations.asp http://www.wunderground.com/weatherstation/ListStations.asp
.. rubric:: Available formatters .. _weather-usage-wunderground:
* `{city}` Location of weather observation .. rubric:: Usage example
* `{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
.. code-block:: python
from i3pystatus import Status
from i3pystatus.weather import wunderground
status = Status()
status.register(
'weather',
format='{condition} {current_temp}{temp_unit}{icon}[ Hi: {high_temp}] Lo: {low_temp}',
colorize=True,
backend=wunderground.Wunderground(
api_key='dbafe887d56ba4ad',
location_code='pws:MAT645',
units='imperial',
),
)
status.run()
See :ref:`here <weather-formatters>` for a list of formatters which can be
used.
''' '''
interval = 300 interval = 300
settings = ( settings = (
('api_key', 'Weather Underground API key'), ('api_key', 'Weather Underground API key'),
('location_code', 'Location code from www.weather.com'), ('location_code', 'Location code from wunderground.com'),
('units', 'Celsius (metric) or Fahrenheit (imperial)'), ('units', '\'metric\' or \'imperial\''),
('use_pws', 'Set to False to use only airport stations'), ('use_pws', 'Set to False to use only airport stations'),
('error_log', 'If set, tracebacks will be logged to this file'), ('forecast', 'Set to ``True`` to check forecast (generates one '
'format', 'additional API request per weather update). If set to '
'``False``, then the ``low_temp`` and ``high_temp`` '
'formatters will be set to empty strings.'),
) )
required = ('api_key', 'location_code') required = ('api_key', 'location_code')
api_key = None api_key = None
location_code = None location_code = None
units = "metric" units = 'metric'
format = "{current_temp}{degrees}"
use_pws = True use_pws = True
error_log = None forecast = False
# These will be set once weather data has been checked
station_id = None station_id = None
forecast_url = None forecast_url = None
on_leftclick = 'open_wunderground' @require(internet)
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): def api_request(self, url):
''' '''
Execute an HTTP POST to the specified URL and return the content Execute an HTTP POST to the specified URL and return the content
@ -106,6 +110,7 @@ class Wunderground(IntervalModule):
pass pass
return response return response
@require(internet)
def geolookup(self): def geolookup(self):
''' '''
Use the location_code to perform a geolookup and find the closest Use the location_code to perform a geolookup and find the closest
@ -148,32 +153,63 @@ class Wunderground(IntervalModule):
raise Exception('No icao entry for station') raise Exception('No icao entry for station')
self.station_id = 'icao:%s' % nearest_airport self.station_id = 'icao:%s' % nearest_airport
def query_station(self): @require(internet)
def get_forecast(self):
''' '''
Query a specific station If configured to do so, make an API request to retrieve the forecast
data for the configured/queried weather station, and return the low and
high temperatures. Otherwise, return two empty strings.
'''
if self.forecast:
query_url = STATION_QUERY_URL % (self.api_key,
'forecast',
self.station_id)
try:
response = self.api_request(query_url)['forecast']
response = response['simpleforecast']['forecastday'][0]
except (KeyError, IndexError, TypeError):
raise Exception('No forecast data found for %s' % self.station_id)
unit = 'celsius' if self.units == 'metric' else 'fahrenheit'
low_temp = response.get('low', {}).get(unit, '')
high_temp = response.get('high', {}).get(unit, '')
return low_temp, high_temp
else:
return '', ''
@require(internet)
def weather_data(self):
'''
Query the configured/queried station and return the weather data
''' '''
# If necessary, do a geolookup to set the station_id # If necessary, do a geolookup to set the station_id
self.geolookup() self.geolookup()
query_url = STATION_LOOKUP_URL % (self.api_key, self.station_id) query_url = STATION_QUERY_URL % (self.api_key,
'conditions',
self.station_id)
try: try:
response = self.api_request(query_url)['current_observation'] response = self.api_request(query_url)['current_observation']
self.forecast_url = response.pop('ob_url', None) self.forecast_url = response.pop('ob_url', None)
except KeyError: except KeyError:
raise Exception('No weather data found for %s' % self.station_id) raise Exception('No weather data found for %s' % self.station_id)
def _find(key, data=None): low_temp, high_temp = self.get_forecast()
data = data or response
return data.get(key, 'N/A')
if self.units == 'metric': if self.units == 'metric':
temp_unit = 'c' temp_unit = 'c'
speed_unit = 'kph' speed_unit = 'kph'
distance_unit = 'km' distance_unit = 'km'
pressure_unit = 'mb'
else: else:
temp_unit = 'f' temp_unit = 'f'
speed_unit = 'mph' speed_unit = 'mph'
distance_unit = 'mi' distance_unit = 'mi'
pressure_unit = 'in'
def _find(key, data=None):
data = data or response
return data.get(key, 'N/A')
try: try:
observation_time = int(_find('observation_epoch')) observation_time = int(_find('observation_epoch'))
@ -185,36 +221,20 @@ class Wunderground(IntervalModule):
condition=_find('weather'), condition=_find('weather'),
observation_time=datetime.fromtimestamp(observation_time), observation_time=datetime.fromtimestamp(observation_time),
current_temp=_find('temp_' + temp_unit), current_temp=_find('temp_' + temp_unit),
low_temp=low_temp,
high_temp=high_temp,
temp_unit='°' + temp_unit.upper(),
feelslike=_find('feelslike_' + temp_unit), feelslike=_find('feelslike_' + temp_unit),
current_wind=_find('wind_' + speed_unit), dewpoint=_find('dewpoint_' + temp_unit),
current_wind_direction=_find('wind_dir'), wind_speed=_find('wind_' + speed_unit),
current_wind_gust=_find('wind_gust_' + speed_unit), wind_unit=speed_unit,
pressure_in=_find('pressure_in'), wind_direction=_find('wind_dir'),
pressure_mb=_find('pressure_mb'), wind_gust=_find('wind_gust_' + speed_unit),
pressure=_find('pressure_' + pressure_unit),
pressure_unit=pressure_unit,
pressure_trend=_find('pressure_trend'), pressure_trend=_find('pressure_trend'),
visibility=_find('visibility_' + distance_unit), visibility=_find('visibility_' + distance_unit),
visibility_unit=distance_unit,
humidity=_find('relative_humidity').rstrip('%'), humidity=_find('relative_humidity').rstrip('%'),
dewpoint=_find('dewpoint_' + temp_unit),
uv_index=_find('uv'), 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
}