From 47dad547eb8cc0987152d0edac30bf9044bc2e7c Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Wed, 2 Aug 2017 07:42:51 +0200 Subject: [PATCH] Add 'forecast' ability to yr weather sensor (#8650) * Add forecast option to YR sensor * Fix some style issues * Fix linting --- homeassistant/components/sensor/yr.py | 93 +++++++++++++++++---------- tests/components/sensor/test_yr.py | 42 ++++++++++++ 2 files changed, 100 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 16951f21c5d..e885363383f 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -50,12 +50,15 @@ SENSOR_TYPES = { 'dewpointTemperature': ['Dewpoint temperature', '°C'], } +CONF_FORECAST = 'forecast' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_MONITORED_CONDITIONS, default=['symbol']): vol.All( cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]), vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_ELEVATION): vol.Coerce(int), + vol.Optional(CONF_FORECAST): vol.Coerce(int) }) @@ -65,6 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0) + forecast = config.get(CONF_FORECAST, 0) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") @@ -79,7 +83,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): dev.append(YrSensor(sensor_type)) async_add_devices(dev) - weather = YrData(hass, coordinates, dev) + weather = YrData(hass, coordinates, forecast, dev) # Update weather on the hour, spread seconds async_track_utc_time_change( hass, weather.async_update, minute=randrange(1, 10), @@ -137,12 +141,13 @@ class YrSensor(Entity): class YrData(object): """Get the latest data and updates the states.""" - def __init__(self, hass, coordinates, devices): + def __init__(self, hass, coordinates, forecast, devices): """Initialize the data object.""" self._url = 'https://aa015h6buqvih86i1.api.met.no/'\ 'weatherapi/locationforecast/1.9/' self._urlparams = coordinates self._nextrun = None + self._forecast = forecast self.devices = devices self.data = {} self.hass = hass @@ -187,46 +192,64 @@ class YrData(object): return now = dt_util.utcnow() + forecast_time = now + dt_util.dt.timedelta(hours=self._forecast) + + # Find the correct time entry. Since not all time entries contain all + # types of data, we cannot just select one. Instead, we order them by + # distance from the desired forecast_time, and for every device iterate + # them in order of increasing distance, taking the first time_point + # that contains the desired data. + + ordered_entries = [] + + for time_entry in self.data['product']['time']: + valid_from = dt_util.parse_datetime(time_entry['@from']) + valid_to = dt_util.parse_datetime(time_entry['@to']) + + if now >= valid_to: + # Has already passed. Never select this. + continue + + average_dist = (abs((valid_to - forecast_time).total_seconds()) + + abs((valid_from - forecast_time).total_seconds())) + + ordered_entries.append((average_dist, time_entry)) + + ordered_entries.sort(key=lambda item: item[0]) - tasks = [] # Update all devices - for dev in self.devices: - # Find sensor - for time_entry in self.data['product']['time']: - valid_from = dt_util.parse_datetime(time_entry['@from']) - valid_to = dt_util.parse_datetime(time_entry['@to']) + tasks = [] + if len(ordered_entries) > 0: + for dev in self.devices: new_state = None - loc_data = time_entry['location'] + for (_, selected_time_entry) in ordered_entries: + loc_data = selected_time_entry['location'] - if dev.type not in loc_data or now >= valid_to: - continue + if dev.type not in loc_data: + continue + + if dev.type == 'precipitation': + new_state = loc_data[dev.type]['@value'] + elif dev.type == 'symbol': + new_state = loc_data[dev.type]['@number'] + elif dev.type in ('temperature', 'pressure', 'humidity', + 'dewpointTemperature'): + new_state = loc_data[dev.type]['@value'] + elif dev.type in ('windSpeed', 'windGust'): + new_state = loc_data[dev.type]['@mps'] + elif dev.type == 'windDirection': + new_state = float(loc_data[dev.type]['@deg']) + elif dev.type in ('fog', 'cloudiness', 'lowClouds', + 'mediumClouds', 'highClouds'): + new_state = loc_data[dev.type]['@percent'] - if dev.type == 'precipitation' and valid_from < now: - new_state = loc_data[dev.type]['@value'] - break - elif dev.type == 'symbol' and valid_from < now: - new_state = loc_data[dev.type]['@number'] - break - elif dev.type in ('temperature', 'pressure', 'humidity', - 'dewpointTemperature'): - new_state = loc_data[dev.type]['@value'] - break - elif dev.type in ('windSpeed', 'windGust'): - new_state = loc_data[dev.type]['@mps'] - break - elif dev.type == 'windDirection': - new_state = float(loc_data[dev.type]['@deg']) - break - elif dev.type in ('fog', 'cloudiness', 'lowClouds', - 'mediumClouds', 'highClouds'): - new_state = loc_data[dev.type]['@percent'] break - # pylint: disable=protected-access - if new_state != dev._state: - dev._state = new_state - tasks.append(dev.async_update_ha_state()) + # pylint: disable=protected-access + if new_state != dev._state: + dev._state = new_state + tasks.append(dev.async_update_ha_state()) - if tasks: + if len(tasks) > 0: yield from asyncio.wait(tasks, loop=self.hass.loop) diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index d0504db963c..4c2118f43f3 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -69,3 +69,45 @@ def test_custom_setup(hass, aioclient_mock): state = hass.states.get('sensor.yr_wind_speed') assert state.attributes.get('unit_of_measurement') == 'm/s' assert state.state == '3.5' + + +@asyncio.coroutine +def test_forecast_setup(hass, aioclient_mock): + """Test a custom setup with 24h forecast.""" + aioclient_mock.get('https://aa015h6buqvih86i1.api.met.no/' + 'weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) + + config = {'platform': 'yr', + 'elevation': 0, + 'forecast': 24, + 'monitored_conditions': [ + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed']} + hass.allow_pool = True + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=NOW), assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', {'sensor': config}) + + state = hass.states.get('sensor.yr_pressure') + assert state.attributes.get('unit_of_measurement') == 'hPa' + assert state.state == '1008.3' + + state = hass.states.get('sensor.yr_wind_direction') + assert state.attributes.get('unit_of_measurement') == '°' + assert state.state == '148.9' + + state = hass.states.get('sensor.yr_humidity') + assert state.attributes.get('unit_of_measurement') == '%' + assert state.state == '77.4' + + state = hass.states.get('sensor.yr_fog') + assert state.attributes.get('unit_of_measurement') == '%' + assert state.state == '0.0' + + state = hass.states.get('sensor.yr_wind_speed') + assert state.attributes.get('unit_of_measurement') == 'm/s' + assert state.state == '3.6'