Async version of Yr.no (#4158)
* initial * feedback * More feedback. Still need to fix match_url * url_match * split_lines
This commit is contained in:
parent
0d14920758
commit
f3595f790a
4 changed files with 186 additions and 140 deletions
|
@ -4,9 +4,13 @@ Support for Yr.no weather service.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.yr/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
import requests
|
||||
import async_timeout
|
||||
from aiohttp.web import HTTPException
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -15,8 +19,10 @@ from homeassistant.const import (
|
|||
CONF_LATITUDE, CONF_LONGITUDE, CONF_ELEVATION, CONF_MONITORED_CONDITIONS,
|
||||
ATTR_ATTRIBUTION)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
REQUIREMENTS = ['xmltodict==0.10.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -43,15 +49,15 @@ SENSOR_TYPES = {
|
|||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
[vol.In(SENSOR_TYPES.keys())],
|
||||
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),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the Yr.no sensor."""
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
|
@ -63,32 +69,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
coordinates = dict(lat=latitude, lon=longitude, msl=elevation)
|
||||
|
||||
weather = YrData(coordinates)
|
||||
|
||||
dev = []
|
||||
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
|
||||
dev.append(YrSensor(sensor_type, weather))
|
||||
dev.append(YrSensor(sensor_type))
|
||||
yield from async_add_devices(dev)
|
||||
|
||||
# add symbol as default sensor
|
||||
if len(dev) == 0:
|
||||
dev.append(YrSensor("symbol", weather))
|
||||
add_devices(dev)
|
||||
weather = YrData(hass, coordinates, dev)
|
||||
yield from weather.async_update()
|
||||
|
||||
|
||||
class YrSensor(Entity):
|
||||
"""Representation of an Yr.no sensor."""
|
||||
|
||||
def __init__(self, sensor_type, weather):
|
||||
def __init__(self, sensor_type):
|
||||
"""Initialize the sensor."""
|
||||
self.client_name = 'yr'
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.type = sensor_type
|
||||
self._state = None
|
||||
self._weather = weather
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
|
||||
self._update = None
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -100,6 +99,11 @@ class YrSensor(Entity):
|
|||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self): # pylint: disable=no-self-use
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Weather symbol if type is symbol."""
|
||||
|
@ -120,78 +124,97 @@ class YrSensor(Entity):
|
|||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from yr.no and updates the states."""
|
||||
now = dt_util.utcnow()
|
||||
# Check if data should be updated
|
||||
if self._update is not None and now <= self._update:
|
||||
return
|
||||
|
||||
self._weather.update()
|
||||
|
||||
# Find sensor
|
||||
for time_entry in self._weather.data['product']['time']:
|
||||
valid_from = dt_util.parse_datetime(time_entry['@from'])
|
||||
valid_to = dt_util.parse_datetime(time_entry['@to'])
|
||||
|
||||
loc_data = time_entry['location']
|
||||
|
||||
if self.type not in loc_data or now >= valid_to:
|
||||
continue
|
||||
|
||||
self._update = valid_to
|
||||
|
||||
if self.type == 'precipitation' and valid_from < now:
|
||||
self._state = loc_data[self.type]['@value']
|
||||
break
|
||||
elif self.type == 'symbol' and valid_from < now:
|
||||
self._state = loc_data[self.type]['@number']
|
||||
break
|
||||
elif self.type in ('temperature', 'pressure', 'humidity',
|
||||
'dewpointTemperature'):
|
||||
self._state = loc_data[self.type]['@value']
|
||||
break
|
||||
elif self.type in ('windSpeed', 'windGust'):
|
||||
self._state = loc_data[self.type]['@mps']
|
||||
break
|
||||
elif self.type == 'windDirection':
|
||||
self._state = float(loc_data[self.type]['@deg'])
|
||||
break
|
||||
elif self.type in ('fog', 'cloudiness', 'lowClouds',
|
||||
'mediumClouds', 'highClouds'):
|
||||
self._state = loc_data[self.type]['@percent']
|
||||
break
|
||||
|
||||
|
||||
class YrData(object):
|
||||
"""Get the latest data and updates the states."""
|
||||
|
||||
def __init__(self, coordinates):
|
||||
def __init__(self, hass, coordinates, devices):
|
||||
"""Initialize the data object."""
|
||||
self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \
|
||||
'lat={lat};lon={lon};msl={msl}'.format(**coordinates)
|
||||
|
||||
self._nextrun = None
|
||||
self.devices = devices
|
||||
self.data = {}
|
||||
self.update()
|
||||
self.hass = hass
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data from yr.no."""
|
||||
# Check if new will be available
|
||||
if self._nextrun is not None and dt_util.utcnow() <= self._nextrun:
|
||||
return
|
||||
try:
|
||||
with requests.Session() as sess:
|
||||
response = sess.get(self._url)
|
||||
except requests.RequestException:
|
||||
return
|
||||
if response.status_code != 200:
|
||||
return
|
||||
data = response.text
|
||||
def try_again(err: str):
|
||||
"""Schedule again later."""
|
||||
_LOGGER.warning('Retrying in 15 minutes: %s', err)
|
||||
nxt = dt_util.utcnow() + timedelta(minutes=15)
|
||||
async_track_point_in_utc_time(self.hass, self.async_update, nxt)
|
||||
|
||||
import xmltodict
|
||||
self.data = xmltodict.parse(data)['weatherdata']
|
||||
model = self.data['meta']['model']
|
||||
if '@nextrun' not in model:
|
||||
model = model[0]
|
||||
self._nextrun = dt_util.parse_datetime(model['@nextrun'])
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
resp = yield from self.hass.websession.get(self._url)
|
||||
if resp.status != 200:
|
||||
try_again('{} returned {}'.format(self._url, resp.status))
|
||||
return
|
||||
text = yield from resp.text()
|
||||
self.hass.loop.create_task(resp.release())
|
||||
except asyncio.TimeoutError as err:
|
||||
try_again(err)
|
||||
return
|
||||
except HTTPException as err:
|
||||
resp.close()
|
||||
try_again(err)
|
||||
return
|
||||
|
||||
try:
|
||||
import xmltodict
|
||||
self.data = xmltodict.parse(text)['weatherdata']
|
||||
model = self.data['meta']['model']
|
||||
if '@nextrun' not in model:
|
||||
model = model[0]
|
||||
next_run = dt_util.parse_datetime(model['@nextrun'])
|
||||
except (ExpatError, IndexError) as err:
|
||||
try_again(err)
|
||||
return
|
||||
|
||||
# Schedule next execution
|
||||
async_track_point_in_utc_time(self.hass, self.async_update, next_run)
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
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'])
|
||||
|
||||
loc_data = time_entry['location']
|
||||
|
||||
if dev.type not in loc_data or now >= valid_to:
|
||||
continue
|
||||
|
||||
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())
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=self.hass.loop)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue