Skip to content
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

Merged
merged 12 commits into from Mar 23, 2017

Conversation

micw
Copy link
Contributor

@micw micw commented Mar 19, 2017

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):

sensor:
  - platform: mqtt
    state_topic: "sensor/value"
    unit_of_measurement: "°C"
    expire_after: 1800

Checklist:

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • no new dependencies or files

I extended the sensors/mqtt_test.py to cover the new feature.

@mention-bot
Copy link

@micw, thanks for your PR! By analyzing the history of the files in this pull request, we identified @sfam, @fabaff and @balloob to be potential reviewers.

@homeassistant
Copy link
Contributor

Hi @micw,

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

""" Not yet expired """
state = self.hass.states.get('sensor.test')
self.assertEqual('100', state.state)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line contains whitespace


state = self.hass.states.get('sensor.test')
self.assertEqual('100', state.state)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line contains whitespace

@@ -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')

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)
Copy link
Member

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)
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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

state = self.hass.states.get('sensor.test')
self.assertEqual('100', state.state)

time.sleep(1)
Copy link
Member

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.

time.sleep(1)
# FIXME: how to call update() on the sensor?

""" Not yet expired """
Copy link
Member

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 #

state = self.hass.states.get('sensor.test')
self.assertEqual('100', state.state)

time.sleep(2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️

""" Expired """
state = self.hass.states.get('sensor.test')
# FIXME: this will fail unless the fixmes above are fixed
# self.assertEqual('unknown', state.state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stale comment.

@micw
Copy link
Contributor Author

micw commented Mar 20, 2017

@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.
Can you give me a hint about this config scheme (string to int) stuff?

Regards,
Michael.

@micw
Copy link
Contributor Author

micw commented Mar 20, 2017

@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,
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Contributor

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(
Copy link
Member

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?

Copy link
Contributor Author

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).

time = time + timedelta(seconds=1)
fire_time_changed(self.hass, time)

""" Not yet expired """
Copy link
Member

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.


""" Expired """
state = self.hass.states.get('sensor.test')
# FIXME: this will fail unless the fixmes above are fixed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Soo, fixed now?

Copy link
Contributor Author

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.


# Expired
state = self.hass.states.get('sensor.test')
# FIXME: I have no idea why this does not work. Got stuck here, help plz! :-(

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this change?

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@emlove emlove dismissed balloob’s stale review March 23, 2017 21:54

OK'd on gitter

@emlove emlove merged commit 6c59898 into home-assistant:dev Mar 23, 2017
@emlove
Copy link
Contributor

emlove commented Mar 23, 2017

Thanks for the contribution! 🎉

@fabaff fabaff mentioned this pull request Apr 6, 2017
@home-assistant home-assistant locked and limited conversation to collaborators Jun 24, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Option to expire sensor values that did not receive an updates
7 participants