Optimize weather module, rewrite Weather.com backend
Weather.com's XML feed is now defunct, this commit includes a rewritten Weather.com module which uses the same JSON feed used by the website itself. The weather updates now happen in a separate thread rather than happening in the ``run()`` function. Since the ``run()`` function is executed before any of the mouse events are run, this was causing the mouse event callbacks to result in a flurry of weather updates, which caused the Weather Underground API to be overutilized beyond its rate limit.
This commit is contained in:
parent
03df1a644a
commit
d798a8c3d8
@ -1,18 +1,61 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from i3pystatus import SettingsBase, IntervalModule, formatp
|
from i3pystatus import SettingsBase, IntervalModule, formatp
|
||||||
from i3pystatus.core.util import user_open, internet, require
|
from i3pystatus.core.util import user_open, internet, require
|
||||||
|
|
||||||
|
|
||||||
class Backend(SettingsBase):
|
class WeatherBackend(SettingsBase):
|
||||||
settings = ()
|
settings = ()
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
def api_request(self, url):
|
||||||
|
self.logger.debug('Making API request to %s', url)
|
||||||
|
try:
|
||||||
|
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 = content.read().decode(charset).strip()
|
||||||
|
if not response_json:
|
||||||
|
self.logger.debug('JSON response from %s was blank', url)
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
response = json.loads(response_json)
|
||||||
|
except json.decoder.JSONDecodeError as exc:
|
||||||
|
self.logger.error('Error loading JSON: %s', exc)
|
||||||
|
self.logger.debug('JSON text that failed to load: %s',
|
||||||
|
response_json)
|
||||||
|
return {}
|
||||||
|
self.logger.log(5, 'API response: %s', response)
|
||||||
|
error = self.check_response(response)
|
||||||
|
if error:
|
||||||
|
self.logger.error('Error in JSON response: %s', error)
|
||||||
|
return {}
|
||||||
|
return response
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.error(
|
||||||
|
'Failed to make API request to %s. Exception follows:', url,
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def check_response(response):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class Weather(IntervalModule):
|
class Weather(IntervalModule):
|
||||||
'''
|
'''
|
||||||
This is a generic weather-checker which must use a configured weather
|
This is a generic weather-checker which must use a configured weather
|
||||||
backend. For list of all available backends see :ref:`weatherbackends`.
|
backend. For list of all available backends see :ref:`weatherbackends`.
|
||||||
|
|
||||||
Left clicking on the module will launch the forecast page for the location
|
Double-clicking on the module will launch the forecast page for the
|
||||||
being checked.
|
location being checked, and single-clicking will trigger an update.
|
||||||
|
|
||||||
.. _weather-formatters:
|
.. _weather-formatters:
|
||||||
|
|
||||||
@ -45,17 +88,26 @@ class Weather(IntervalModule):
|
|||||||
metric or imperial units are being used
|
metric or imperial units are being used
|
||||||
* `{humidity}` — Current humidity, excluding percentage symbol
|
* `{humidity}` — Current humidity, excluding percentage symbol
|
||||||
* `{uv_index}` — UV Index
|
* `{uv_index}` — UV Index
|
||||||
|
* `{update_error}` — When the configured weather backend encounters an
|
||||||
|
error during an update, this formatter will be set to the value of the
|
||||||
|
backend's **update_error** config value. Otherwise, this formatter will
|
||||||
|
be an empty string.
|
||||||
|
|
||||||
This module supports the :ref:`formatp <formatp>` extended string format
|
This module supports the :ref:`formatp <formatp>` extended string format
|
||||||
syntax. This allows for values to be hidden when they evaluate as False.
|
syntax. This allows for values to be hidden when they evaluate as False.
|
||||||
This comes in handy for the :py:mod:`weathercom <.weather.weathercom>`
|
The default **format** string value for this module makes use of this
|
||||||
backend, which at a certain point in the afternoon will have a blank
|
syntax to conditionally show the value of the **update_error** config value
|
||||||
``{high_temp}`` value. Using the following snippet in your format string
|
when the backend encounters an error during an update.
|
||||||
will only display the high temperature information if it is not blank:
|
|
||||||
|
The extended string format syntax also 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}
|
{current_temp}{temp_unit}[ Hi: {high_temp}] Lo: {low_temp}[ {update_error}]
|
||||||
|
|
||||||
Brackets are evaluated from the outside-in, so the fact that the only
|
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
|
formatter in the outer block (``{high_temp}``) is empty would keep the
|
||||||
@ -67,6 +119,44 @@ class Weather(IntervalModule):
|
|||||||
|
|
||||||
- :ref:`Weather.com <weather-usage-weathercom>`
|
- :ref:`Weather.com <weather-usage-weathercom>`
|
||||||
- :ref:`Weather Underground <weather-usage-wunderground>`
|
- :ref:`Weather Underground <weather-usage-wunderground>`
|
||||||
|
|
||||||
|
.. rubric:: Troubleshooting
|
||||||
|
|
||||||
|
If an error is encountered while updating, the ``{update_error}`` formatter
|
||||||
|
will be set, and (provided it is in your ``format`` string) will show up
|
||||||
|
next to the forecast to alert you to the error. The error message will (by
|
||||||
|
default be logged to ``~/li3pystatus-<pid>`` where ``<pid>`` is the PID of
|
||||||
|
the update thread. However, it may be more convenient to manually set the
|
||||||
|
logfile to make the location of the log data predictable and avoid clutter
|
||||||
|
in your home directory. Additionally, using the ``DEBUG`` log level can
|
||||||
|
be helpful in revealing why the module is not working as expected. For
|
||||||
|
example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from i3pystatus import Status
|
||||||
|
from i3pystatus.weather import weathercom
|
||||||
|
|
||||||
|
status = Status(logfile='/home/username/var/i3pystatus.log')
|
||||||
|
|
||||||
|
status.register(
|
||||||
|
'weather',
|
||||||
|
format='{condition} {current_temp}{temp_unit}[ {icon}][ Hi: {high_temp}][ Lo: {low_temp}][ {update_error}]',
|
||||||
|
colorize=True,
|
||||||
|
hints={'markup': 'pango'},
|
||||||
|
update_error='<span color="#ff0000">!</span>',
|
||||||
|
log_level=logging.DEBUG,
|
||||||
|
backend=weathercom.Weathercom(
|
||||||
|
location_code='94107:4:US',
|
||||||
|
units='imperial',
|
||||||
|
log_level=logging.DEBUG,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
The log level must be set separately in both the module and backend
|
||||||
|
contexts.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
settings = (
|
settings = (
|
||||||
@ -77,7 +167,10 @@ class Weather(IntervalModule):
|
|||||||
('color', 'Display color (or fallback color if ``colorize`` is True). '
|
('color', 'Display color (or fallback color if ``colorize`` is True). '
|
||||||
'If not specified, falls back to default i3bar color.'),
|
'If not specified, falls back to default i3bar color.'),
|
||||||
('backend', 'Weather backend instance'),
|
('backend', 'Weather backend instance'),
|
||||||
'interval',
|
('refresh_icon', 'Text to display (in addition to any text currently '
|
||||||
|
'shown by the module) when refreshing weather data. '
|
||||||
|
'**NOTE:** Depending on how quickly the update is '
|
||||||
|
'performed, the icon may not be displayed.'),
|
||||||
'format',
|
'format',
|
||||||
)
|
)
|
||||||
required = ('backend',)
|
required = ('backend',)
|
||||||
@ -98,16 +191,77 @@ class Weather(IntervalModule):
|
|||||||
color = None
|
color = None
|
||||||
backend = None
|
backend = None
|
||||||
interval = 1800
|
interval = 1800
|
||||||
format = '{current_temp}{temp_unit}'
|
refresh_icon = '⟳'
|
||||||
|
format = '{current_temp}{temp_unit}[ {update_error}]'
|
||||||
|
|
||||||
on_leftclick = 'open_forecast_url'
|
output = {'full_text': ''}
|
||||||
|
|
||||||
def open_forecast_url(self):
|
on_doubleleftclick = ['launch_web']
|
||||||
|
on_leftclick = ['check_weather']
|
||||||
|
|
||||||
|
def launch_web(self):
|
||||||
if self.backend.forecast_url and self.backend.forecast_url != 'N/A':
|
if self.backend.forecast_url and self.backend.forecast_url != 'N/A':
|
||||||
|
self.logger.debug('Launching %s in browser', self.backend.forecast_url)
|
||||||
user_open(self.backend.forecast_url)
|
user_open(self.backend.forecast_url)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
pass
|
if self.backend is None:
|
||||||
|
raise RuntimeError('A backend is required')
|
||||||
|
|
||||||
|
self.backend.data = {
|
||||||
|
'city': '',
|
||||||
|
'condition': '',
|
||||||
|
'observation_time': '',
|
||||||
|
'current_temp': '',
|
||||||
|
'low_temp': '',
|
||||||
|
'high_temp': '',
|
||||||
|
'temp_unit': '',
|
||||||
|
'feelslike': '',
|
||||||
|
'dewpoint': '',
|
||||||
|
'wind_speed': '',
|
||||||
|
'wind_unit': '',
|
||||||
|
'wind_direction': '',
|
||||||
|
'wind_gust': '',
|
||||||
|
'pressure': '',
|
||||||
|
'pressure_unit': '',
|
||||||
|
'pressure_trend': '',
|
||||||
|
'visibility': '',
|
||||||
|
'visibility_unit': '',
|
||||||
|
'humidity': '',
|
||||||
|
'uv_index': '',
|
||||||
|
'update_error': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backend.init()
|
||||||
|
|
||||||
|
self.condition = threading.Condition()
|
||||||
|
self.thread = threading.Thread(target=self.update_thread, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def update_thread(self):
|
||||||
|
try:
|
||||||
|
self.check_weather()
|
||||||
|
while True:
|
||||||
|
with self.condition:
|
||||||
|
self.condition.wait(self.interval)
|
||||||
|
self.check_weather()
|
||||||
|
except Exception:
|
||||||
|
msg = 'Exception in {thread} at {time}, module {name}'.format(
|
||||||
|
thread=threading.current_thread().name,
|
||||||
|
time=time.strftime('%c'),
|
||||||
|
name=self.__class__.__name__,
|
||||||
|
)
|
||||||
|
self.logger.error(msg, exc_info=True)
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
def check_weather(self):
|
||||||
|
'''
|
||||||
|
Check the weather using the configured backend
|
||||||
|
'''
|
||||||
|
self.output['full_text'] = \
|
||||||
|
self.refresh_icon + self.output.get('full_text', '')
|
||||||
|
self.backend.check_weather()
|
||||||
|
self.refresh_display()
|
||||||
|
|
||||||
def get_color_data(self, condition):
|
def get_color_data(self, condition):
|
||||||
'''
|
'''
|
||||||
@ -117,11 +271,13 @@ class Weather(IntervalModule):
|
|||||||
if condition not in self.color_icons:
|
if condition not in self.color_icons:
|
||||||
# Check for similarly-named conditions if no exact match found
|
# Check for similarly-named conditions if no exact match found
|
||||||
condition_lc = condition.lower()
|
condition_lc = condition.lower()
|
||||||
if 'cloudy' in condition_lc:
|
if 'cloudy' in condition_lc or 'clouds' in condition_lc:
|
||||||
if 'partly' in condition_lc:
|
if 'partly' in condition_lc:
|
||||||
condition = 'Partly Cloudy'
|
condition = 'Partly Cloudy'
|
||||||
else:
|
else:
|
||||||
condition = 'Cloudy'
|
condition = 'Cloudy'
|
||||||
|
elif condition_lc == 'overcast':
|
||||||
|
condition = 'Cloudy'
|
||||||
elif 'thunder' in condition_lc or 't-storm' in condition_lc:
|
elif 'thunder' in condition_lc or 't-storm' in condition_lc:
|
||||||
condition = 'Thunderstorm'
|
condition = 'Thunderstorm'
|
||||||
elif 'snow' in condition_lc:
|
elif 'snow' in condition_lc:
|
||||||
@ -139,13 +295,16 @@ class Weather(IntervalModule):
|
|||||||
if condition not in self.color_icons \
|
if condition not in self.color_icons \
|
||||||
else self.color_icons[condition]
|
else self.color_icons[condition]
|
||||||
|
|
||||||
@require(internet)
|
def refresh_display(self):
|
||||||
def run(self):
|
self.logger.debug('Weather data: %s', self.backend.data)
|
||||||
data = self.backend.weather_data()
|
self.backend.data['icon'], condition_color = \
|
||||||
data['icon'], condition_color = self.get_color_data(data['condition'])
|
self.get_color_data(self.backend.data['condition'])
|
||||||
color = condition_color if self.colorize else self.color
|
color = condition_color if self.colorize else self.color
|
||||||
|
|
||||||
self.output = {
|
self.output = {
|
||||||
'full_text': formatp(self.format, **data).strip(),
|
'full_text': formatp(self.format, **self.backend.data).strip(),
|
||||||
'color': color,
|
'color': color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
@ -1,17 +1,120 @@
|
|||||||
from i3pystatus.core.util import internet, require
|
from i3pystatus.core.util import internet, require
|
||||||
from i3pystatus.weather import Backend
|
from i3pystatus.weather import WeatherBackend
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree as ElementTree
|
|
||||||
|
|
||||||
WEATHER_COM_URL = \
|
API_PARAMS = ('api_key', 'lang', 'latitude', 'longitude')
|
||||||
'http://wxdata.weather.com/wxdata/weather/local/%s?unit=%s&dayf=1&cc=*'
|
|
||||||
ON_LEFTCLICK_URL = 'https://weather.com/weather/today/l/%s'
|
API_URL = 'https://api.weather.com/v2/turbo/vt1precipitation;vt1currentdatetime;vt1pollenforecast;vt1dailyForecast;vt1observation?units=%s&language=%s&geocode=%s,%s&format=json&apiKey=%s'
|
||||||
|
|
||||||
|
FORECAST_URL = 'https://weather.com/weather/today/l/%s'
|
||||||
|
|
||||||
|
|
||||||
class Weathercom(Backend):
|
class WeathercomHTMLParser(HTMLParser):
|
||||||
|
'''
|
||||||
|
Obtain data points required by the Weather.com API which are obtained
|
||||||
|
through some other source at runtime and added as <script> elements to the
|
||||||
|
page source.
|
||||||
|
'''
|
||||||
|
def __init__(self, logger, location_code):
|
||||||
|
self.logger = logger
|
||||||
|
self.location_code = location_code
|
||||||
|
for attr in API_PARAMS:
|
||||||
|
setattr(self, attr, None)
|
||||||
|
# Not required for API call, but still parsed from the forecast page
|
||||||
|
self.city_name = ''
|
||||||
|
super(WeathercomHTMLParser, self).__init__()
|
||||||
|
|
||||||
|
def safe_eval(self, data):
|
||||||
|
'''
|
||||||
|
Execute an eval with no builtins and no locals
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
return eval(data, {'__builtins__': None}, {})
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.log(
|
||||||
|
5,
|
||||||
|
'Failed to eval() data: %s\n\nOriginal data follows:\n%s',
|
||||||
|
exc, data
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def read_forecast_page(self):
|
||||||
|
with urlopen(FORECAST_URL % self.location_code) as content:
|
||||||
|
try:
|
||||||
|
content_type = dict(content.getheaders())['Content-Type']
|
||||||
|
charset = re.search(r'charset=(.*)', content_type).group(1)
|
||||||
|
except AttributeError:
|
||||||
|
charset = 'utf-8'
|
||||||
|
html = content.read().decode(charset)
|
||||||
|
try:
|
||||||
|
self.feed(html)
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.debug(
|
||||||
|
'Exception raised while parsing forecast page',
|
||||||
|
exc
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_data(self, content):
|
||||||
|
try:
|
||||||
|
tag_text = self.get_starttag_text().lower()
|
||||||
|
except AttributeError:
|
||||||
|
tag_text = ''
|
||||||
|
if tag_text == '<script>':
|
||||||
|
if 'apiKey' in content:
|
||||||
|
# Key is part of a javascript data structure which looks
|
||||||
|
# similar to the following:
|
||||||
|
#
|
||||||
|
# 'sunTurbo': {
|
||||||
|
# 'baseUrl': 'https://api.weather.com',
|
||||||
|
# 'apiKey': 'c1ea9f47f6a88b9acb43aba7faf389d4',
|
||||||
|
# 'locale': 'en-US' || 'en-us'
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# For our purposes this is close enough to a Python data
|
||||||
|
# structure such that it should be able to be eval'ed to a
|
||||||
|
# Python dict.
|
||||||
|
sunturbo = content.find('\'sunTurbo\'')
|
||||||
|
if sunturbo != -1:
|
||||||
|
# Look for the left curly brace after the 'sunTurbo' key
|
||||||
|
lbrace = content.find('{', sunturbo)
|
||||||
|
if lbrace != -1:
|
||||||
|
# Now look for the right curly brace
|
||||||
|
rbrace = content.find('}', lbrace)
|
||||||
|
if rbrace != -1:
|
||||||
|
api_info = content[lbrace:rbrace + 1]
|
||||||
|
# Change '||' to 'or' to allow it to be eval'ed
|
||||||
|
api_info = api_info.replace('||', 'or')
|
||||||
|
api_data = self.safe_eval(api_info)
|
||||||
|
for attr, key in (('api_key', 'apiKey'),
|
||||||
|
('lang', 'locale')):
|
||||||
|
try:
|
||||||
|
setattr(self, attr, api_data[key])
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logger.debug(
|
||||||
|
'\'%s\' key not present in %s',
|
||||||
|
key, api_data
|
||||||
|
)
|
||||||
|
if 'explicit_location' in content and self.location_code in content:
|
||||||
|
lbrace = content.find('{')
|
||||||
|
rbrace = content.rfind('}')
|
||||||
|
if lbrace != rbrace != -1:
|
||||||
|
loc_data = json.loads(content[lbrace:rbrace + 1])
|
||||||
|
for attr, key in (('latitude', 'lat'),
|
||||||
|
('longitude', 'long'),
|
||||||
|
('city_name', 'prsntNm')):
|
||||||
|
try:
|
||||||
|
setattr(self, attr, loc_data[key])
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
self.logger.debug('\'%s\' key not present in %s',
|
||||||
|
key, loc_data)
|
||||||
|
|
||||||
|
|
||||||
|
class Weathercom(WeatherBackend):
|
||||||
'''
|
'''
|
||||||
This module gets the weather from weather.com. The ``location_code``
|
This module gets the weather from weather.com. The ``location_code``
|
||||||
parameter should be set to the location code from weather.com. To obtain
|
parameter should be set to the location code from weather.com. To obtain
|
||||||
@ -27,16 +130,18 @@ class Weathercom(Backend):
|
|||||||
from i3pystatus import Status
|
from i3pystatus import Status
|
||||||
from i3pystatus.weather import weathercom
|
from i3pystatus.weather import weathercom
|
||||||
|
|
||||||
status = Status()
|
status = Status(logfile='/home/username/var/i3pystatus.log')
|
||||||
|
|
||||||
status.register(
|
status.register(
|
||||||
'weather',
|
'weather',
|
||||||
format='{condition} {current_temp}{temp_unit}{icon}\
|
format='{condition} {current_temp}{temp_unit}[ {icon}][ Hi: {high_temp}][ Lo: {low_temp}][ {update_error}]',
|
||||||
[ Hi: {high_temp}] Lo: {low_temp}',
|
interval=900,
|
||||||
colorize=True,
|
colorize=True,
|
||||||
|
hints={'markup': 'pango'},
|
||||||
backend=weathercom.Weathercom(
|
backend=weathercom.Weathercom(
|
||||||
location_code='94107:4:US',
|
location_code='94107:4:US',
|
||||||
units='imperial',
|
units='imperial',
|
||||||
|
update_error='<span color="#ff0000">!</span>',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,91 +153,143 @@ class Weathercom(Backend):
|
|||||||
settings = (
|
settings = (
|
||||||
('location_code', 'Location code from www.weather.com'),
|
('location_code', 'Location code from www.weather.com'),
|
||||||
('units', '\'metric\' or \'imperial\''),
|
('units', '\'metric\' or \'imperial\''),
|
||||||
|
('update_error', 'Value for the ``{update_error}`` formatter when an '
|
||||||
|
'error is encountered while checking weather data'),
|
||||||
)
|
)
|
||||||
required = ('location_code',)
|
required = ('location_code',)
|
||||||
|
|
||||||
location_code = None
|
location_code = None
|
||||||
|
|
||||||
units = 'metric'
|
units = 'metric'
|
||||||
|
update_error = '!'
|
||||||
|
|
||||||
# This will be set once weather data has been checked
|
# This will be set once weather data has been checked
|
||||||
forecast_url = None
|
forecast_url = None
|
||||||
|
|
||||||
@require(internet)
|
@require(internet)
|
||||||
def weather_data(self):
|
def init(self):
|
||||||
'''
|
if self.location_code is None:
|
||||||
Fetches the current weather from wxdata.weather.com service.
|
raise RuntimeError('A location_code is required')
|
||||||
'''
|
self.location_code = str(self.location_code)
|
||||||
if self.forecast_url is None and ':' in self.location_code:
|
if ':' in self.location_code:
|
||||||
# Set the URL so that clicking the weather will launch the
|
# Set the URL so that clicking the weather will launch the
|
||||||
# weather.com forecast page. Only set it though if there is a colon
|
# weather.com forecast page. Only set it though if there is a colon
|
||||||
# in the location_code. Technically, the weather.com API will
|
# in the location_code. Technically, the weather.com API will
|
||||||
# successfully return weather data if a U.S. ZIP code is used as
|
# successfully return weather data if a U.S. ZIP code is used as
|
||||||
# the location_code (e.g. 94107), but if substituted in
|
# the location_code (e.g. 94107), but if substituted in
|
||||||
# ON_LEFTCLICK_URL it may or may not result in a valid URL.
|
# FORECAST_URl it may or may not result in a valid URL.
|
||||||
self.forecast_url = ON_LEFTCLICK_URL % self.location_code
|
self.forecast_url = FORECAST_URL % self.location_code
|
||||||
|
|
||||||
unit = '' if self.units == 'imperial' or self.units == '' else 'm'
|
parser = WeathercomHTMLParser(self.logger, self.location_code)
|
||||||
url = WEATHER_COM_URL % (self.location_code, unit)
|
parser.read_forecast_page()
|
||||||
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
|
for attr in API_PARAMS:
|
||||||
# space, hence the use of rpartition). International timezones (or ones
|
value = getattr(parser, attr, None)
|
||||||
# outside the system locale) don't seem to be handled well by
|
if value is None:
|
||||||
# datetime.datetime.strptime().
|
raise RuntimeError(
|
||||||
try:
|
'Unable to parse %s from forecast page' % attr)
|
||||||
observation_time_str = doc.findtext('cc/lsup').rpartition(' ')[0]
|
setattr(self, attr, value)
|
||||||
observation_time = datetime.strptime(observation_time_str,
|
self.city_name = parser.city_name
|
||||||
'%m/%d/%y %I:%M %p')
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
observation_time = datetime.fromtimestamp(0)
|
|
||||||
|
|
||||||
pressure_trend_str = doc.findtext('cc/bar/d').lower()
|
units = 'e' if self.units == 'imperial' or self.units == '' else 'm'
|
||||||
if pressure_trend_str == 'rising':
|
self.url = API_URL % (
|
||||||
pressure_trend = '+'
|
'e' if self.units in ('imperial', '') else 'm',
|
||||||
elif pressure_trend_str == 'falling':
|
self.lang, self.latitude, self.longitude, self.api_key
|
||||||
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'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def check_response(self, response):
|
||||||
|
# Errors for weather.com API manifest in HTTP error codes, not in the
|
||||||
|
# JSON response.
|
||||||
|
return False
|
||||||
|
|
||||||
|
@require(internet)
|
||||||
|
def check_weather(self):
|
||||||
|
'''
|
||||||
|
Fetches the current weather from wxdata.weather.com service.
|
||||||
|
'''
|
||||||
|
self.data['update_error'] = ''
|
||||||
|
try:
|
||||||
|
response = self.api_request(self.url)
|
||||||
|
if not response:
|
||||||
|
self.data['update_error'] = self.update_error
|
||||||
|
return
|
||||||
|
|
||||||
|
observed = response.get('vt1observation', {})
|
||||||
|
forecast = response.get('vt1dailyForecast', {})
|
||||||
|
|
||||||
|
# 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().
|
||||||
|
try:
|
||||||
|
observation_time_str = str(observed.get('observationTime', ''))
|
||||||
|
observation_time = datetime.strptime(observation_time_str,
|
||||||
|
'%Y-%d-%yT%H:%M:%S%z')
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
observation_time = datetime.fromtimestamp(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pressure_trend_str = observed.get('barometerTrend', '').lower()
|
||||||
|
except AttributeError:
|
||||||
|
pressure_trend_str = ''
|
||||||
|
|
||||||
|
if pressure_trend_str == 'rising':
|
||||||
|
pressure_trend = '+'
|
||||||
|
elif pressure_trend_str == 'falling':
|
||||||
|
pressure_trend = '-'
|
||||||
|
else:
|
||||||
|
pressure_trend = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
high_temp = forecast.get('day', {}).get('temperature', [])[0]
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
high_temp = ''
|
||||||
|
else:
|
||||||
|
if high_temp is None:
|
||||||
|
# In the mid-afternoon, the high temp disappears from the
|
||||||
|
# forecast, so just set high_temp to an empty string.
|
||||||
|
high_temp = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
low_temp = forecast.get('night', {}).get('temperature', [])[0]
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
low_temp = ''
|
||||||
|
|
||||||
|
if self.units == 'imperial':
|
||||||
|
temp_unit = '°F'
|
||||||
|
wind_unit = 'mph'
|
||||||
|
pressure_unit = 'in'
|
||||||
|
visibility_unit = 'mi'
|
||||||
|
else:
|
||||||
|
temp_unit = '°C'
|
||||||
|
wind_unit = 'kph'
|
||||||
|
pressure_unit = 'mb'
|
||||||
|
visibility_unit = 'km'
|
||||||
|
|
||||||
|
self.data['city'] = self.city_name
|
||||||
|
self.data['condition'] = str(observed.get('phrase', ''))
|
||||||
|
self.data['observation_time'] = observation_time
|
||||||
|
self.data['current_temp'] = str(observed.get('temperature', ''))
|
||||||
|
self.data['low_temp'] = str(low_temp)
|
||||||
|
self.data['high_temp'] = str(high_temp)
|
||||||
|
self.data['temp_unit'] = temp_unit
|
||||||
|
self.data['feelslike'] = str(observed.get('feelsLike', ''))
|
||||||
|
self.data['dewpoint'] = str(observed.get('dewPoint', ''))
|
||||||
|
self.data['wind_speed'] = str(observed.get('windSpeed', ''))
|
||||||
|
self.data['wind_unit'] = wind_unit
|
||||||
|
self.data['wind_direction'] = str(observed.get('windDirCompass', ''))
|
||||||
|
# Gust can be None, using "or" to ensure empty string in this case
|
||||||
|
self.data['wind_gust'] = str(observed.get('gust', '') or '')
|
||||||
|
self.data['pressure'] = str(observed.get('altimeter', ''))
|
||||||
|
self.data['pressure_unit'] = pressure_unit
|
||||||
|
self.data['pressure_trend'] = pressure_trend
|
||||||
|
self.data['visibility'] = str(observed.get('visibility', ''))
|
||||||
|
self.data['visibility_unit'] = visibility_unit
|
||||||
|
self.data['humidity'] = str(observed.get('humidity', ''))
|
||||||
|
self.data['uv_index'] = str(observed.get('uvIndex', ''))
|
||||||
|
except Exception:
|
||||||
|
# Don't let an uncaught exception kill the update thread
|
||||||
|
self.logger.error(
|
||||||
|
'Uncaught error occurred while checking weather. '
|
||||||
|
'Exception follows:', exc_info=True
|
||||||
|
)
|
||||||
|
self.data['update_error'] = self.update_error
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
from i3pystatus import IntervalModule
|
|
||||||
from i3pystatus.core.util import internet, require
|
from i3pystatus.core.util import internet, require
|
||||||
|
from i3pystatus.weather import WeatherBackend
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
import json
|
|
||||||
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_QUERY_URL = 'http://api.wunderground.com/api/%s/%s/q/%s.json'
|
STATION_QUERY_URL = 'http://api.wunderground.com/api/%s/%s/q/%s.json'
|
||||||
|
|
||||||
|
|
||||||
class Wunderground(IntervalModule):
|
class Wunderground(WeatherBackend):
|
||||||
'''
|
'''
|
||||||
This module retrieves weather data using the Weather Underground API.
|
This module retrieves weather data using the Weather Underground API.
|
||||||
|
|
||||||
@ -19,10 +17,6 @@ class Wunderground(IntervalModule):
|
|||||||
sign up 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, 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:
|
||||||
|
|
||||||
* **State/City_Name** - CA/San_Francisco
|
* **State/City_Name** - CA/San_Francisco
|
||||||
@ -34,11 +28,29 @@ class Wunderground(IntervalModule):
|
|||||||
* **Personal Weather Station (PWS)** - pws:KILCHICA30
|
* **Personal Weather Station (PWS)** - pws:KILCHICA30
|
||||||
|
|
||||||
When not using a ``pws`` or ``icao`` station ID, the location will be
|
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
|
queried (this uses an API query), and the closest station will be used.
|
||||||
station IDs, visit the following URL:
|
For a list of PWS station IDs, visit the following URL:
|
||||||
|
|
||||||
http://www.wunderground.com/weatherstation/ListStations.asp
|
http://www.wunderground.com/weatherstation/ListStations.asp
|
||||||
|
|
||||||
|
.. rubric:: API usage
|
||||||
|
|
||||||
|
An 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 (the default is 1800 seconds, or 30
|
||||||
|
minutes), and one should be careful how often one restarts i3pystatus
|
||||||
|
and how often a refresh is forced by left-clicking the module.
|
||||||
|
|
||||||
|
As noted above, when not using a ``pws`` or ``icao`` station ID, an API
|
||||||
|
query will be used to determine the station ID to use. This will be
|
||||||
|
done once when i3pystatus is started, and not repeated until the next
|
||||||
|
time i3pystatus is started.
|
||||||
|
|
||||||
|
When updating weather data, one API query will be used to obtain the
|
||||||
|
current conditions. The high/low temperature forecast requires an
|
||||||
|
additonal API query, and is optional (disabled by default). To enable
|
||||||
|
forecast checking, set ``forecast=True``.
|
||||||
|
|
||||||
.. _weather-usage-wunderground:
|
.. _weather-usage-wunderground:
|
||||||
|
|
||||||
.. rubric:: Usage example
|
.. rubric:: Usage example
|
||||||
@ -48,16 +60,19 @@ class Wunderground(IntervalModule):
|
|||||||
from i3pystatus import Status
|
from i3pystatus import Status
|
||||||
from i3pystatus.weather import wunderground
|
from i3pystatus.weather import wunderground
|
||||||
|
|
||||||
status = Status()
|
status = Status(logfile='/home/username/var/i3pystatus.log')
|
||||||
|
|
||||||
status.register(
|
status.register(
|
||||||
'weather',
|
'weather',
|
||||||
format='{condition} {current_temp}{temp_unit}{icon}[ Hi: {high_temp}] Lo: {low_temp}',
|
format='{condition} {current_temp}{temp_unit}[ {icon}][ Hi: {high_temp}][ Lo: {low_temp}][ {update_error}]',
|
||||||
colorize=True,
|
colorize=True,
|
||||||
|
hints={'markup': 'pango'},
|
||||||
backend=wunderground.Wunderground(
|
backend=wunderground.Wunderground(
|
||||||
api_key='dbafe887d56ba4ad',
|
api_key='dbafe887d56ba4ad',
|
||||||
location_code='pws:MAT645',
|
location_code='pws:MAT645',
|
||||||
units='imperial',
|
units='imperial',
|
||||||
|
forecast=True,
|
||||||
|
update_error='<span color="#ff0000">!</span>',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,9 +81,6 @@ class Wunderground(IntervalModule):
|
|||||||
See :ref:`here <weather-formatters>` for a list of formatters which can be
|
See :ref:`here <weather-formatters>` for a list of formatters which can be
|
||||||
used.
|
used.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
interval = 300
|
|
||||||
|
|
||||||
settings = (
|
settings = (
|
||||||
('api_key', 'Weather Underground API key'),
|
('api_key', 'Weather Underground API key'),
|
||||||
('location_code', 'Location code from wunderground.com'),
|
('location_code', 'Location code from wunderground.com'),
|
||||||
@ -78,6 +90,8 @@ class Wunderground(IntervalModule):
|
|||||||
'additional API request per weather update). If set to '
|
'additional API request per weather update). If set to '
|
||||||
'``False``, then the ``low_temp`` and ``high_temp`` '
|
'``False``, then the ``low_temp`` and ``high_temp`` '
|
||||||
'formatters will be set to empty strings.'),
|
'formatters will be set to empty strings.'),
|
||||||
|
('update_error', 'Value for the ``{update_error}`` formatter when an '
|
||||||
|
'error is encountered while checking weather data'),
|
||||||
)
|
)
|
||||||
|
|
||||||
required = ('api_key', 'location_code')
|
required = ('api_key', 'location_code')
|
||||||
@ -87,71 +101,53 @@ class Wunderground(IntervalModule):
|
|||||||
units = 'metric'
|
units = 'metric'
|
||||||
use_pws = True
|
use_pws = True
|
||||||
forecast = False
|
forecast = False
|
||||||
|
update_error = '!'
|
||||||
|
|
||||||
# These will be set once weather data has been checked
|
# These will be set once weather data has been checked
|
||||||
station_id = None
|
station_id = None
|
||||||
forecast_url = None
|
forecast_url = None
|
||||||
|
|
||||||
@require(internet)
|
@require(internet)
|
||||||
def api_request(self, url):
|
def init(self):
|
||||||
'''
|
|
||||||
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
|
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
|
station. If the location is a pws or icao station ID, no lookup will be
|
||||||
peformed.
|
peformed.
|
||||||
'''
|
'''
|
||||||
if self.station_id is None:
|
try:
|
||||||
try:
|
for no_lookup in ('pws', 'icao'):
|
||||||
for no_lookup in ('pws', 'icao'):
|
sid = self.location_code.partition(no_lookup + ':')[-1]
|
||||||
sid = self.location_code.partition(no_lookup + ':')[-1]
|
if sid:
|
||||||
if sid:
|
self.station_id = self.location_code
|
||||||
self.station_id = self.location_code
|
return
|
||||||
return
|
except AttributeError:
|
||||||
except AttributeError:
|
# Numeric or some other type, either way we'll just stringify
|
||||||
# Numeric or some other type, either way we'll just stringify
|
# it below and perform a lookup.
|
||||||
# it below and perform a lookup.
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
extra_opts = '/pws:0' if not self.use_pws else ''
|
extra_opts = '/pws:0' if not self.use_pws else ''
|
||||||
api_url = GEOLOOKUP_URL % (self.api_key,
|
api_url = GEOLOOKUP_URL % (self.api_key,
|
||||||
extra_opts,
|
extra_opts,
|
||||||
self.location_code)
|
self.location_code)
|
||||||
response = self.api_request(api_url)
|
response = self.api_request(api_url)
|
||||||
station_type = 'pws' if self.use_pws else 'airport'
|
station_type = 'pws' if self.use_pws else 'airport'
|
||||||
try:
|
try:
|
||||||
stations = response['location']['nearby_weather_stations']
|
stations = response['location']['nearby_weather_stations']
|
||||||
nearest = stations[station_type]['station'][0]
|
nearest = stations[station_type]['station'][0]
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
raise Exception('No locations matched location_code %s'
|
raise Exception(
|
||||||
% self.location_code)
|
'No locations matched location_code %s' % self.location_code)
|
||||||
|
|
||||||
if self.use_pws:
|
if self.use_pws:
|
||||||
nearest_pws = nearest.get('id', '')
|
nearest_pws = nearest.get('id', '')
|
||||||
if not nearest_pws:
|
if not nearest_pws:
|
||||||
raise Exception('No id entry for station')
|
raise Exception('No id entry for nearest PWS')
|
||||||
self.station_id = 'pws:%s' % nearest_pws
|
self.station_id = 'pws:%s' % nearest_pws
|
||||||
else:
|
else:
|
||||||
nearest_airport = nearest.get('icao', '')
|
nearest_airport = nearest.get('icao', '')
|
||||||
if not nearest_airport:
|
if not nearest_airport:
|
||||||
raise Exception('No icao entry for station')
|
raise Exception('No icao entry for nearest airport')
|
||||||
self.station_id = 'icao:%s' % nearest_airport
|
self.station_id = 'icao:%s' % nearest_airport
|
||||||
|
|
||||||
@require(internet)
|
@require(internet)
|
||||||
def get_forecast(self):
|
def get_forecast(self):
|
||||||
@ -160,6 +156,7 @@ class Wunderground(IntervalModule):
|
|||||||
data for the configured/queried weather station, and return the low and
|
data for the configured/queried weather station, and return the low and
|
||||||
high temperatures. Otherwise, return two empty strings.
|
high temperatures. Otherwise, return two empty strings.
|
||||||
'''
|
'''
|
||||||
|
no_data = ('', '')
|
||||||
if self.forecast:
|
if self.forecast:
|
||||||
query_url = STATION_QUERY_URL % (self.api_key,
|
query_url = STATION_QUERY_URL % (self.api_key,
|
||||||
'forecast',
|
'forecast',
|
||||||
@ -168,73 +165,110 @@ class Wunderground(IntervalModule):
|
|||||||
response = self.api_request(query_url)['forecast']
|
response = self.api_request(query_url)['forecast']
|
||||||
response = response['simpleforecast']['forecastday'][0]
|
response = response['simpleforecast']['forecastday'][0]
|
||||||
except (KeyError, IndexError, TypeError):
|
except (KeyError, IndexError, TypeError):
|
||||||
raise Exception('No forecast data found for %s' % self.station_id)
|
self.logger.error(
|
||||||
|
'No forecast data found for %s', self.station_id)
|
||||||
|
self.data['update_error'] = self.update_error
|
||||||
|
return no_data
|
||||||
|
|
||||||
unit = 'celsius' if self.units == 'metric' else 'fahrenheit'
|
unit = 'celsius' if self.units == 'metric' else 'fahrenheit'
|
||||||
low_temp = response.get('low', {}).get(unit, '')
|
low_temp = response.get('low', {}).get(unit, '')
|
||||||
high_temp = response.get('high', {}).get(unit, '')
|
high_temp = response.get('high', {}).get(unit, '')
|
||||||
return low_temp, high_temp
|
return low_temp, high_temp
|
||||||
else:
|
else:
|
||||||
return '', ''
|
return no_data
|
||||||
|
|
||||||
|
def check_response(self, response):
|
||||||
|
try:
|
||||||
|
return response['response']['error']['description']
|
||||||
|
except KeyError:
|
||||||
|
# No error in response
|
||||||
|
return False
|
||||||
|
|
||||||
@require(internet)
|
@require(internet)
|
||||||
def weather_data(self):
|
def check_weather(self):
|
||||||
'''
|
'''
|
||||||
Query the configured/queried station and return the weather data
|
Query the configured/queried station and return the weather data
|
||||||
'''
|
'''
|
||||||
# If necessary, do a geolookup to set the station_id
|
self.data['update_error'] = ''
|
||||||
self.geolookup()
|
|
||||||
|
|
||||||
query_url = STATION_QUERY_URL % (self.api_key,
|
|
||||||
'conditions',
|
|
||||||
self.station_id)
|
|
||||||
try:
|
try:
|
||||||
response = self.api_request(query_url)['current_observation']
|
query_url = STATION_QUERY_URL % (self.api_key,
|
||||||
self.forecast_url = response.pop('ob_url', None)
|
'conditions',
|
||||||
except KeyError:
|
self.station_id)
|
||||||
raise Exception('No weather data found for %s' % self.station_id)
|
try:
|
||||||
|
response = self.api_request(query_url)['current_observation']
|
||||||
|
self.forecast_url = response.pop('ob_url', None)
|
||||||
|
except KeyError:
|
||||||
|
self.logger.error('No weather data found for %s', self.station_id)
|
||||||
|
self.data['update_error'] = self.update_error
|
||||||
|
return
|
||||||
|
|
||||||
low_temp, high_temp = self.get_forecast()
|
if self.forecast:
|
||||||
|
query_url = STATION_QUERY_URL % (self.api_key,
|
||||||
|
'forecast',
|
||||||
|
self.station_id)
|
||||||
|
try:
|
||||||
|
forecast = self.api_request(query_url)['forecast']
|
||||||
|
forecast = forecast['simpleforecast']['forecastday'][0]
|
||||||
|
except (KeyError, IndexError, TypeError):
|
||||||
|
self.logger.error(
|
||||||
|
'No forecast data found for %s', self.station_id)
|
||||||
|
# This is a non-fatal error, so don't return but do set the
|
||||||
|
# error flag.
|
||||||
|
self.data['update_error'] = self.update_error
|
||||||
|
|
||||||
if self.units == 'metric':
|
unit = 'celsius' if self.units == 'metric' else 'fahrenheit'
|
||||||
temp_unit = 'c'
|
low_temp = forecast.get('low', {}).get(unit, '')
|
||||||
speed_unit = 'kph'
|
high_temp = forecast.get('high', {}).get(unit, '')
|
||||||
distance_unit = 'km'
|
else:
|
||||||
pressure_unit = 'mb'
|
low_temp = high_temp = ''
|
||||||
else:
|
|
||||||
temp_unit = 'f'
|
|
||||||
speed_unit = 'mph'
|
|
||||||
distance_unit = 'mi'
|
|
||||||
pressure_unit = 'in'
|
|
||||||
|
|
||||||
def _find(key, data=None):
|
if self.units == 'metric':
|
||||||
data = data or response
|
temp_unit = 'c'
|
||||||
return data.get(key, 'N/A')
|
speed_unit = 'kph'
|
||||||
|
distance_unit = 'km'
|
||||||
|
pressure_unit = 'mb'
|
||||||
|
else:
|
||||||
|
temp_unit = 'f'
|
||||||
|
speed_unit = 'mph'
|
||||||
|
distance_unit = 'mi'
|
||||||
|
pressure_unit = 'in'
|
||||||
|
|
||||||
try:
|
def _find(key, data=None, default=''):
|
||||||
observation_time = int(_find('observation_epoch'))
|
if data is None:
|
||||||
except TypeError:
|
data = response
|
||||||
observation_time = 0
|
return str(data.get(key, default))
|
||||||
|
|
||||||
return dict(
|
try:
|
||||||
city=_find('city', response['observation_location']),
|
observation_time = datetime.fromtimestamp(
|
||||||
condition=_find('weather'),
|
int(_find('observation_epoch'))
|
||||||
observation_time=datetime.fromtimestamp(observation_time),
|
)
|
||||||
current_temp=_find('temp_' + temp_unit),
|
except TypeError:
|
||||||
low_temp=low_temp,
|
observation_time = datetime.fromtimestamp(0)
|
||||||
high_temp=high_temp,
|
|
||||||
temp_unit='°' + temp_unit.upper(),
|
self.data['city'] = _find('city', response['observation_location'])
|
||||||
feelslike=_find('feelslike_' + temp_unit),
|
self.data['condition'] = _find('weather')
|
||||||
dewpoint=_find('dewpoint_' + temp_unit),
|
self.data['observation_time'] = observation_time
|
||||||
wind_speed=_find('wind_' + speed_unit),
|
self.data['current_temp'] = _find('temp_' + temp_unit)
|
||||||
wind_unit=speed_unit,
|
self.data['low_temp'] = low_temp
|
||||||
wind_direction=_find('wind_dir'),
|
self.data['high_temp'] = high_temp
|
||||||
wind_gust=_find('wind_gust_' + speed_unit),
|
self.data['temp_unit'] = '°' + temp_unit.upper()
|
||||||
pressure=_find('pressure_' + pressure_unit),
|
self.data['feelslike'] = _find('feelslike_' + temp_unit)
|
||||||
pressure_unit=pressure_unit,
|
self.data['dewpoint'] = _find('dewpoint_' + temp_unit)
|
||||||
pressure_trend=_find('pressure_trend'),
|
self.data['wind_speed'] = _find('wind_' + speed_unit)
|
||||||
visibility=_find('visibility_' + distance_unit),
|
self.data['wind_unit'] = speed_unit
|
||||||
visibility_unit=distance_unit,
|
self.data['wind_direction'] = _find('wind_dir')
|
||||||
humidity=_find('relative_humidity').rstrip('%'),
|
self.data['wind_gust'] = _find('wind_gust_' + speed_unit)
|
||||||
uv_index=_find('uv'),
|
self.data['pressure'] = _find('pressure_' + pressure_unit)
|
||||||
)
|
self.data['pressure_unit'] = pressure_unit
|
||||||
|
self.data['pressure_trend'] = _find('pressure_trend')
|
||||||
|
self.data['visibility'] = _find('visibility_' + distance_unit)
|
||||||
|
self.data['visibility_unit'] = distance_unit
|
||||||
|
self.data['humidity'] = _find('relative_humidity').rstrip('%')
|
||||||
|
self.data['uv_index'] = _find('UV')
|
||||||
|
except Exception:
|
||||||
|
# Don't let an uncaught exception kill the update thread
|
||||||
|
self.logger.error(
|
||||||
|
'Uncaught error occurred while checking weather. '
|
||||||
|
'Exception follows:', exc_info=True
|
||||||
|
)
|
||||||
|
self.data['update_error'] = self.update_error
|
||||||
|
Loading…
Reference in New Issue
Block a user