From d38acfbd39b2c0e8f4cd2bbb041f9511086312b7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 7 Jun 2017 10:49:54 +0200 Subject: [PATCH] Add Yahoo! weather platform (#7939) --- .coveragerc | 1 + homeassistant/components/weather/__init__.py | 18 +- homeassistant/components/weather/yweather.py | 186 +++++++++++++++++++ requirements_all.txt | 1 + 4 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/weather/yweather.py diff --git a/.coveragerc b/.coveragerc index 15054df24e0..231d3685e69 100644 --- a/.coveragerc +++ b/.coveragerc @@ -506,6 +506,7 @@ omit = homeassistant/components/weather/buienradar.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py + homeassistant/components/weather/yweather.py homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py homeassistant/components/zwave/util.py diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 9b0daf10efb..17a47fbc522 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -22,16 +22,17 @@ DOMAIN = 'weather' ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_CONDITION_CLASS = 'condition_class' +ATTR_FORECAST = 'forecast' +ATTR_FORECAST_TEMP = 'temperature' +ATTR_FORECAST_TIME = 'datetime' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' ATTR_WEATHER_OZONE = 'ozone' ATTR_WEATHER_PRESSURE = 'pressure' ATTR_WEATHER_TEMPERATURE = 'temperature' +ATTR_WEATHER_VISIBILITY = 'visibility' ATTR_WEATHER_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_WIND_SPEED = 'wind_speed' -ATTR_FORECAST = 'forecast' -ATTR_FORECAST_TEMP = 'temperature' -ATTR_FORECAST_TIME = 'datetime' @asyncio.coroutine @@ -45,7 +46,7 @@ def async_setup(hass, config): # pylint: disable=no-member, no-self-use class WeatherEntity(Entity): - """ABC for a weather data.""" + """ABC for weather data.""" @property def temperature(self): @@ -87,6 +88,11 @@ class WeatherEntity(Entity): """Return the attribution.""" return None + @property + def visibility(self): + """Return the visibility.""" + return None + @property def forecast(self): """Return the forecast.""" @@ -116,6 +122,10 @@ class WeatherEntity(Entity): if wind_speed is not None: data[ATTR_WEATHER_WIND_SPEED] = wind_speed + visibility = self.visibility + if visibility is not None: + data[ATTR_WEATHER_VISIBILITY] = visibility + attribution = self.attribution if attribution is not None: data[ATTR_WEATHER_ATTRIBUTION] = attribution diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py new file mode 100644 index 00000000000..0e216273d65 --- /dev/null +++ b/homeassistant/components/weather/yweather.py @@ -0,0 +1,186 @@ +""" +Support for the Yahoo! Weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.yweather/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP) +from homeassistant.const import (TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN) + +REQUIREMENTS = ["yahooweather==0.8"] + +_LOGGER = logging.getLogger(__name__) + +ATTR_FORECAST_CONDITION = 'condition' +ATTRIBUTION = "Weather details provided by Yahoo! Inc." + +CONF_FORECAST = 'forecast' +CONF_WOEID = 'woeid' + +DEFAULT_NAME = 'Yweather' + +SCAN_INTERVAL = timedelta(minutes=10) + +CONDITION_CLASSES = { + 'cloudy': [26, 27, 28, 29, 30], + 'fog': [19, 20, 21, 22, 23], + 'hail': [17, 18, 35], + 'lightning': [37], + 'lightning-rainy': [38, 39], + 'partlycloudy': [44], + 'pouring': [40, 45], + 'rainy': [9, 11, 12], + 'snowy': [8, 13, 14, 15, 16, 41, 42, 43], + 'snowy-rainy': [5, 6, 7, 10, 46, 47], + 'sunny': [32], + 'windy': [24], + 'windy-variant': [], + 'exceptional': [0, 1, 2, 3, 4, 25, 36], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_WOEID, default=None): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_FORECAST, default=0): + vol.All(vol.Coerce(int), vol.Range(min=0, max=5)), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Yahoo! weather platform.""" + from yahooweather import get_woeid, UNIT_C, UNIT_F + + unit = hass.config.units.temperature_unit + woeid = config.get(CONF_WOEID) + forecast = config.get(CONF_FORECAST) + name = config.get(CONF_NAME) + + yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F + + # If not exists a customer WOEID/calculation from Home Assistant + if woeid is None: + woeid = get_woeid(hass.config.latitude, hass.config.longitude) + if woeid is None: + _LOGGER.warning("Can't retrieve WOEID from Yahoo!") + return False + + yahoo_api = YahooWeatherData(woeid, yunit) + + if not yahoo_api.update(): + _LOGGER.critical("Can't retrieve weather data from Yahoo!") + return False + + if forecast >= len(yahoo_api.yahoo.Forecast): + _LOGGER.error("Yahoo! only support %d days forecast", + len(yahoo_api.yahoo.Forecast)) + return False + + add_devices([YahooWeatherWeather(yahoo_api, name, forecast)], True) + + +class YahooWeatherWeather(WeatherEntity): + """Representation of Yahoo! weather data.""" + + def __init__(self, weather_data, name, forecast): + """Initialize the sensor.""" + self._name = name + self._data = weather_data + self._forecast = forecast + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def condition(self): + """Return the current condition.""" + try: + return [k for k, v in CONDITION_CLASSES.items() if + int(self._data.yahoo.Now['code']) in v][0] + except IndexError: + return STATE_UNKNOWN + + @property + def temperature(self): + """Return the temperature.""" + return self._data.yahoo.Now['temp'] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def pressure(self): + """Return the pressure.""" + return self._data.yahoo.Atmosphere['pressure'] + + @property + def humidity(self): + """Return the humidity.""" + return self._data.yahoo.Atmosphere['humidity'] + + @property + def visibility(self): + """Return the visibility.""" + return self._data.yahoo.Atmosphere['visibility'] + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._data.yahoo.Wind['speed'] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast array.""" + try: + forecast_condition = \ + [k for k, v in CONDITION_CLASSES.items() if + int(self._data.yahoo.Forecast[self._forecast]['code']) + in v][0] + except IndexError: + return STATE_UNKNOWN + + return [{ + ATTR_FORECAST_CONDITION: forecast_condition, + ATTR_FORECAST_TEMP: + self._data.yahoo.Forecast[self._forecast]['high'], + }] + + def update(self): + """Get the latest data from Yahoo! and updates the states.""" + self._data.update() + if not self._data.yahoo.RawData: + _LOGGER.info("Don't receive weather data from Yahoo!") + return + + +class YahooWeatherData(object): + """Handle the Yahoo! API object and limit updates.""" + + def __init__(self, woeid, temp_unit): + """Initialize the data object.""" + from yahooweather import YahooWeather + self._yahoo = YahooWeather(woeid, temp_unit) + + @property + def yahoo(self): + """Return Yahoo! API object.""" + return self._yahoo + + def update(self): + """Get the latest data from Yahoo!.""" + return self._yahoo.updateWeather() diff --git a/requirements_all.txt b/requirements_all.txt index 8f913e7a270..fe3d0731961 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,6 +898,7 @@ xmltodict==0.11.0 yahoo-finance==1.4.0 # homeassistant.components.sensor.yweather +# homeassistant.components.weather.yweather yahooweather==0.8 # homeassistant.components.light.yeelight