Skip to content

Commit

Permalink
Fix both broken weather modules (#789)
Browse files Browse the repository at this point in the history
* Fix both broken weather modules

The Weather.com changed its API again, this fixes that as well as fixing
a bug that caused the weather conditions to always show as "None".

For Weather Underground, the module had been broken for some time due to
the discontinuation of their API. The module has been rewritten to use
the same API calls that the website itself uses.

* Fix double-click browser launch in wunderground module

* Add example of longer location_code for weather.com
  • Loading branch information
terminalmage authored Aug 28, 2020
1 parent a7c24e4 commit d5082fa
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 245 deletions.
74 changes: 32 additions & 42 deletions i3pystatus/weather/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
import threading
import time
from urllib.request import urlopen
from urllib.request import Request, urlopen

from i3pystatus import SettingsBase, IntervalModule, formatp
from i3pystatus.core.util import user_open, internet, require
Expand All @@ -12,41 +12,46 @@ class WeatherBackend(SettingsBase):
settings = ()

@require(internet)
def api_request(self, url):
def http_request(self, url, headers=None):
req = Request(url, headers=headers or {})
with urlopen(req) as content:
try:
content_type = dict(content.getheaders())['Content-Type']
charset = re.search(r'charset=(.*)', content_type).group(1)
except AttributeError:
charset = 'utf-8'
return content.read().decode(charset)

@require(internet)
def api_request(self, url, headers=None):
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
response_json = self.http_request(url, headers=headers).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
def check_response(self, response):
return False


class Weather(IntervalModule):
Expand Down Expand Up @@ -99,21 +104,6 @@ class Weather(IntervalModule):
syntax to conditionally show the value of the **update_error** config value
when the backend encounters an error during an update.
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}] Lo: {low_temp}[ {update_error}]
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:
Expand Down
22 changes: 8 additions & 14 deletions i3pystatus/weather/weathercom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
from i3pystatus.core.util import internet, require
from i3pystatus.weather import WeatherBackend

USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0'


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.
'''
user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0'

def __init__(self, logger):
self.logger = logger
Expand All @@ -24,7 +23,7 @@ def __init__(self, logger):
def get_weather_data(self, url):
self.logger.debug('Making request to %s to retrieve weather data', url)
self.weather_data = None
req = Request(url, headers={'User-Agent': USER_AGENT})
req = Request(url, headers={'User-Agent': self.user_agent})
with urlopen(req) as content:
try:
content_type = dict(content.getheaders())['Content-Type']
Expand Down Expand Up @@ -98,7 +97,8 @@ class Weathercom(WeatherBackend):
parameter should be set to the location code from weather.com. To obtain
this code, search for your location on weather.com, and when you go to the
forecast page, the code you need will be everything after the last slash in
the URL (e.g. ``94107:4:US``).
the URL (e.g. ``94107:4:US``, or
``8b7867695971473a260df2c5d49ff92dc9079dcb673c545f5f107f5c4ab30732``).
.. _weather-usage-weathercom:
Expand Down Expand Up @@ -154,16 +154,11 @@ def init(self):

# Setting the locale to en-AU returns units in metric. Leaving it blank
# causes weather.com to return the default, which is imperial.
self.locale = 'en-AU' if self.units == 'metric' else ''
self.locale = 'en-CA' if self.units == 'metric' else ''

self.forecast_url = self.url_template.format(**vars(self))
self.parser = WeathercomHTMLParser(self.logger)

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):
'''
Expand Down Expand Up @@ -211,7 +206,7 @@ def check_weather(self):
return

try:
forecast = self.parser.weather_data['getSunV3DailyForecastUrlConfig']
forecast = self.parser.weather_data['getSunV3CurrentObservationsUrlConfig']
# Same as above, use next(iter(forecast)) to drill down to the
# correct nested dict level.
forecast = forecast[next(iter(forecast))]['data']
Expand Down Expand Up @@ -255,14 +250,13 @@ def check_weather(self):
else:
pressure_trend = ''

self.logger.critical('forecast = %s', forecast)
try:
high_temp = forecast.get('temperatureMax', [])[0] or ''
high_temp = forecast.get('temperatureMax24Hour', '')
except (AttributeError, IndexError):
high_temp = ''

try:
low_temp = forecast.get('temperatureMin', [])[0]
low_temp = forecast.get('temperatureMin24Hour', '')
except (AttributeError, IndexError):
low_temp = ''

Expand Down
Loading

0 comments on commit d5082fa

Please sign in to comment.