-
-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Adding expire_after to mqtt sensor to expire outdated values #6708
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
Changes from 6 commits
7c62b42
45ee8b9
3413a9b
74afdb8
ae5e0e3
0dc47d7
4a25e4e
596fc2f
5d53175
b48f8fa
520a0b8
c34a562
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 |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| """ | ||
| import asyncio | ||
| import logging | ||
| from datetime import timedelta | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
|
|
@@ -16,10 +17,13 @@ | |
| from homeassistant.helpers.entity import Entity | ||
| import homeassistant.components.mqtt as mqtt | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.helpers.event import async_track_point_in_utc_time | ||
| from homeassistant.util import dt as dt_util | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| CONF_FORCE_UPDATE = 'force_update' | ||
| CONF_EXPIRE_AFTER = 'expire_after' | ||
|
|
||
| DEFAULT_NAME = 'MQTT Sensor' | ||
| DEFAULT_FORCE_UPDATE = False | ||
|
|
@@ -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, | ||
| vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, | ||
| }) | ||
|
|
||
|
|
@@ -48,6 +53,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): | |
| config.get(CONF_QOS), | ||
| config.get(CONF_UNIT_OF_MEASUREMENT), | ||
| config.get(CONF_FORCE_UPDATE), | ||
| config.get(CONF_EXPIRE_AFTER), | ||
| value_template, | ||
| )]) | ||
|
|
||
|
|
@@ -56,7 +62,7 @@ class MqttSensor(Entity): | |
| """Representation of a sensor that can be updated using MQTT.""" | ||
|
|
||
| def __init__(self, name, state_topic, qos, unit_of_measurement, | ||
| force_update, value_template): | ||
| force_update, expire_after, value_template): | ||
| """Initialize the sensor.""" | ||
| self._state = STATE_UNKNOWN | ||
| self._name = name | ||
|
|
@@ -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 = expire_after | ||
| self._expiration_trigger = None | ||
|
|
||
| def async_added_to_hass(self): | ||
| """Subscribe mqtt events. | ||
|
|
@@ -74,15 +82,39 @@ def async_added_to_hass(self): | |
| @callback | ||
| def message_received(topic, payload, qos): | ||
| """A new MQTT message has been received.""" | ||
| # auto-expire enabled? | ||
| if self._expire_after > 0: | ||
| # Reset old trigger | ||
| if self._expiration_trigger: | ||
| self._expiration_trigger() | ||
| self._expiration_trigger = None | ||
|
|
||
| # Set new trigger | ||
| expiration_at = ( | ||
| dt_util.utcnow() + timedelta(seconds=self._expire_after)) | ||
|
|
||
| self._expiration_trigger = async_track_point_in_utc_time( | ||
| self.hass, | ||
| self.value_is_expired, | ||
| expiration_at) | ||
|
|
||
| if self._template is not None: | ||
| payload = self._template.async_render_with_possible_json_value( | ||
|
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 did you change this?
Contributor
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. Line was 80 chars long (not my edit but wanted to have it fixed). |
||
| template = self._template | ||
| payload = template.async_render_with_possible_json_value( | ||
|
Contributor
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 did this change?
Contributor
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. Line was >79 chars long. I wonder how it passed the checks
Contributor
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. 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.
Contributor
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. You're right, don't know why I got an error there. Maybe I accidently idented one more. Reverted.
Contributor
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. Yeah, I figured it probably got indented while working on it, then reverted. |
||
| payload, self._state) | ||
| self._state = payload | ||
| self.hass.async_add_job(self.async_update_ha_state()) | ||
|
|
||
| return mqtt.async_subscribe( | ||
| self.hass, self._state_topic, message_received, self._qos) | ||
|
|
||
| @callback | ||
| def value_is_expired(self, *_): | ||
| """Triggered when value is expired.""" | ||
| self._expiration_trigger = None | ||
| self._state = STATE_UNKNOWN | ||
| self.hass.async_add_job(self.async_update_ha_state()) | ||
|
|
||
| @property | ||
| def should_poll(self): | ||
| """No polling needed.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,17 @@ | ||
| """The tests for the MQTT sensor platform.""" | ||
| import unittest | ||
|
|
||
| from datetime import timedelta, datetime | ||
|
|
||
| import homeassistant.core as ha | ||
| from homeassistant.setup import setup_component | ||
| import homeassistant.components.sensor as sensor | ||
| from homeassistant.const import EVENT_STATE_CHANGED | ||
| from tests.common import mock_mqtt_component, fire_mqtt_message | ||
| import homeassistant.util.dt as dt_util | ||
|
|
||
| from tests.common import mock_mqtt_component, fire_mqtt_message | ||
| from tests.common import get_test_home_assistant, mock_component | ||
| from tests.common import fire_time_changed | ||
|
|
||
|
|
||
| class TestSensorMQTT(unittest.TestCase): | ||
|
|
@@ -42,6 +46,67 @@ def test_setting_sensor_value_via_mqtt_message(self): | |
| self.assertEqual('fav unit', | ||
| state.attributes.get('unit_of_measurement')) | ||
|
|
||
| def test_setting_sensor_value_expires(self): | ||
| """Test the expiration of the value.""" | ||
| mock_component(self.hass, 'mqtt') | ||
| assert setup_component(self.hass, sensor.DOMAIN, { | ||
| sensor.DOMAIN: { | ||
| 'platform': 'mqtt', | ||
| 'name': 'test', | ||
| 'state_topic': 'test-topic', | ||
| 'unit_of_measurement': 'fav unit', | ||
| 'expire_after': '4', | ||
| 'force_update': True | ||
| } | ||
| }) | ||
|
|
||
| now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) | ||
| fire_time_changed(self.hass, now) | ||
|
|
||
| state = self.hass.states.get('sensor.test') | ||
| self.assertEqual('unknown', state.state) | ||
|
|
||
| fire_mqtt_message(self.hass, 'test-topic', '100') | ||
| self.hass.block_till_done() | ||
|
|
||
| state = self.hass.states.get('sensor.test') | ||
| self.assertEqual('100', state.state) | ||
|
|
||
| # +3s | ||
| now = now + timedelta(seconds=3) | ||
| fire_time_changed(self.hass, now) | ||
| self.hass.block_till_done() | ||
|
|
||
| # Not yet expired | ||
| state = self.hass.states.get('sensor.test') | ||
| self.assertEqual('100', state.state) | ||
|
|
||
| # Next message resets timer | ||
| fire_mqtt_message(self.hass, 'test-topic', '100') | ||
| self.hass.block_till_done() | ||
|
|
||
| state = self.hass.states.get('sensor.test') | ||
| self.assertEqual('100', state.state) | ||
|
|
||
| # +3s | ||
| now = now + timedelta(seconds=3) | ||
| fire_time_changed(self.hass, now) | ||
| self.hass.block_till_done() | ||
|
|
||
| # Not yet expired | ||
| state = self.hass.states.get('sensor.test') | ||
| self.assertEqual('100', state.state) | ||
|
|
||
| # +3s | ||
| now = now + timedelta(seconds=3) | ||
| fire_time_changed(self.hass, now) | ||
| self.hass.block_till_done() | ||
|
|
||
| # 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 commentThe reason will be displayed to describe this comment to others. Learn more. line too long (85 > 79 characters) |
||
| # self.assertEqual('unknown', state.state) | ||
|
|
||
| def test_setting_sensor_value_via_mqtt_json_message(self): | ||
| """Test the setting of the value via MQTT with JSON playload.""" | ||
| mock_component(self.hass, 'mqtt') | ||
|
|
||
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.