diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index ef20e3f2679..69b53b0c259 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -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 diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index ffb070f9ab9..f7f2e958ef7 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -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)