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-*
|
||||
ci-build
|
||||
docs/_build
|
||||
|
@ -17,6 +17,7 @@ David Garcia Quintas
|
||||
David Wahlstrom
|
||||
dubwoc
|
||||
eBrnd
|
||||
Erik Johnson
|
||||
enkore
|
||||
facetoe
|
||||
Frank Tackitt
|
||||
|
@ -175,9 +175,9 @@ decimal dot
|
||||
formatp
|
||||
~~~~~~~
|
||||
|
||||
Some modules use an extended format string syntax (the :py:mod:`.mpd`
|
||||
module, for example). Given the format string below the output adapts
|
||||
itself to the available data.
|
||||
Some modules use an extended format string syntax (the :py:mod:`.mpd` and
|
||||
:py:mod:`.weather` modules, for example). Given the format string below the
|
||||
output adapts itself to the available data.
|
||||
|
||||
::
|
||||
|
||||
|
@ -52,3 +52,12 @@ Update Backends
|
||||
.. autogen:: i3pystatus.updates SettingsBase
|
||||
|
||||
.. 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