Update weather.com weather backend to reflect API changes (#783)

This commit is contained in:
Erik Johnson 2020-06-12 10:31:28 -05:00 committed by GitHub
parent e0234c1223
commit dad5eb0c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -67,53 +67,27 @@ class WeathercomHTMLParser(HTMLParser):
# Look for feed information embedded as a javascript variable # Look for feed information embedded as a javascript variable
begin = content.find('window.__data') begin = content.find('window.__data')
if begin != -1: if begin != -1:
weather_data = None
self.logger.debug('Located window.__data') self.logger.debug('Located window.__data')
# Look for end of JSON dict and end of javascript statement # Strip the "window.__data=" from the beginning and load json
end = content.find('};', begin)
if end == -1:
self.logger.debug('Failed to locate end of javascript statement')
else:
# Strip the "window.__data=" from the beginning
json_data = self.load_json( json_data = self.load_json(
content[begin:end + 1].split('=', 1)[1].lstrip() content[begin:].split('=', 1)[1].lstrip()
) )
if json_data is not None: if json_data is not None:
def _find_weather_data(data): try:
''' weather_data = json_data['dal']
Helper designed to minimize impact of potential except KeyError:
structural changes to this data. pass
'''
if isinstance(data, dict):
if 'Observation' in data and 'DailyForecast' in data:
return data
else:
for key in data:
ret = _find_weather_data(data[key])
if ret is not None:
return ret
return None
weather_data = _find_weather_data(json_data)
if weather_data is None: if weather_data is None:
self.logger.debug( self.logger.debug(
'Failed to locate weather data in the ' 'Failed to locate weather data in the '
'following data structure: %s', json_data 'following data: %s', json_data
) )
else: else:
self.weather_data = weather_data self.weather_data = weather_data
return return
for line in content.splitlines():
line = line.strip().rstrip(';')
if line.startswith('var adaptorParams'):
# Strip off the "var adaptorParams = " from the beginning,
# and the javascript semicolon from the end. This will give
# us JSON that we can load.
weather_data = self.load_json(line.split('=', 1)[1].lstrip())
if weather_data is not None:
self.weather_data = weather_data
return
class Weathercom(WeatherBackend): class Weathercom(WeatherBackend):
''' '''
@ -216,15 +190,15 @@ class Weathercom(WeatherBackend):
return return
try: try:
observed = self.parser.weather_data['Observation'] observed = self.parser.weather_data['getSunV3CurrentObservationsUrlConfig']
# Observation data stored under a sub-key containing the # Observation data stored under a sub-key containing the
# lat/long coordinates. For example: # lat/long coordinates, locale info, etc. For example:
# #
# geocode:41.77,-88.35:language:en-US:units:e # geocode:41.77,-88.35:language:en-US:units:e
# #
# Since this is the only key under "Observation", we can just # Since this is the only key under "Observation", we can just
# use next(iter(observed)) to get it. # use next(iter(observed)) to get it.
observed = observed[next(iter(observed))]['data']['vt1observation'] observed = observed[next(iter(observed))]['data']
except KeyError: except KeyError:
self.logger.error( self.logger.error(
'Failed to retrieve current conditions from API response. ' 'Failed to retrieve current conditions from API response. '
@ -234,11 +208,10 @@ class Weathercom(WeatherBackend):
return return
try: try:
forecast = self.parser.weather_data['DailyForecast'] forecast = self.parser.weather_data['getSunV3DailyForecastUrlConfig']
# Same as above, use next(iter(forecast)) to drill down to the # Same as above, use next(iter(forecast)) to drill down to the
# correct nested dict level. # correct nested dict level.
forecast = forecast[next(iter(forecast))] forecast = forecast[next(iter(forecast))]['data']
forecast = forecast['data']['vt1dailyForecast'][0]
except (IndexError, KeyError): except (IndexError, KeyError):
self.logger.error( self.logger.error(
'Failed to retrieve forecast data from API response. ' 'Failed to retrieve forecast data from API response. '
@ -248,11 +221,11 @@ class Weathercom(WeatherBackend):
return return
try: try:
self.city_name = self.parser.weather_data['Location'] location = self.parser.weather_data['getSunV3LocationPointUrlConfig']
# Again, same technique as above used to get down to the # Again, same technique as above used to get down to the
# correct nested dict level. # correct nested dict level.
self.city_name = self.city_name[next(iter(self.city_name))] location = location[next(iter(location))]
self.city_name = self.city_name['data']['location']['displayName'] self.city_name = location['data']['location']['displayName']
except KeyError: except KeyError:
self.logger.warning( self.logger.warning(
'Failed to get city name from API response, falling back ' 'Failed to get city name from API response, falling back '
@ -260,19 +233,15 @@ class Weathercom(WeatherBackend):
) )
self.city_name = self.location_code self.city_name = self.location_code
# 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: try:
observation_time_str = str(observed.get('observationTime', '')) observation_time_str = str(observed.get('validTimeLocal', ''))
observation_time = datetime.strptime(observation_time_str, observation_time = datetime.strptime(observation_time_str,
'%Y-%d-%yT%H:%M:%S%z') '%Y-%d-%yT%H:%M:%S%z')
except (ValueError, AttributeError): except (ValueError, AttributeError):
observation_time = datetime.fromtimestamp(0) observation_time = datetime.fromtimestamp(0)
try: try:
pressure_trend_str = observed.get('barometerTrend', '').lower() pressure_trend_str = observed.get('pressureTendencyTrend', '').lower()
except AttributeError: except AttributeError:
pressure_trend_str = '' pressure_trend_str = ''
@ -283,18 +252,14 @@ class Weathercom(WeatherBackend):
else: else:
pressure_trend = '' pressure_trend = ''
self.logger.critical('forecast = %s', forecast)
try: try:
high_temp = forecast.get('day', {}).get('temperature', '') high_temp = forecast.get('temperatureMax', [])[0] or ''
except (AttributeError, IndexError): except (AttributeError, IndexError):
high_temp = '' 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: try:
low_temp = forecast.get('night', {}).get('temperature', '') low_temp = forecast.get('temperatureMin', [])[0]
except (AttributeError, IndexError): except (AttributeError, IndexError):
low_temp = '' low_temp = ''
@ -310,25 +275,25 @@ class Weathercom(WeatherBackend):
visibility_unit = 'km' visibility_unit = 'km'
self.data['city'] = self.city_name self.data['city'] = self.city_name
self.data['condition'] = str(observed.get('phrase', '')) self.data['condition'] = str(observed.get('wxPhraseMedium', ''))
self.data['observation_time'] = observation_time self.data['observation_time'] = observation_time
self.data['current_temp'] = str(observed.get('temperature', '')) self.data['current_temp'] = str(observed.get('temperature', ''))
self.data['low_temp'] = str(low_temp) self.data['low_temp'] = str(low_temp)
self.data['high_temp'] = str(high_temp) self.data['high_temp'] = str(high_temp)
self.data['temp_unit'] = temp_unit self.data['temp_unit'] = temp_unit
self.data['feelslike'] = str(observed.get('feelsLike', '')) self.data['feelslike'] = str(observed.get('temperatureFeelsLike', ''))
self.data['dewpoint'] = str(observed.get('dewPoint', '')) self.data['dewpoint'] = str(observed.get('temperatureDewPoint', ''))
self.data['wind_speed'] = str(observed.get('windSpeed', '')) self.data['wind_speed'] = str(observed.get('windSpeed', ''))
self.data['wind_unit'] = wind_unit self.data['wind_unit'] = wind_unit
self.data['wind_direction'] = str(observed.get('windDirCompass', '')) self.data['wind_direction'] = str(observed.get('windDirectionCardinal', ''))
# Gust can be None, using "or" to ensure empty string in this case # Gust can be None, using "or" to ensure empty string in this case
self.data['wind_gust'] = str(observed.get('gust', '') or '') self.data['wind_gust'] = str(observed.get('windGust', '') or '')
self.data['pressure'] = str(observed.get('altimeter', '')) self.data['pressure'] = str(observed.get('pressureAltimeter', ''))
self.data['pressure_unit'] = pressure_unit self.data['pressure_unit'] = pressure_unit
self.data['pressure_trend'] = pressure_trend self.data['pressure_trend'] = pressure_trend
self.data['visibility'] = str(observed.get('visibility', '')) self.data['visibility'] = str(observed.get('visibility', ''))
self.data['visibility_unit'] = visibility_unit self.data['visibility_unit'] = visibility_unit
self.data['humidity'] = str(observed.get('humidity', '')) self.data['humidity'] = str(observed.get('relativeHumidity', ''))
self.data['uv_index'] = str(observed.get('uvIndex', '')) self.data['uv_index'] = str(observed.get('uvIndex', ''))
except Exception: except Exception:
# Don't let an uncaught exception kill the update thread # Don't let an uncaught exception kill the update thread