Add 'forecast' ability to yr weather sensor (#8650)
* Add forecast option to YR sensor * Fix some style issues * Fix linting
This commit is contained in:
parent
86c06ad76e
commit
47dad547eb
2 changed files with 100 additions and 35 deletions
|
@ -50,12 +50,15 @@ SENSOR_TYPES = {
|
||||||
'dewpointTemperature': ['Dewpoint temperature', '°C'],
|
'dewpointTemperature': ['Dewpoint temperature', '°C'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CONF_FORECAST = 'forecast'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=['symbol']): vol.All(
|
vol.Optional(CONF_MONITORED_CONDITIONS, default=['symbol']): vol.All(
|
||||||
cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]),
|
cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]),
|
||||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||||
vol.Optional(CONF_ELEVATION): vol.Coerce(int),
|
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)
|
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||||
elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0)
|
elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0)
|
||||||
|
forecast = config.get(CONF_FORECAST, 0)
|
||||||
|
|
||||||
if None in (latitude, longitude):
|
if None in (latitude, longitude):
|
||||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
_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))
|
dev.append(YrSensor(sensor_type))
|
||||||
async_add_devices(dev)
|
async_add_devices(dev)
|
||||||
|
|
||||||
weather = YrData(hass, coordinates, dev)
|
weather = YrData(hass, coordinates, forecast, dev)
|
||||||
# Update weather on the hour, spread seconds
|
# Update weather on the hour, spread seconds
|
||||||
async_track_utc_time_change(
|
async_track_utc_time_change(
|
||||||
hass, weather.async_update, minute=randrange(1, 10),
|
hass, weather.async_update, minute=randrange(1, 10),
|
||||||
|
@ -137,12 +141,13 @@ class YrSensor(Entity):
|
||||||
class YrData(object):
|
class YrData(object):
|
||||||
"""Get the latest data and updates the states."""
|
"""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."""
|
"""Initialize the data object."""
|
||||||
self._url = 'https://aa015h6buqvih86i1.api.met.no/'\
|
self._url = 'https://aa015h6buqvih86i1.api.met.no/'\
|
||||||
'weatherapi/locationforecast/1.9/'
|
'weatherapi/locationforecast/1.9/'
|
||||||
self._urlparams = coordinates
|
self._urlparams = coordinates
|
||||||
self._nextrun = None
|
self._nextrun = None
|
||||||
|
self._forecast = forecast
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
self.data = {}
|
self.data = {}
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
@ -187,40 +192,58 @@ class YrData(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
now = dt_util.utcnow()
|
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 = []
|
||||||
|
|
||||||
tasks = []
|
|
||||||
# Update all devices
|
|
||||||
for dev in self.devices:
|
|
||||||
# Find sensor
|
|
||||||
for time_entry in self.data['product']['time']:
|
for time_entry in self.data['product']['time']:
|
||||||
valid_from = dt_util.parse_datetime(time_entry['@from'])
|
valid_from = dt_util.parse_datetime(time_entry['@from'])
|
||||||
valid_to = dt_util.parse_datetime(time_entry['@to'])
|
valid_to = dt_util.parse_datetime(time_entry['@to'])
|
||||||
new_state = None
|
|
||||||
|
|
||||||
loc_data = time_entry['location']
|
if now >= valid_to:
|
||||||
|
# Has already passed. Never select this.
|
||||||
if dev.type not in loc_data or now >= valid_to:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if dev.type == 'precipitation' and valid_from < now:
|
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])
|
||||||
|
|
||||||
|
# Update all devices
|
||||||
|
tasks = []
|
||||||
|
if len(ordered_entries) > 0:
|
||||||
|
for dev in self.devices:
|
||||||
|
new_state = None
|
||||||
|
|
||||||
|
for (_, selected_time_entry) in ordered_entries:
|
||||||
|
loc_data = selected_time_entry['location']
|
||||||
|
|
||||||
|
if dev.type not in loc_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if dev.type == 'precipitation':
|
||||||
new_state = loc_data[dev.type]['@value']
|
new_state = loc_data[dev.type]['@value']
|
||||||
break
|
elif dev.type == 'symbol':
|
||||||
elif dev.type == 'symbol' and valid_from < now:
|
|
||||||
new_state = loc_data[dev.type]['@number']
|
new_state = loc_data[dev.type]['@number']
|
||||||
break
|
|
||||||
elif dev.type in ('temperature', 'pressure', 'humidity',
|
elif dev.type in ('temperature', 'pressure', 'humidity',
|
||||||
'dewpointTemperature'):
|
'dewpointTemperature'):
|
||||||
new_state = loc_data[dev.type]['@value']
|
new_state = loc_data[dev.type]['@value']
|
||||||
break
|
|
||||||
elif dev.type in ('windSpeed', 'windGust'):
|
elif dev.type in ('windSpeed', 'windGust'):
|
||||||
new_state = loc_data[dev.type]['@mps']
|
new_state = loc_data[dev.type]['@mps']
|
||||||
break
|
|
||||||
elif dev.type == 'windDirection':
|
elif dev.type == 'windDirection':
|
||||||
new_state = float(loc_data[dev.type]['@deg'])
|
new_state = float(loc_data[dev.type]['@deg'])
|
||||||
break
|
|
||||||
elif dev.type in ('fog', 'cloudiness', 'lowClouds',
|
elif dev.type in ('fog', 'cloudiness', 'lowClouds',
|
||||||
'mediumClouds', 'highClouds'):
|
'mediumClouds', 'highClouds'):
|
||||||
new_state = loc_data[dev.type]['@percent']
|
new_state = loc_data[dev.type]['@percent']
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
@ -228,5 +251,5 @@ class YrData(object):
|
||||||
dev._state = new_state
|
dev._state = new_state
|
||||||
tasks.append(dev.async_update_ha_state())
|
tasks.append(dev.async_update_ha_state())
|
||||||
|
|
||||||
if tasks:
|
if len(tasks) > 0:
|
||||||
yield from asyncio.wait(tasks, loop=self.hass.loop)
|
yield from asyncio.wait(tasks, loop=self.hass.loop)
|
||||||
|
|
|
@ -69,3 +69,45 @@ def test_custom_setup(hass, aioclient_mock):
|
||||||
state = hass.states.get('sensor.yr_wind_speed')
|
state = hass.states.get('sensor.yr_wind_speed')
|
||||||
assert state.attributes.get('unit_of_measurement') == 'm/s'
|
assert state.attributes.get('unit_of_measurement') == 'm/s'
|
||||||
assert state.state == '3.5'
|
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'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue