From 66d8787d47b2c69e46bacec33ee1d62394896f84 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Mon, 20 Feb 2017 01:42:12 +0100 Subject: [PATCH] Weather platform Forecast (#4721) * Added forecast functionality to the weather platform, updated OWM to get forecast data * style fixes * Changed returned forecast data to a more managable dict * Fixed line length * forecast will always be collected, data returned from owm is based on metric setting * use list comprehension to create the forecast data * Added forecast data to the weather demo * Create dict directly in list comprehension * Minor variable change in weather demo platform * Convert forecast temperatures in weather component * set forecast attributes as constants * Style fixes and tests * Copied forecast_entry instead of mutating data --- homeassistant/components/weather/__init__.py | 24 ++++++++++--- homeassistant/components/weather/demo.py | 30 +++++++++++++--- .../components/weather/openweathermap.py | 34 ++++++++++++++++--- tests/components/weather/test_weather.py | 5 ++- 4 files changed, 80 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 67dc7924aa3..d67af26f560 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -29,6 +29,9 @@ ATTR_WEATHER_PRESSURE = 'pressure' ATTR_WEATHER_TEMPERATURE = 'temperature' 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 @@ -84,11 +87,16 @@ class WeatherEntity(Entity): """Return the attribution.""" return None + @property + def forecast(self): + """Return the forecast.""" + return None + @property def state_attributes(self): """Return the state attributes.""" data = { - ATTR_WEATHER_TEMPERATURE: self._temp_for_display, + ATTR_WEATHER_TEMPERATURE: self._temp_for_display(self.temperature), ATTR_WEATHER_HUMIDITY: self.humidity, } @@ -112,6 +120,16 @@ class WeatherEntity(Entity): if attribution is not None: data[ATTR_WEATHER_ATTRIBUTION] = attribution + if self.forecast is not None: + forecast = [] + for forecast_entry in self.forecast: + forecast_entry = dict(forecast_entry) + forecast_entry[ATTR_FORECAST_TEMP] = self._temp_for_display( + forecast_entry[ATTR_FORECAST_TEMP]) + forecast.append(forecast_entry) + + data[ATTR_FORECAST] = forecast + return data @property @@ -124,10 +142,8 @@ class WeatherEntity(Entity): """Return the current condition.""" raise NotImplementedError() - @property - def _temp_for_display(self): + def _temp_for_display(self, temp): """Convert temperature into preferred units for display purposes.""" - temp = self.temperature unit = self.temperature_unit hass_unit = self.hass.config.units.temperature_unit diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index b919722f2a4..00470e86e1b 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -4,7 +4,10 @@ Demo platform that offers fake meteorological data. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.weather import WeatherEntity +from datetime import datetime, timedelta + +from homeassistant.components.weather import ( + WeatherEntity, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT) CONDITION_CLASSES = { @@ -28,8 +31,10 @@ CONDITION_CLASSES = { def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Demo weather.""" add_devices([ - DemoWeather('South', 'Sunshine', 21, 92, 1099, 0.5, TEMP_CELSIUS), - DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT) + DemoWeather('South', 'Sunshine', 21, 92, 1099, 0.5, TEMP_CELSIUS, + [22, 19, 15, 12, 14, 18, 21]), + DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT, + [-10, -13, -18, -23, -19, -14, -9]) ]) @@ -37,7 +42,7 @@ class DemoWeather(WeatherEntity): """Representation of a weather condition.""" def __init__(self, name, condition, temperature, humidity, pressure, - wind_speed, temperature_unit): + wind_speed, temperature_unit, forecast): """Initialize the Demo weather.""" self._name = name self._condition = condition @@ -46,6 +51,7 @@ class DemoWeather(WeatherEntity): self._humidity = humidity self._pressure = pressure self._wind_speed = wind_speed + self._forecast = forecast @property def name(self): @@ -92,3 +98,19 @@ class DemoWeather(WeatherEntity): def attribution(self): """Return the attribution.""" return 'Powered by Home Assistant' + + @property + def forecast(self): + """Return the forecast.""" + reftime = datetime.now().replace(hour=16, minute=00) + + forecast_data = [] + for entry in self._forecast: + data_dict = { + ATTR_FORECAST_TIME: reftime.isoformat(), + ATTR_FORECAST_TEMP: entry + } + reftime = reftime + timedelta(hours=4) + forecast_data.append(data_dict) + + return forecast_data diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index c08666881f3..aa3213c3832 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -9,9 +9,10 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.weather import WeatherEntity, PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, STATE_UNKNOWN) +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) +from homeassistant.const import (CONF_API_KEY, CONF_NAME, CONF_LATITUDE, + CONF_LONGITUDE, STATE_UNKNOWN, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -23,6 +24,7 @@ DEFAULT_NAME = 'OpenWeatherMap' ATTRIBUTION = 'Data provided by OpenWeatherMap' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) CONDITION_CLASSES = { 'cloudy': [804], @@ -79,6 +81,7 @@ class OpenWeatherMapWeather(WeatherEntity): self._owm = owm self._temperature_unit = temperature_unit self.data = None + self.forecast_data = None @property def name(self): @@ -102,7 +105,7 @@ class OpenWeatherMapWeather(WeatherEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - return self._temperature_unit + return TEMP_CELSIUS @property def pressure(self): @@ -129,10 +132,20 @@ class OpenWeatherMapWeather(WeatherEntity): """Return the attribution.""" return ATTRIBUTION + @property + def forecast(self): + """Return the forecast array.""" + return [{ + ATTR_FORECAST_TIME: entry.get_reference_time('iso'), + ATTR_FORECAST_TEMP: entry.get_temperature('celsius').get('temp')} + for entry in self.forecast_data.get_weathers()] + def update(self): """Get the latest data from OWM and updates the states.""" self._owm.update() + self._owm.update_forecast() self.data = self._owm.data + self.forecast_data = self._owm.forecast_data class WeatherData(object): @@ -144,6 +157,7 @@ class WeatherData(object): self.latitude = latitude self.longitude = longitude self.data = None + self.forecast_data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -154,3 +168,15 @@ class WeatherData(object): return self.data = obs.get_weather() + + @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) + def update_forecast(self): + """Get the lastest forecast from OpenWeatherMap.""" + fcd = self.owm.three_hours_forecast_at_coords( + self.latitude, self.longitude) + + if fcd is None: + _LOGGER.warning("Failed to fetch forecast data from OWM") + return + + self.forecast_data = fcd.get_forecast() diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index 97aaf0f6486..8ebe4b5355d 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -5,7 +5,7 @@ from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED) + ATTR_WEATHER_WIND_SPEED, ATTR_FORECAST, ATTR_FORECAST_TEMP) from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.bootstrap import setup_component @@ -45,6 +45,9 @@ class TestWeather(unittest.TestCase): assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_WEATHER_ATTRIBUTION) == \ 'Powered by Home Assistant' + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21 + assert len(data.get(ATTR_FORECAST)) == 7 def test_temperature_convert(self): """Test temperature conversion."""