Added the ability to Weather Underground to track severe weather alerts (#3505)

*  Added the ability to Weather Underground to track severe weather alerts

*   * Added message on the advisory attr

  * Updated tests

* * Making use of guard clause

* Checking multiple_alerts prior loop

* Using a better way to create dict

* Fixed issue to set to None only the object that failed

* Added unittest

* Split update() method to different calls with their one throttle control to minimize API calls

* Updated unittest and make sure the alert sensor will not return 'unknown' status'

* Removed update() method from state property

* Branch rebased and include Weather Underground attribution

* Update wunderground.py
This commit is contained in:
Marcelo Moreira de Mello 2016-10-15 00:35:27 -04:00 committed by Paulus Schoutsen
parent 1bf5554017
commit 6fcb1b548e
2 changed files with 86 additions and 24 deletions

View File

@ -19,6 +19,7 @@ from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/'
_ALERTS = 'http://api.wunderground.com/api/{}/alerts/q/'
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by the WUnderground weather service"
@ -28,6 +29,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
# Sensor types are defined like: Name, units
SENSOR_TYPES = {
'alerts': ['Alerts', None],
'weather': ['Weather Summary', None],
'station_id': ['Station ID', None],
'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS],
@ -57,6 +59,14 @@ SENSOR_TYPES = {
'solarradiation': ['Solar Radiation', None]
}
# Alert Attributes
ALERTS_ATTRS = [
'date',
'description',
'expires',
'message',
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PWS_ID): cv.string,
@ -106,15 +116,31 @@ class WUndergroundSensor(Entity):
return int(self.rest.data[self._condition][:-1])
else:
return self.rest.data[self._condition]
else:
return STATE_UNKNOWN
if self.rest.alerts and self._condition == 'alerts':
return len(self.rest.alerts)
return STATE_UNKNOWN
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
}
attrs = {}
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
if not self.rest.alerts or self._condition != 'alerts':
return attrs
multiple_alerts = len(self.rest.alerts) > 1
for data in self.rest.alerts:
for alert in ALERTS_ATTRS:
if data[alert]:
if multiple_alerts:
dkey = alert.capitalize() + '_' + data['type']
else:
dkey = alert.capitalize()
attrs[dkey] = data[alert]
return attrs
@property
def entity_picture(self):
@ -129,7 +155,10 @@ class WUndergroundSensor(Entity):
def update(self):
"""Update current conditions."""
self.rest.update()
if self._condition == 'alerts':
self.rest.update_alerts()
else:
self.rest.update()
# pylint: disable=too-few-public-methods
@ -144,9 +173,10 @@ class WUndergroundData(object):
self._latitude = hass.config.latitude
self._longitude = hass.config.longitude
self.data = None
self.alerts = None
def _build_url(self):
url = _RESOURCE.format(self._api_key)
def _build_url(self, baseurl=_RESOURCE):
url = baseurl.format(self._api_key)
if self._pws_id:
url = url + 'pws:{}'.format(self._pws_id)
else:
@ -168,3 +198,18 @@ class WUndergroundData(object):
_LOGGER.error("Check WUnderground API %s", err.args)
self.data = None
raise
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update_alerts(self):
"""Get the latest alerts data from WUnderground."""
try:
result = requests.get(self._build_url(_ALERTS), timeout=10).json()
if "error" in result['response']:
raise ValueError(result['response']["error"]
["description"])
else:
self.alerts = result["alerts"]
except ValueError as err:
_LOGGER.error("Check WUnderground API %s", err.args)
self.alerts = None
raise

View File

@ -11,7 +11,7 @@ VALID_CONFIG_PWS = {
'api_key': 'foo',
'pws_id': 'bar',
'monitored_conditions': [
'weather', 'feelslike_c'
'weather', 'feelslike_c', 'alerts'
]
}
@ -19,17 +19,19 @@ VALID_CONFIG = {
'platform': 'wunderground',
'api_key': 'foo',
'monitored_conditions': [
'weather', 'feelslike_c'
'weather', 'feelslike_c', 'alerts'
]
}
FEELS_LIKE = '40'
WEATHER = 'Clear'
ICON_URL = 'http://icons.wxug.com/i/c/k/clear.gif'
ALERT_MESSAGE = 'This is a test alert message'
def mocked_requests_get(*args, **kwargs):
"""Mock requests.get invocations."""
# pylint: disable=too-few-public-methods
class MockResponse:
"""Class to represent a mocked response."""
@ -61,26 +63,36 @@ def mocked_requests_get(*args, **kwargs):
"feelslike_c": FEELS_LIKE,
"weather": WEATHER,
"icon_url": ICON_URL
}
}, "alerts": [
{
"type": 'FLO',
"description": "Areal Flood Warning",
"date": "9:36 PM CDT on September 22, 2016",
"expires": "10:00 AM CDT on September 23, 2016",
"message": ALERT_MESSAGE,
},
],
}, 200)
else:
return MockResponse({
"response": {
"version": "0.1",
"termsofService":
"http://www.wunderground.com/weather/api/d/terms.html",
"features": {},
"error": {
"type": "keynotfound",
"description": "this key does not exist"
}
"response": {
"version": "0.1",
"termsofService":
"http://www.wunderground.com/weather/api/d/terms.html",
"features": {},
"error": {
"type": "keynotfound",
"description": "this key does not exist"
}
}, 200)
}
}, 200)
class TestWundergroundSetup(unittest.TestCase):
"""Test the WUnderground platform."""
# pylint: disable=invalid-name
DEVICES = []
def add_devices(self, devices):
@ -107,14 +119,13 @@ class TestWundergroundSetup(unittest.TestCase):
self.add_devices, None))
self.assertTrue(
wunderground.setup_platform(self.hass, VALID_CONFIG,
self.add_devices,
None))
self.add_devices, None))
invalid_config = {
'platform': 'wunderground',
'api_key': 'BOB',
'pws_id': 'bar',
'monitored_conditions': [
'weather', 'feelslike_c'
'weather', 'feelslike_c', 'alerts'
]
}
@ -128,11 +139,17 @@ class TestWundergroundSetup(unittest.TestCase):
wunderground.setup_platform(self.hass, VALID_CONFIG, self.add_devices,
None)
for device in self.DEVICES:
device.update()
self.assertTrue(str(device.name).startswith('PWS_'))
if device.name == 'PWS_weather':
self.assertEqual(ICON_URL, device.entity_picture)
self.assertEqual(WEATHER, device.state)
self.assertIsNone(device.unit_of_measurement)
elif device.name == 'PWS_alerts':
self.assertEqual(1, device.state)
self.assertEqual(ALERT_MESSAGE,
device.device_state_attributes['Message'])
self.assertIsNone(device.entity_picture)
else:
self.assertIsNone(device.entity_picture)
self.assertEqual(FEELS_LIKE, device.state)