Skip to content
36 changes: 34 additions & 2 deletions homeassistant/components/sensor/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
import asyncio
import logging
from datetime import timedelta

import voluptuous as vol

Expand All @@ -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
Expand All @@ -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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
})

Expand All @@ -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,
)])

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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(

Copy link
Copy Markdown
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
Copy Markdown
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).

template = self._template
payload = template.async_render_with_possible_json_value(

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

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."""
Expand Down
67 changes: 66 additions & 1 deletion tests/components/sensor/test_mqtt.py
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):
Expand Down Expand Up @@ -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! :-(

Copy link
Copy Markdown

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)

# 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')
Expand Down