-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
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
Make WUnderground async #12385
Conversation
assert "Weather Summary" == friendly_name | ||
elif entity_id == 'sensor.pws_alerts': | ||
assert 1 == device.state | ||
assert 'This is a test alert message' == device.device_state_attributes['Message'] |
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.
line too long (94 > 79 characters)
friendly_name = device.name | ||
|
||
if entity_id == 'sensor.pws_weather': | ||
assert 'https://icons.wxug.com/i/c/k/clear.gif' == device.entity_picture |
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.
line too long (84 > 79 characters)
"""Test that the component is loaded if passed in PWS Id.""" | ||
aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) | ||
aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json')) | ||
aioclient_mock.get(INVALID_URL, text=load_fixture('wunderground-error.json')) |
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.
line too long (81 > 79 characters)
@@ -764,28 +770,31 @@ def request_feature(self, 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) |
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.
self._features
needs to be sorted in order to make the generated URL stable for tests - i.e. so that the generated URL doesn't depend on the order of execution.
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.
Please also remove the use of STATE_UNKNOWN
from this module. Default unknown state should be None
in the entities. The base entity class will handle this and return the correct unknown state.
if not rest.data: | ||
raise PlatformNotReady | ||
|
||
add_devices(sensors) | ||
async_add_devices(sensors) | ||
|
||
return True |
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.
Please remove this. Nothing is checking this return value.
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.
Fixed. Side note: Does this comment apply to the whole codebase? Was wondering about that in #12274
"""Get the latest data from WUnderground.""" | ||
try: | ||
result = requests.get(self._build_url(), timeout=10).json() | ||
websession = async_get_clientsession(self._hass) |
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 think the session is persistent, so you should not do this in the update method but in async_setup_platform
and pass it to the instance and store the session as an instance attribute.
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.
Sure. Fixed.
self.data = result | ||
return True | ||
self.data = result | ||
return True |
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.
Nothing is checking this return value.
_LOGGER.error("Error fetching WUnderground data: %s", repr(err)) | ||
self.data = None | ||
self.data = None | ||
return False |
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.
Same as above.
|
||
result = yield from wunderground.async_setup_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.
Instead setup the component and assert that a platform was setup with assert_setup_component
.
|
||
result = yield from wunderground.async_setup_platform(hass, VALID_CONFIG, | ||
async_add_devices) | ||
assert result |
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.
Assert that async_add_devices
was called once.
|
||
def add_devices(self, devices): | ||
@callback | ||
def async_add_devices(new_devices): |
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 don't want to mock this function, append each call to this function to a list that is accessible from the outer scope, similar to devices
. This will allow you to check that this function was called.
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 know the difference makes it difficult to see the changes ... but that's exactly what I'm doing at the moment 👀?
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 think you're only appending the devices currently, correct?
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.
Ah, you're right. Didn't understand your comment completely there 🙈
@@ -663,7 +668,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( |
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.
All other platforms using async_generate_entity_i
d this way, but should we really be using an event loop callback from within __init__
? Would async_added_to_hass
also work or is the entity_id required before that method is called?
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.
Why not? It's not doing any I/O, so should be ok.
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.
In order to call an async_*
function in Home Assistant we need to guarantee that the caller is inside the event loop. If we can't guarantee that, we usually wrap the async call inside a hass.add_job()
.
So, because of our direct call to async_generate_entity_id()
we'd also need to guarantee that __init__
is executed from within the event loop. Can we guarantee that though? In this case, it's only used in asyn_setup_platform
(therefore inside the event loop), but I mean the object could also be initialized from outside the event loop.
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.
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.
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.
async_added_to_hass
is executed after entity_id is assigned. This is fine since the platform uses async_setup_platform
I've updated the code according to your comments now, though I've kind of hit a wall and need your advise:
|
If you add a second argument
|
Thank you very much! That's very good to know, and patching |
Just drop support for |
@balloob I've now removed |
@@ -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() |
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 True
to async_add_devices
because 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.
If you do this update before initializing the sensors, you will not do unnecessary work.
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, ...)
It also means that you don't have to pass
True
toasync_add_devices
The reason I'm passing True
to async_add_devices
is that the only way for sensors to get new data is through their own WUndergroundSensor#async_update
. They don't get notified by WUndergroundData
that 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 before async_update
is automatically called is by passing True
to async_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 weather
component (btw, I'm not using this platform too much, so I wouldn't be the one who would like to do that 🤓).
Description:
Makes WUnderground sensor platform async, using aiohttp's HTTP client.
Previous tests had to be updated quite significantly to use the
aiohttp_mock
fixture. During that process, the JSON payloads were moved to file fixtures. I didn't know how aunittest.TestCase
class could be transformed to handle async tests, so I had to move the tests to top-level coroutines and change theself.assert*
calls.If someone knows how to fix the tests/make them aiohttp-aware without changing the whole
test_wunderground.py
file and making reviewing more difficult, please tell me.Example entry for
configuration.yaml
(if applicable):Checklist:
If the code does not interact with devices:
tox
run successfully. Your PR cannot be merged unless tests pass