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
Adding expire_after to mqtt sensor to expire outdated values #6708
Conversation
tests/components/sensor/test_mqtt.py
Outdated
""" Not yet expired """ | ||
state = self.hass.states.get('sensor.test') | ||
self.assertEqual('100', state.state) | ||
|
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.
blank line contains whitespace
tests/components/sensor/test_mqtt.py
Outdated
|
||
state = self.hass.states.get('sensor.test') | ||
self.assertEqual('100', state.state) | ||
|
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.
blank line contains whitespace
tests/components/sensor/test_mqtt.py
Outdated
@@ -37,11 +38,49 @@ def test_setting_sensor_value_via_mqtt_message(self): | |||
fire_mqtt_message(self.hass, 'test-topic', '100') | |||
self.hass.block_till_done() | |||
state = self.hass.states.get('sensor.test') | |||
|
|||
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.
blank line contains whitespace
@@ -65,6 +71,8 @@ def __init__(self, name, state_topic, qos, unit_of_measurement, | |||
self._unit_of_measurement = unit_of_measurement | |||
self._force_update = force_update | |||
self._template = value_template | |||
self._expire_after = int(expire_after or 0) |
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 use the config schema to convert it to an integer and set default value
@@ -31,6 +35,7 @@ | |||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, | |||
}) | |||
|
|||
SCAN_INTERVAL = timedelta(seconds=1) |
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.
Remove this.
@@ -73,6 +81,9 @@ def async_added_to_hass(self): | |||
""" | |||
@callback | |||
def message_received(topic, payload, qos): | |||
"""Reset expiration time.""" | |||
self._value_expiration_at = time.time() + self._expire_after |
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 use the event helper async_track_point_in_utc_time
to call an async function to set the state to None. async_track_point_in_utc_time
will return a function that can be called to remove the listener. So on message received, remove the last listener and add a new time listener.
"""No polling needed.""" | ||
return False | ||
"""Polling needed only for auto-expire.""" | ||
return self._expire_after > 0 |
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.
This should not be implemented with polling
tests/components/sensor/test_mqtt.py
Outdated
state = self.hass.states.get('sensor.test') | ||
self.assertEqual('100', state.state) | ||
|
||
time.sleep(1) |
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.
You are never allowed to call time.sleep in tests. Please use the mock_fire_time_changed
helper.
tests/components/sensor/test_mqtt.py
Outdated
time.sleep(1) | ||
# FIXME: how to call update() on the sensor? | ||
|
||
""" Not yet expired """ |
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.
Comments in Python start with a #
tests/components/sensor/test_mqtt.py
Outdated
state = self.hass.states.get('sensor.test') | ||
self.assertEqual('100', state.state) | ||
|
||
time.sleep(2) |
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.
tests/components/sensor/test_mqtt.py
Outdated
""" Expired """ | ||
state = self.hass.states.get('sensor.test') | ||
# FIXME: this will fail unless the fixmes above are fixed | ||
# self.assertEqual('unknown', state.state) |
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.
stale comment.
@balloob Thank you very much for the good feedback. I'm new to python and new to home assistant development as well, so I'm glad that you point me in the right direction. I'll revise my code later today. Regards, |
@balloob I tried to change everything according your comments. Coding part was easy, the lint stuff was the really hard part. Hope it's good now. |
dt_util.utcnow() + | ||
timedelta(seconds=self._expire_after) | ||
) | ||
async_track_point_in_utc_time(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.
async_track_point_in_utc_time
returns a function that will unsubscribe the just subscribed listener. So instead of storing a timestamp when to expire, unsubscribe the old listener if it exists and subscribe a new one.
Then in the callback, set the unsubscribe function to 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.
Will this be thread-safe? E.g. no race-condition between unsubscribing a (probably in exactly in this moment executed callback) and a new message?
Flow with new code:
- Callback is executed in the moment the message comes in
- Unsubscribe is executed -> too late to have effect
- new value is set
- already running Callback resets the value
The more I think about it, the more I have the impression that I can also run in that with my current code:
Flow with my old code:
- Callback is executed in the moment the message comes in
- Callback checks time, detects that the value is to expire
- new expire time and value is set
- already running Callback resets the value
Any thoughts about the thread safety here?
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.
To answer this question, the mqtt component is written using Python's asyncio library, which means that only one of these callbacks can be running at a time. If you do some searching for asyncio you can find some good literature.
if self._template is not None: | ||
payload = self._template.async_render_with_possible_json_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.
why did you change 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.
Line was 80 chars long (not my edit but wanted to have it fixed).
tests/components/sensor/test_mqtt.py
Outdated
time = time + timedelta(seconds=1) | ||
fire_time_changed(self.hass, time) | ||
|
||
""" Not yet expired """ |
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 prefix comments with #
. Only doc strings (first comment in a function) should contain triple quotes.
tests/components/sensor/test_mqtt.py
Outdated
|
||
""" Expired """ | ||
state = self.hass.states.get('sensor.test') | ||
# FIXME: this will fail unless the fixmes above are fixed |
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.
Soo, fixed now?
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.
Oops, forgot that one ;) Yes, the test is fixed now.
tests/components/sensor/test_mqtt.py
Outdated
|
||
# Expired | ||
state = self.hass.states.get('sensor.test') | ||
# FIXME: I have no idea why this does not work. Got stuck here, help plz! :-( |
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 (85 > 79 characters)
@@ -28,6 +32,7 @@ | |||
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ | |||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | |||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, | |||
vol.Optional(CONF_EXPIRE_AFTER, default=0): cv.positive_int, |
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.
Can we remove the default and just check if self._expire_after is not None:
below?
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 can but i'd still have to check 0 and None because with 0 it would cause strange behaviour otherwise.
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.
Fair enough. Sounds good to me.
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.
Which one? default or check 0 and 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.
Sorry, you can keep it as is. keep the default zero.
if self._template is not None: | ||
payload = self._template.async_render_with_possible_json_value( | ||
template = self._template | ||
payload = template.async_render_with_possible_json_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.
Why did this change?
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 was >79 chars long. I wonder how it passed the checks
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.
It looked to me that it was exactly 79 characters long. It should be fine. If it was in there it is OK for length.
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.
You're right, don't know why I got an error there. Maybe I accidently idented one more. Reverted.
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.
Yeah, I figured it probably got indented while working on it, then reverted.
Thanks for the contribution! 🎉 |
Description:
Related issue (if applicable): fixes #6705
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#2297
Example entry for
configuration.yaml
(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
tox
run successfully. Your PR cannot be merged unless tests passI extended the sensors/mqtt_test.py to cover the new feature.