diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py new file mode 100644 index 00000000000..cb724247436 --- /dev/null +++ b/homeassistant/components/sensor/wunderground.py @@ -0,0 +1,159 @@ +"""Support for Wunderground weather service.""" +from datetime import timedelta +import logging +import requests + +import voluptuous as vol + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.config_validation import ensure_list +from homeassistant.util import Throttle +from homeassistant.const import (CONF_PLATFORM, CONF_MONITORED_CONDITIONS, + CONF_API_KEY, TEMP_FAHRENHEIT, TEMP_CELSIUS, + STATE_UNKNOWN) + +CONF_PWS_ID = 'pws_id' +_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/' +_LOGGER = logging.getLogger(__name__) + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) + +# Sensor types are defined like: Name, units +SENSOR_TYPES = { + 'weather': ['Weather Summary', None], + 'station_id': ['Station ID', None], + 'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS], + 'feelslike_f': ['Feels Like (°F)', TEMP_FAHRENHEIT], + 'feelslike_string': ['Feels Like', None], + 'heat_index_c': ['Dewpoint (°C)', TEMP_CELSIUS], + 'heat_index_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], + 'heat_index_string': ['Heat Index Summary', None], + 'dewpoint_c': ['Dewpoint (°C)', TEMP_CELSIUS], + 'dewpoint_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], + 'dewpoint_string': ['Dewpoint Summary', None], + 'wind_kph': ['Wind Speed', 'kpH'], + 'wind_mph': ['Wind Speed', 'mpH'], + 'UV': ['UV', None], + 'pressure_in': ['Pressure', 'in'], + 'pressure_mb': ['Pressure', 'mbar'], + 'wind_dir': ['Wind Direction', None], + 'wind_string': ['Wind Summary', None], + 'temp_c': ['Temperature (°C)', TEMP_CELSIUS], + 'temp_f': ['Temperature (°F)', TEMP_FAHRENHEIT], + 'relative_humidity': ['Relative Humidity', '%'], + 'visibility_mi': ['Visibility (miles)', 'mi'], + 'visibility_km': ['Visibility (km)', 'km'], + 'precip_today_in': ['Precipation Today', 'in'], + 'precip_today_metric': ['Precipitation Today', 'mm'], + 'precip_today_string': ['Precipitation today', None], + 'solarradiation': ['Solar Radiation', None] +} + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): "wunderground", + vol.Required(CONF_API_KEY): vol.Coerce(str), + CONF_PWS_ID: vol.Coerce(str), + vol.Required(CONF_MONITORED_CONDITIONS, + default=[]): vol.All(ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Wunderground sensor.""" + rest = WUndergroundData(hass, + config.get(CONF_API_KEY), + config.get(CONF_PWS_ID, None)) + sensors = [] + for variable in config['monitored_conditions']: + if variable in SENSOR_TYPES: + sensors.append(WUndergroundSensor(rest, variable)) + else: + _LOGGER.error('Wunderground sensor: "%s" does not exist', variable) + + try: + rest.update() + except ValueError as err: + _LOGGER.error("Received error from WUnderground: %s", err) + return False + + add_devices(sensors) + + return True + + +class WUndergroundSensor(Entity): + """Implementing the Wunderground sensor.""" + + def __init__(self, rest, condition): + """Initialize the sensor.""" + self.rest = rest + self._condition = condition + self._unit_of_measurement = None + + @property + def name(self): + """Return the name of the sensor.""" + return "PWS_" + self._condition + + @property + def state(self): + """Return the state of the sensor.""" + if self.rest.data and self._condition in self.rest.data: + return self.rest.data[self._condition] + else: + return STATE_UNKNOWN + + @property + def entity_picture(self): + """Return the entity picture.""" + if self._condition == 'weather': + return self.rest.data['icon_url'] + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return SENSOR_TYPES[self._condition][1] + + def update(self): + """Update current conditions.""" + self.rest.update() + +# pylint: disable=too-few-public-methods + + +class WUndergroundData(object): + """Get data from Wundeground.""" + + def __init__(self, hass, api_key, pws_id=None): + """Initialize the data object.""" + self._hass = hass + self._api_key = api_key + self._pws_id = pws_id + self._latitude = hass.config.latitude + self._longitude = hass.config.longitude + self.data = None + + def _build_url(self): + url = _RESOURCE.format(self._api_key) + if self._pws_id: + url = url + 'pws:' + self._pws_id + else: + url = url + '{},{}'.format(self._latitude, self._longitude) + + return url + '.json' + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from wunderground.""" + try: + result = requests.get(self._build_url(), timeout=10).json() + if "error" in result['response']: + raise ValueError(result['response']["error"] + ["description"]) + else: + self.data = result["current_observation"] + except ValueError as err: + _LOGGER.error("Check Wunderground API %s", err.args) + self.data = None + raise diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py new file mode 100644 index 00000000000..c6664b71254 --- /dev/null +++ b/tests/components/sensor/test_wunderground.py @@ -0,0 +1,138 @@ +"""The tests for the forecast.io platform.""" +import unittest + +from homeassistant.components.sensor import wunderground +from homeassistant.const import TEMP_CELSIUS +from homeassistant import core as ha + +VALID_CONFIG_PWS = { + 'platform': 'wunderground', + 'api_key': 'foo', + 'pws_id': 'bar', + 'monitored_conditions': [ + 'weather', 'feelslike_c' + ] +} + +VALID_CONFIG = { + 'platform': 'wunderground', + 'api_key': 'foo', + 'monitored_conditions': [ + 'weather', 'feelslike_c' + ] +} + +FEELS_LIKE = '40' +WEATHER = 'Clear' +ICON_URL = 'http://icons.wxug.com/i/c/k/clear.gif' + + +def mocked_requests_get(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + if str(args[0]).startswith('http://api.wunderground.com/api/foo/'): + # Return valid response + print('VALID RESPONSE') + return MockResponse({ + "response": { + "version": "0.1", + "termsofService": + "http://www.wunderground.com/weather/api/d/terms.html", + "features": { + "conditions": 1 + } + }, "current_observation": { + "image": { + "url": + 'http://icons.wxug.com/graphics/wu2/logo_130x80.png', + "title": "Weather Underground", + "link": "http://www.wunderground.com" + }, + "feelslike_c": FEELS_LIKE, + "weather": WEATHER, + "icon_url": ICON_URL + } + }, 200) + else: + # Return invalid api key + print('INVALID RESPONSE') + 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" + } + } + }, 200) + + +class TestWundergroundSetup(unittest.TestCase): + """Test the wunderground platform.""" + + DEVICES = [] + + def add_devices(self, devices): + for device in devices: + self.DEVICES.append(device) + + def setUp(self): + """Initialize values for this testcase class.""" + self.DEVICES = [] + self.hass = ha.HomeAssistant() + self.key = 'foo' + self.config = VALID_CONFIG_PWS + self.lat = 37.8267 + self.lon = -122.423 + self.hass.config.latitude = self.lat + self.hass.config.longitude = self.lon + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_setup(self, req_mock): + """Test that the component is loaded if passed in PSW Id.""" + print('1') + self.assertTrue( + wunderground.setup_platform(self.hass, VALID_CONFIG_PWS, + self.add_devices, None)) + print('2') + self.assertTrue( + wunderground.setup_platform(self.hass, VALID_CONFIG, + self.add_devices, + None)) + invalid_config = { + 'platform': 'wunderground', + 'api_key': 'BOB', + 'pws_id': 'bar', + 'monitored_conditions': [ + 'weather', 'feelslike_c' + ] + } + + self.assertFalse( + wunderground.setup_platform(self.hass, invalid_config, + self.add_devices, None)) + + @unittest.mock.patch('requests.get', side_effect=mocked_requests_get) + def test_sensor(self, req_mock): + wunderground.setup_platform(self.hass, VALID_CONFIG, self.add_devices, + None) + print(str(self.DEVICES)) + for device in self.DEVICES: + 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) + else: + self.assertIsNone(device.entity_picture) + self.assertEqual(FEELS_LIKE, device.state) + self.assertEqual(TEMP_CELSIUS, device.unit_of_measurement)