Merge pull request #349 from terminalmage/wunderground
Add generic weather module
This commit is contained in:
commit
11861a500a
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ dist/*
|
|||||||
*~
|
*~
|
||||||
.i3pystatus-*
|
.i3pystatus-*
|
||||||
ci-build
|
ci-build
|
||||||
|
docs/_build
|
||||||
|
@ -17,6 +17,7 @@ David Garcia Quintas
|
|||||||
David Wahlstrom
|
David Wahlstrom
|
||||||
dubwoc
|
dubwoc
|
||||||
eBrnd
|
eBrnd
|
||||||
|
Erik Johnson
|
||||||
enkore
|
enkore
|
||||||
facetoe
|
facetoe
|
||||||
Frank Tackitt
|
Frank Tackitt
|
||||||
|
@ -175,9 +175,9 @@ decimal dot
|
|||||||
formatp
|
formatp
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
Some modules use an extended format string syntax (the :py:mod:`.mpd`
|
Some modules use an extended format string syntax (the :py:mod:`.mpd` and
|
||||||
module, for example). Given the format string below the output adapts
|
:py:mod:`.weather` modules, for example). Given the format string below the
|
||||||
itself to the available data.
|
output adapts itself to the available data.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
@ -52,3 +52,12 @@ Update Backends
|
|||||||
.. autogen:: i3pystatus.updates SettingsBase
|
.. autogen:: i3pystatus.updates SettingsBase
|
||||||
|
|
||||||
.. nothin'
|
.. nothin'
|
||||||
|
|
||||||
|
.. _weatherbackends:
|
||||||
|
|
||||||
|
Weather Backends
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. autogen:: i3pystatus.weather SettingsBase
|
||||||
|
|
||||||
|
.. nothin'
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
from i3pystatus import IntervalModule
|
|
||||||
from i3pystatus.core.util import internet, require
|
|
||||||
|
|
||||||
from urllib.request import urlopen
|
|
||||||
import re
|
|
||||||
import xml.etree.ElementTree as ElementTree
|
|
||||||
|
|
||||||
WEATHER_COM_URL = 'http://wxdata.weather.com/wxdata/weather/local/%s?unit=%s&dayf=1&cc=*'
|
|
||||||
|
|
||||||
|
|
||||||
class Weather(IntervalModule):
|
|
||||||
"""
|
|
||||||
This module gets the weather from weather.com.
|
|
||||||
First, you need to get the code for the location from www.weather.com
|
|
||||||
|
|
||||||
.. rubric:: Available formatters
|
|
||||||
|
|
||||||
* `{current_temp}` — current temperature including unit (and symbol if colorize is true)
|
|
||||||
* `{min_temp}` — today's minimum temperature including unit
|
|
||||||
* `{max_temp}` — today's maximum temperature including unit
|
|
||||||
* `{current_wind}` — current wind direction, speed including unit
|
|
||||||
* `{humidity}` — current humidity excluding percentage symbol
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
interval = 20
|
|
||||||
|
|
||||||
settings = (
|
|
||||||
("location_code", "Location code from www.weather.com"),
|
|
||||||
("colorize", "Enable color with temperature and UTF-8 icons."),
|
|
||||||
("color_icons", "Dictionary mapping weather conditions to tuples "
|
|
||||||
"containing a UTF-8 code for the icon, and the color "
|
|
||||||
"to be used."),
|
|
||||||
("units", "Celsius (metric) or Fahrenheit (imperial)"),
|
|
||||||
"format",
|
|
||||||
)
|
|
||||||
required = ("location_code",)
|
|
||||||
|
|
||||||
location_code = None
|
|
||||||
units = "metric"
|
|
||||||
format = "{current_temp}"
|
|
||||||
colorize = False
|
|
||||||
color_icons = {
|
|
||||||
"Fair": (u"\u2600", "#FFCC00"),
|
|
||||||
"Cloudy": (u"\u2601", "#F8F8FF"),
|
|
||||||
"Partly Cloudy": (u"\u2601", "#F8F8FF"), # \u26c5 is not in many fonts
|
|
||||||
"Rainy": (u"\u2614", "#CBD2C0"),
|
|
||||||
"Sunny": (u"\u263C", "#FFFF00"),
|
|
||||||
"Snow": (u"\u2603", "#FFFFFF"),
|
|
||||||
"default": ("", None),
|
|
||||||
}
|
|
||||||
|
|
||||||
def fetch_weather(self):
|
|
||||||
'''Fetches the current weather from wxdata.weather.com service.'''
|
|
||||||
unit = '' if self.units == 'imperial' or self.units == '' else 'm'
|
|
||||||
url = WEATHER_COM_URL % (self.location_code, unit)
|
|
||||||
with urlopen(url) as handler:
|
|
||||||
try:
|
|
||||||
content_type = dict(handler.getheaders())['Content-Type']
|
|
||||||
charset = re.search(r'charset=(.*)', content_type).group(1)
|
|
||||||
except AttributeError:
|
|
||||||
charset = 'utf-8'
|
|
||||||
xml = handler.read().decode(charset)
|
|
||||||
doc = ElementTree.XML(xml)
|
|
||||||
return dict(
|
|
||||||
current_conditions=dict(
|
|
||||||
text=doc.findtext('cc/t'),
|
|
||||||
temperature=doc.findtext('cc/tmp'),
|
|
||||||
humidity=doc.findtext('cc/hmid'),
|
|
||||||
wind=dict(
|
|
||||||
text=doc.findtext('cc/wind/t'),
|
|
||||||
speed=doc.findtext('cc/wind/s'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
today=dict(
|
|
||||||
min_temperature=doc.findtext('dayf/day[@d="0"]/low'),
|
|
||||||
max_temperature=doc.findtext('dayf/day[@d="0"]/hi'),
|
|
||||||
),
|
|
||||||
units=dict(
|
|
||||||
temperature=doc.findtext('head/ut'),
|
|
||||||
speed=doc.findtext('head/us'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@require(internet)
|
|
||||||
def run(self):
|
|
||||||
result = self.fetch_weather()
|
|
||||||
conditions = result["current_conditions"]
|
|
||||||
temperature = conditions["temperature"]
|
|
||||||
humidity = conditions["humidity"]
|
|
||||||
wind = conditions["wind"]
|
|
||||||
units = result["units"]
|
|
||||||
color = None
|
|
||||||
current_temp = "{t}°{d}".format(t=temperature, d=units["temperature"])
|
|
||||||
min_temp = "{t}°{d}".format(t=result["today"]["min_temperature"], d=units["temperature"])
|
|
||||||
max_temp = "{t}°{d}".format(t=result["today"]["max_temperature"], d=units["temperature"])
|
|
||||||
current_wind = "{t} {s}{d}".format(t=wind["text"], s=wind["speed"], d=units["speed"])
|
|
||||||
|
|
||||||
if self.colorize:
|
|
||||||
icon, color = self.color_icons.get(conditions["text"],
|
|
||||||
self.color_icons["default"])
|
|
||||||
current_temp = "{t}°{d} {i}".format(t=temperature,
|
|
||||||
d=units["temperature"],
|
|
||||||
i=icon)
|
|
||||||
color = color
|
|
||||||
|
|
||||||
self.output = {
|
|
||||||
"full_text": self.format.format(
|
|
||||||
current_temp=current_temp,
|
|
||||||
current_wind=current_wind,
|
|
||||||
humidity=humidity,
|
|
||||||
min_temp=min_temp,
|
|
||||||
max_temp=max_temp,
|
|
||||||
),
|
|
||||||
"color": color
|
|
||||||
}
|
|
141
i3pystatus/weather/__init__.py
Normal file
141
i3pystatus/weather/__init__.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
from i3pystatus import SettingsBase, IntervalModule, formatp
|
||||||
|
from i3pystatus.core.util import user_open, internet, require
|
||||||
|
|
||||||
|
|
||||||
|
class Backend(SettingsBase):
|
||||||
|
settings = ()
|
||||||
|
|
||||||
|
|
||||||
|
class Weather(IntervalModule):
|
||||||
|
'''
|
||||||
|
This is a generic weather-checker which must use a configured weather
|
||||||
|
backend. For list of all available backends see :ref:`weatherbackends`.
|
||||||
|
|
||||||
|
Left clicking on the module will launch the forecast page for the location
|
||||||
|
being checked.
|
||||||
|
|
||||||
|
.. _weather-formatters:
|
||||||
|
|
||||||
|
.. rubric:: Available formatters
|
||||||
|
|
||||||
|
* `{city}` — Location of weather observation
|
||||||
|
* `{condition}` — Current weather condition (Rain, Snow, Overcast, etc.)
|
||||||
|
* `{icon}` — Icon representing the current weather condition
|
||||||
|
* `{observation_time}` — Time of weather observation (supports strftime format flags)
|
||||||
|
* `{current_temp}` — Current temperature, excluding unit
|
||||||
|
* `{low_temp}` — Forecasted low temperature, excluding unit
|
||||||
|
* `{high_temp}` — Forecasted high temperature, excluding unit (may be
|
||||||
|
empty in the late afternoon)
|
||||||
|
* `{temp_unit}` — Either ``°C`` or ``°F``, depending on whether metric or
|
||||||
|
* `{feelslike}` — "Feels Like" temperature, excluding unit
|
||||||
|
* `{dewpoint}` — Dewpoint temperature, excluding unit
|
||||||
|
imperial units are being used
|
||||||
|
* `{wind_speed}` — Wind speed, excluding unit
|
||||||
|
* `{wind_unit}` — Either ``kph`` or ``mph``, depending on whether metric or
|
||||||
|
imperial units are being used
|
||||||
|
* `{wind_direction}` — Wind direction
|
||||||
|
* `{wind_gust}` — Speed of wind gusts in mph/kph, excluding unit
|
||||||
|
* `{pressure}` — Barometric pressure, excluding unit
|
||||||
|
* `{pressure_unit}` — ``mb`` or ``in``, depending on whether metric or
|
||||||
|
imperial units are being used
|
||||||
|
* `{pressure_trend}` — ``+`` if rising, ``-`` if falling, or an empty
|
||||||
|
string if the pressure is steady (neither rising nor falling)
|
||||||
|
* `{visibility}` — Visibility distance, excluding unit
|
||||||
|
* `{visibility_unit}` — Either ``km`` or ``mi``, depending on whether
|
||||||
|
metric or imperial units are being used
|
||||||
|
* `{humidity}` — Current humidity, excluding percentage symbol
|
||||||
|
* `{uv_index}` — UV Index
|
||||||
|
|
||||||
|
This module supports the :ref:`formatp <formatp>` extended string format
|
||||||
|
syntax. This allows for values to be hidden when they evaluate as False.
|
||||||
|
This comes in handy for the :py:mod:`weathercom <.weather.weathercom>`
|
||||||
|
backend, which at a certain point in the afternoon will have a blank
|
||||||
|
``{high_temp}`` value. Using the following snippet in your format string
|
||||||
|
will only display the high temperature information if it is not blank:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{current_temp}{temp_unit}[ Hi: {high_temp}[{temp_unit}]] Lo: {low_temp}{temp_unit}
|
||||||
|
|
||||||
|
Brackets are evaluated from the outside-in, so the fact that the only
|
||||||
|
formatter in the outer block (``{high_temp}``) is empty would keep the
|
||||||
|
inner block from being evaluated at all, and entire block would not be
|
||||||
|
displayed.
|
||||||
|
|
||||||
|
See the following links for usage examples for the available weather
|
||||||
|
backends:
|
||||||
|
|
||||||
|
- :ref:`Weather.com <weather-usage-weathercom>`
|
||||||
|
- :ref:`Weather Underground <weather-usage-wunderground>`
|
||||||
|
'''
|
||||||
|
|
||||||
|
settings = (
|
||||||
|
('colorize', 'Vary the color depending on the current conditions.'),
|
||||||
|
('color_icons', 'Dictionary mapping weather conditions to tuples '
|
||||||
|
'containing a UTF-8 code for the icon, and the color '
|
||||||
|
'to be used.'),
|
||||||
|
('color', 'Display color (or fallback color if ``colorize`` is True). '
|
||||||
|
'If not specified, falls back to default i3bar color.'),
|
||||||
|
('backend', 'Weather backend instance'),
|
||||||
|
'interval',
|
||||||
|
'format',
|
||||||
|
)
|
||||||
|
required = ('backend',)
|
||||||
|
|
||||||
|
colorize = False
|
||||||
|
color_icons = {
|
||||||
|
'Fair': (u'\u2600', '#ffcc00'),
|
||||||
|
'Cloudy': (u'\u2601', '#f8f8ff'),
|
||||||
|
'Partly Cloudy': (u'\u2601', '#f8f8ff'), # \u26c5 is not in many fonts
|
||||||
|
'Rainy': (u'\u26c8', '#cbd2c0'),
|
||||||
|
'Thunderstorm': (u'\u03de', '#cbd2c0'),
|
||||||
|
'Sunny': (u'\u263c', '#ffff00'),
|
||||||
|
'Snow': (u'\u2603', '#ffffff'),
|
||||||
|
'default': ('', None),
|
||||||
|
}
|
||||||
|
|
||||||
|
color = None
|
||||||
|
backend = None
|
||||||
|
interval = 1800
|
||||||
|
format = '{current_temp}{temp_unit}'
|
||||||
|
|
||||||
|
on_leftclick = 'open_forecast_url'
|
||||||
|
|
||||||
|
def open_forecast_url(self):
|
||||||
|
if self.backend.forecast_url and self.backend.forecast_url != 'N/A':
|
||||||
|
user_open(self.backend.forecast_url)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_color_data(self, condition):
|
||||||
|
'''
|
||||||
|
Disambiguate similarly-named weather conditions, and return the icon
|
||||||
|
and color that match.
|
||||||
|
'''
|
||||||
|
condition_lc = condition.lower()
|
||||||
|
if condition_lc == 'clear':
|
||||||
|
condition = 'Fair'
|
||||||
|
if 'cloudy' in condition_lc:
|
||||||
|
condition = 'Cloudy'
|
||||||
|
elif 'rain' in condition_lc:
|
||||||
|
condition = 'Rainy'
|
||||||
|
elif 'thunder' in condition_lc:
|
||||||
|
condition = 'Thunderstorm'
|
||||||
|
elif 'snow' in condition_lc:
|
||||||
|
condition = 'Snow'
|
||||||
|
|
||||||
|
return self.color_icons['default'] \
|
||||||
|
if condition not in self.color_icons \
|
||||||
|
else self.color_icons[condition]
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
def run(self):
|
||||||
|
data = self.backend.weather_data()
|
||||||
|
data['icon'], condition_color = self.get_color_data(data['condition'])
|
||||||
|
color = condition_color if self.colorize else self.color
|
||||||
|
|
||||||
|
self.output = {
|
||||||
|
'full_text': formatp(self.format, **data).strip(),
|
||||||
|
'color': color,
|
||||||
|
}
|
137
i3pystatus/weather/weathercom.py
Normal file
137
i3pystatus/weather/weathercom.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
from i3pystatus import IntervalModule
|
||||||
|
from i3pystatus.core.util import internet, require
|
||||||
|
from i3pystatus.weather import Backend
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.request import urlopen
|
||||||
|
import re
|
||||||
|
import xml.etree.ElementTree as ElementTree
|
||||||
|
|
||||||
|
WEATHER_COM_URL = 'http://wxdata.weather.com/wxdata/weather/local/%s?unit=%s&dayf=1&cc=*'
|
||||||
|
ON_LEFTCLICK_URL = 'https://weather.com/weather/today/l/%s'
|
||||||
|
|
||||||
|
|
||||||
|
class Weathercom(Backend):
|
||||||
|
'''
|
||||||
|
This module gets the weather from weather.com. The ``location_code``
|
||||||
|
parameter should be set to the location code from weather.com. To obtain
|
||||||
|
this code, search for the location on weather.com, and the location code
|
||||||
|
will be everything after the last slash (e.g. ``94107:4:US``).
|
||||||
|
|
||||||
|
.. _weather-usage-weathercom:
|
||||||
|
|
||||||
|
.. rubric:: Usage example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from i3pystatus import Status
|
||||||
|
from i3pystatus.weather import weathercom
|
||||||
|
|
||||||
|
status = Status()
|
||||||
|
|
||||||
|
status.register(
|
||||||
|
'weather',
|
||||||
|
format='{condition} {current_temp}{temp_unit}{icon}[ Hi: {high_temp}] Lo: {low_temp}',
|
||||||
|
colorize=True,
|
||||||
|
backend=weathercom.Weathercom(
|
||||||
|
location_code='94107:4:US',
|
||||||
|
units='imperial',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
status.run()
|
||||||
|
|
||||||
|
See :ref:`here <weather-formatters>` for a list of formatters which can be
|
||||||
|
used.
|
||||||
|
'''
|
||||||
|
settings = (
|
||||||
|
('location_code', 'Location code from www.weather.com'),
|
||||||
|
('units', '\'metric\' or \'imperial\''),
|
||||||
|
)
|
||||||
|
required = ('location_code',)
|
||||||
|
|
||||||
|
location_code = None
|
||||||
|
|
||||||
|
units = 'metric'
|
||||||
|
|
||||||
|
# This will be set once weather data has been checked
|
||||||
|
forecast_url = None
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
def weather_data(self):
|
||||||
|
'''
|
||||||
|
Fetches the current weather from wxdata.weather.com service.
|
||||||
|
'''
|
||||||
|
if self.forecast_url is None and ':' in self.location_code:
|
||||||
|
# Set the URL so that clicking the weather will launch the
|
||||||
|
# weather.com forecast page. Only set it though if there is a colon
|
||||||
|
# in the location_code. Technically, the weather.com API will
|
||||||
|
# successfully return weather data if a U.S. ZIP code is used as
|
||||||
|
# the location_code (e.g. 94107), but if substituted in
|
||||||
|
# ON_LEFTCLICK_URL it may or may not result in a valid URL.
|
||||||
|
self.forecast_url = ON_LEFTCLICK_URL % self.location_code
|
||||||
|
|
||||||
|
unit = '' if self.units == 'imperial' or self.units == '' else 'm'
|
||||||
|
url = WEATHER_COM_URL % (self.location_code, unit)
|
||||||
|
with urlopen(url) as handler:
|
||||||
|
try:
|
||||||
|
content_type = dict(handler.getheaders())['Content-Type']
|
||||||
|
charset = re.search(r'charset=(.*)', content_type).group(1)
|
||||||
|
except AttributeError:
|
||||||
|
charset = 'utf-8'
|
||||||
|
xml = handler.read().decode(charset)
|
||||||
|
doc = ElementTree.XML(xml)
|
||||||
|
|
||||||
|
# Cut off the timezone from the end of the string (it's after the last
|
||||||
|
# space, hence the use of rpartition). International timezones (or ones
|
||||||
|
# outside the system locale) don't seem to be handled well by
|
||||||
|
# datetime.datetime.strptime().
|
||||||
|
observation_time_str = doc.findtext('cc/lsup').rpartition(' ')[0]
|
||||||
|
try:
|
||||||
|
observation_time = datetime.strptime(observation_time_str,
|
||||||
|
'%m/%d/%y %I:%M %p')
|
||||||
|
except ValueError:
|
||||||
|
observation_time = datetime.fromtimestamp(0)
|
||||||
|
|
||||||
|
pressure_trend_str = doc.findtext('cc/bar/d').lower()
|
||||||
|
if pressure_trend_str == 'rising':
|
||||||
|
pressure_trend = '+'
|
||||||
|
elif pressure_trend_str == 'falling':
|
||||||
|
pressure_trend = '-'
|
||||||
|
else:
|
||||||
|
pressure_trend = ''
|
||||||
|
|
||||||
|
if not doc.findtext('dayf/day[@d="0"]/part[@p="d"]/icon').strip():
|
||||||
|
# If the "d" (day) part of today's forecast's keys are empty, there
|
||||||
|
# is no high temp anymore (this happens in the afternoon), but
|
||||||
|
# instead of handling things in a sane way and setting the high
|
||||||
|
# temp to an empty string or something like that, the API returns
|
||||||
|
# the current temp as the high temp, which is incorrect. This if
|
||||||
|
# statement catches it so that we can correctly report that there
|
||||||
|
# is no high temp at this point of the day.
|
||||||
|
high_temp = ''
|
||||||
|
else:
|
||||||
|
high_temp = doc.findtext('dayf/day[@d="0"]/hi')
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
city=doc.findtext('loc/dnam'),
|
||||||
|
condition=doc.findtext('cc/t'),
|
||||||
|
observation_time=observation_time,
|
||||||
|
current_temp=doc.findtext('cc/tmp'),
|
||||||
|
low_temp=doc.findtext('dayf/day[@d="0"]/low'),
|
||||||
|
high_temp=high_temp,
|
||||||
|
temp_unit='°' + doc.findtext('head/ut').upper(),
|
||||||
|
feelslike=doc.findtext('cc/flik'),
|
||||||
|
dewpoint=doc.findtext('cc/dewp'),
|
||||||
|
wind_speed=doc.findtext('cc/wind/s'),
|
||||||
|
wind_unit=doc.findtext('head/us'),
|
||||||
|
wind_direction=doc.findtext('cc/wind/t'),
|
||||||
|
wind_gust=doc.findtext('cc/wind/gust'),
|
||||||
|
pressure=doc.findtext('cc/bar/r'),
|
||||||
|
pressure_unit=doc.findtext('head/up'),
|
||||||
|
pressure_trend=pressure_trend,
|
||||||
|
visibility=doc.findtext('cc/vis'),
|
||||||
|
visibility_unit=doc.findtext('head/ud'),
|
||||||
|
humidity=doc.findtext('cc/hmid'),
|
||||||
|
uv_index=doc.findtext('cc/uv/i'),
|
||||||
|
)
|
240
i3pystatus/weather/wunderground.py
Normal file
240
i3pystatus/weather/wunderground.py
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
from i3pystatus import IntervalModule
|
||||||
|
from i3pystatus.core.util import 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_QUERY_URL = 'http://api.wunderground.com/api/%s/%s/q/%s.json'
|
||||||
|
|
||||||
|
|
||||||
|
class Wunderground(IntervalModule):
|
||||||
|
'''
|
||||||
|
This module retrieves weather data using the Weather Underground API.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
A Weather Underground API key is required to use this module, you can
|
||||||
|
sign up for a developer API key free at
|
||||||
|
https://www.wunderground.com/weather/api/
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
* **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`` or ``icao`` station ID, the location will be
|
||||||
|
queried, and the closest station will be used. For a list of PWS
|
||||||
|
station IDs, visit the following URL:
|
||||||
|
|
||||||
|
http://www.wunderground.com/weatherstation/ListStations.asp
|
||||||
|
|
||||||
|
.. _weather-usage-wunderground:
|
||||||
|
|
||||||
|
.. rubric:: Usage example
|
||||||
|
|
||||||
|
.. 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
|
||||||
|
|
||||||
|
settings = (
|
||||||
|
('api_key', 'Weather Underground API key'),
|
||||||
|
('location_code', 'Location code from wunderground.com'),
|
||||||
|
('units', '\'metric\' or \'imperial\''),
|
||||||
|
('use_pws', 'Set to False to use only airport stations'),
|
||||||
|
('forecast', 'Set to ``True`` to check forecast (generates one '
|
||||||
|
'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')
|
||||||
|
|
||||||
|
api_key = None
|
||||||
|
location_code = None
|
||||||
|
units = 'metric'
|
||||||
|
use_pws = True
|
||||||
|
forecast = False
|
||||||
|
|
||||||
|
# These will be set once weather data has been checked
|
||||||
|
station_id = None
|
||||||
|
forecast_url = None
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
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
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
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
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
def get_forecast(self):
|
||||||
|
'''
|
||||||
|
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
|
||||||
|
self.geolookup()
|
||||||
|
|
||||||
|
query_url = STATION_QUERY_URL % (self.api_key,
|
||||||
|
'conditions',
|
||||||
|
self.station_id)
|
||||||
|
try:
|
||||||
|
response = self.api_request(query_url)['current_observation']
|
||||||
|
self.forecast_url = response.pop('ob_url', None)
|
||||||
|
except KeyError:
|
||||||
|
raise Exception('No weather data found for %s' % self.station_id)
|
||||||
|
|
||||||
|
low_temp, high_temp = self.get_forecast()
|
||||||
|
|
||||||
|
if self.units == 'metric':
|
||||||
|
temp_unit = 'c'
|
||||||
|
speed_unit = 'kph'
|
||||||
|
distance_unit = 'km'
|
||||||
|
pressure_unit = 'mb'
|
||||||
|
else:
|
||||||
|
temp_unit = 'f'
|
||||||
|
speed_unit = 'mph'
|
||||||
|
distance_unit = 'mi'
|
||||||
|
pressure_unit = 'in'
|
||||||
|
|
||||||
|
def _find(key, data=None):
|
||||||
|
data = data or response
|
||||||
|
return data.get(key, 'N/A')
|
||||||
|
|
||||||
|
try:
|
||||||
|
observation_time = int(_find('observation_epoch'))
|
||||||
|
except TypeError:
|
||||||
|
observation_time = 0
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
city=_find('city', response['observation_location']),
|
||||||
|
condition=_find('weather'),
|
||||||
|
observation_time=datetime.fromtimestamp(observation_time),
|
||||||
|
current_temp=_find('temp_' + temp_unit),
|
||||||
|
low_temp=low_temp,
|
||||||
|
high_temp=high_temp,
|
||||||
|
temp_unit='°' + temp_unit.upper(),
|
||||||
|
feelslike=_find('feelslike_' + temp_unit),
|
||||||
|
dewpoint=_find('dewpoint_' + temp_unit),
|
||||||
|
wind_speed=_find('wind_' + speed_unit),
|
||||||
|
wind_unit=speed_unit,
|
||||||
|
wind_direction=_find('wind_dir'),
|
||||||
|
wind_gust=_find('wind_gust_' + speed_unit),
|
||||||
|
pressure=_find('pressure_' + pressure_unit),
|
||||||
|
pressure_unit=pressure_unit,
|
||||||
|
pressure_trend=_find('pressure_trend'),
|
||||||
|
visibility=_find('visibility_' + distance_unit),
|
||||||
|
visibility_unit=distance_unit,
|
||||||
|
humidity=_find('relative_humidity').rstrip('%'),
|
||||||
|
uv_index=_find('uv'),
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user