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 import homeassistant.helpers.config_validation as cv
_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/' _RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/'
_ALERTS = 'http://api.wunderground.com/api/{}/alerts/q/'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" 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 are defined like: Name, units
SENSOR_TYPES = { SENSOR_TYPES = {
'alerts': ['Alerts', None],
'weather': ['Weather Summary', None], 'weather': ['Weather Summary', None],
'station_id': ['Station ID', None], 'station_id': ['Station ID', None],
'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS], 'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS],
@ -57,6 +59,14 @@ SENSOR_TYPES = {
'solarradiation': ['Solar Radiation', None] 'solarradiation': ['Solar Radiation', None]
} }
# Alert Attributes
ALERTS_ATTRS = [
'date',
'description',
'expires',
'message',
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PWS_ID): cv.string, vol.Optional(CONF_PWS_ID): cv.string,
@ -106,15 +116,31 @@ class WUndergroundSensor(Entity):
return int(self.rest.data[self._condition][:-1]) return int(self.rest.data[self._condition][:-1])
else: else:
return self.rest.data[self._condition] return self.rest.data[self._condition]
else:
if self.rest.alerts and self._condition == 'alerts':
return len(self.rest.alerts)
return STATE_UNKNOWN return STATE_UNKNOWN
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { attrs = {}
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
} 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 @property
def entity_picture(self): def entity_picture(self):
@ -129,6 +155,9 @@ class WUndergroundSensor(Entity):
def update(self): def update(self):
"""Update current conditions.""" """Update current conditions."""
if self._condition == 'alerts':
self.rest.update_alerts()
else:
self.rest.update() self.rest.update()
@ -144,9 +173,10 @@ class WUndergroundData(object):
self._latitude = hass.config.latitude self._latitude = hass.config.latitude
self._longitude = hass.config.longitude self._longitude = hass.config.longitude
self.data = None self.data = None
self.alerts = None
def _build_url(self): def _build_url(self, baseurl=_RESOURCE):
url = _RESOURCE.format(self._api_key) url = baseurl.format(self._api_key)
if self._pws_id: if self._pws_id:
url = url + 'pws:{}'.format(self._pws_id) url = url + 'pws:{}'.format(self._pws_id)
else: else:
@ -168,3 +198,18 @@ class WUndergroundData(object):
_LOGGER.error("Check WUnderground API %s", err.args) _LOGGER.error("Check WUnderground API %s", err.args)
self.data = None self.data = None
raise 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', 'api_key': 'foo',
'pws_id': 'bar', 'pws_id': 'bar',
'monitored_conditions': [ 'monitored_conditions': [
'weather', 'feelslike_c' 'weather', 'feelslike_c', 'alerts'
] ]
} }
@ -19,17 +19,19 @@ VALID_CONFIG = {
'platform': 'wunderground', 'platform': 'wunderground',
'api_key': 'foo', 'api_key': 'foo',
'monitored_conditions': [ 'monitored_conditions': [
'weather', 'feelslike_c' 'weather', 'feelslike_c', 'alerts'
] ]
} }
FEELS_LIKE = '40' FEELS_LIKE = '40'
WEATHER = 'Clear' WEATHER = 'Clear'
ICON_URL = 'http://icons.wxug.com/i/c/k/clear.gif' 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): def mocked_requests_get(*args, **kwargs):
"""Mock requests.get invocations.""" """Mock requests.get invocations."""
# pylint: disable=too-few-public-methods
class MockResponse: class MockResponse:
"""Class to represent a mocked response.""" """Class to represent a mocked response."""
@ -61,7 +63,16 @@ def mocked_requests_get(*args, **kwargs):
"feelslike_c": FEELS_LIKE, "feelslike_c": FEELS_LIKE,
"weather": WEATHER, "weather": WEATHER,
"icon_url": ICON_URL "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) }, 200)
else: else:
return MockResponse({ return MockResponse({
@ -81,6 +92,7 @@ def mocked_requests_get(*args, **kwargs):
class TestWundergroundSetup(unittest.TestCase): class TestWundergroundSetup(unittest.TestCase):
"""Test the WUnderground platform.""" """Test the WUnderground platform."""
# pylint: disable=invalid-name
DEVICES = [] DEVICES = []
def add_devices(self, devices): def add_devices(self, devices):
@ -107,14 +119,13 @@ class TestWundergroundSetup(unittest.TestCase):
self.add_devices, None)) self.add_devices, None))
self.assertTrue( self.assertTrue(
wunderground.setup_platform(self.hass, VALID_CONFIG, wunderground.setup_platform(self.hass, VALID_CONFIG,
self.add_devices, self.add_devices, None))
None))
invalid_config = { invalid_config = {
'platform': 'wunderground', 'platform': 'wunderground',
'api_key': 'BOB', 'api_key': 'BOB',
'pws_id': 'bar', 'pws_id': 'bar',
'monitored_conditions': [ '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, wunderground.setup_platform(self.hass, VALID_CONFIG, self.add_devices,
None) None)
for device in self.DEVICES: for device in self.DEVICES:
device.update()
self.assertTrue(str(device.name).startswith('PWS_')) self.assertTrue(str(device.name).startswith('PWS_'))
if device.name == 'PWS_weather': if device.name == 'PWS_weather':
self.assertEqual(ICON_URL, device.entity_picture) self.assertEqual(ICON_URL, device.entity_picture)
self.assertEqual(WEATHER, device.state) self.assertEqual(WEATHER, device.state)
self.assertIsNone(device.unit_of_measurement) 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: else:
self.assertIsNone(device.entity_picture) self.assertIsNone(device.entity_picture)
self.assertEqual(FEELS_LIKE, device.state) self.assertEqual(FEELS_LIKE, device.state)