-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Make WUnderground async #12385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make WUnderground async #12385
Changes from all commits
cecdc4a
c546d8e
371767d
52000ca
988e907
20bcc44
2b37fcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,21 +4,24 @@ | |
| For more details about this platform, please refer to the documentation at | ||
| https://home-assistant.io/components/sensor.wunderground/ | ||
| """ | ||
| import asyncio | ||
| from datetime import timedelta | ||
| import logging | ||
|
|
||
| import re | ||
| import requests | ||
|
|
||
| import aiohttp | ||
| import async_timeout | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.helpers.typing import HomeAssistantType | ||
| from homeassistant.helpers.typing import HomeAssistantType, ConfigType | ||
| from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT | ||
| from homeassistant.const import ( | ||
| CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, | ||
| TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS, | ||
| LENGTH_MILES, LENGTH_FEET, STATE_UNKNOWN, ATTR_ATTRIBUTION) | ||
| LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION) | ||
| from homeassistant.exceptions import PlatformNotReady | ||
| from homeassistant.helpers.entity import Entity, generate_entity_id | ||
| from homeassistant.helpers.entity import Entity, async_generate_entity_id | ||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
| from homeassistant.util import Throttle | ||
| import homeassistant.helpers.config_validation as cv | ||
|
|
||
|
|
@@ -627,7 +630,9 @@ def _get_attributes(rest): | |
| }) | ||
|
|
||
|
|
||
| def setup_platform(hass, config, add_devices, discovery_info=None): | ||
| @asyncio.coroutine | ||
| def async_setup_platform(hass: HomeAssistantType, config: ConfigType, | ||
| async_add_devices, discovery_info=None): | ||
| """Set up the WUnderground sensor.""" | ||
| latitude = config.get(CONF_LATITUDE, hass.config.latitude) | ||
| longitude = config.get(CONF_LONGITUDE, hass.config.longitude) | ||
|
|
@@ -639,13 +644,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): | |
| for variable in config[CONF_MONITORED_CONDITIONS]: | ||
| sensors.append(WUndergroundSensor(hass, rest, variable)) | ||
|
|
||
| rest.update() | ||
| yield from rest.async_update() | ||
| if not rest.data: | ||
| raise PlatformNotReady | ||
|
|
||
| add_devices(sensors) | ||
|
|
||
| return True | ||
| async_add_devices(sensors, True) | ||
|
|
||
|
|
||
| class WUndergroundSensor(Entity): | ||
|
|
@@ -663,7 +666,7 @@ def __init__(self, hass: HomeAssistantType, rest, condition): | |
| self._entity_picture = None | ||
| self._unit_of_measurement = self._cfg_expand("unit_of_measurement") | ||
| self.rest.request_feature(SENSOR_TYPES[condition].feature) | ||
| self.entity_id = generate_entity_id( | ||
| self.entity_id = async_generate_entity_id( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All other platforms using
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not? It's not doing any I/O, so should be ok.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to call an So, because of our direct call to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Entities are supposed to be initialized in setup_platform/async_setup_platform. Also the caller is responsible to properly handle the objects it calls. So it's fine like this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ENTITY_ID_FORMAT, "pws_" + condition, hass=hass) | ||
|
|
||
| def _cfg_expand(self, what, default=None): | ||
|
|
@@ -727,15 +730,16 @@ def unit_of_measurement(self): | |
| """Return the units of measurement.""" | ||
| return self._unit_of_measurement | ||
|
|
||
| def update(self): | ||
| @asyncio.coroutine | ||
| def async_update(self): | ||
| """Update current conditions.""" | ||
| self.rest.update() | ||
| yield from self.rest.async_update() | ||
|
|
||
| if not self.rest.data: | ||
| # no data, return | ||
| return | ||
|
|
||
| self._state = self._cfg_expand("value", STATE_UNKNOWN) | ||
| self._state = self._cfg_expand("value") | ||
| self._update_attrs() | ||
| self._icon = self._cfg_expand("icon", super().icon) | ||
| url = self._cfg_expand("entity_picture") | ||
|
|
@@ -757,35 +761,36 @@ def __init__(self, hass, api_key, pws_id, lang, latitude, longitude): | |
| self._longitude = longitude | ||
| self._features = set() | ||
| self.data = None | ||
| self._session = async_get_clientsession(self._hass) | ||
|
|
||
| def request_feature(self, feature): | ||
| """Register feature to be fetched from WU API.""" | ||
| self._features.add(feature) | ||
|
|
||
| def _build_url(self, baseurl=_RESOURCE): | ||
| url = baseurl.format( | ||
| self._api_key, "/".join(self._features), self._lang) | ||
| self._api_key, '/'.join(sorted(self._features)), self._lang) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if self._pws_id: | ||
| url = url + 'pws:{}'.format(self._pws_id) | ||
| else: | ||
| url = url + '{},{}'.format(self._latitude, self._longitude) | ||
|
|
||
| return url + '.json' | ||
|
|
||
| @asyncio.coroutine | ||
| @Throttle(MIN_TIME_BETWEEN_UPDATES) | ||
| def update(self): | ||
| def async_update(self): | ||
| """Get the latest data from WUnderground.""" | ||
| try: | ||
| result = requests.get(self._build_url(), timeout=10).json() | ||
| with async_timeout.timeout(10, loop=self._hass.loop): | ||
| response = yield from self._session.get(self._build_url()) | ||
| result = yield from response.json() | ||
| if "error" in result['response']: | ||
| raise ValueError(result['response']["error"] | ||
| ["description"]) | ||
| else: | ||
| self.data = result | ||
| return True | ||
| raise ValueError(result['response']["error"]["description"]) | ||
| self.data = result | ||
| except ValueError as err: | ||
| _LOGGER.error("Check WUnderground API %s", err.args) | ||
| self.data = None | ||
| except requests.RequestException as err: | ||
| except (asyncio.TimeoutError, aiohttp.ClientError) as err: | ||
| _LOGGER.error("Error fetching WUnderground data: %s", repr(err)) | ||
| self.data = None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do this update before initializing the sensors, you will not do unnecessary work. It also means that you don't have to pass
Truetoasync_add_devicesbecause the sensors have access to data the moment it is initialized.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current design of this platform (which isn't ideal, imho), when a sensor entity is created it tells the data holder that its "feature" should be monitored, so doing
WUndergroundData#async_update()before the sensors have added their features would result in an empty query. We could however instead just pass the features into the data holder directly and not through the sensors, but I tried to limit this PR for now as this platform has many improvements that could be made (using the dispatcher to not rely on polling, ...)The reason I'm passing
Truetoasync_add_devicesis that the only way for sensors to get new data is through their ownWUndergroundSensor#async_update. They don't get notified byWUndergroundDatathat a request has been made. Instead, they periodically poll data from the data holder, which also takes care of throttling the request. Therefore the only way to have some data in the entities beforeasync_updateis automatically called is by passingTruetoasync_add_devices. Again, this could be fixed by completely reworking this platform.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay, fine fine 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry if this was perceived as being a bit rude - that was not my intention. I wanted to explain why I did this so that you, and anybody attempting to change this part of the codebase in the future, can follow what's going on. It's always better to ask when some code snippet "smells wrong" 👍.
Anyway, the underlying issue of this component being weird with it using polling still persists. But I think changing that would make the most sense during a transition to the
weathercomponent (btw, I'm not using this platform too much, so I wouldn't be the one who would like to do that 🤓).