Mqtt binary sensor expire after (#26058)

* Added expire_after to mqtt binary_sensor. Updated mqtt test_binary_sensor test.

* Cleanup MQTT Binary Sensor and tests after suggestions

* Updated to not alter state at all

* Change to include custom expired variable, and override available property to check expired

* Added # pylint: disable=no-member
This commit is contained in:
Albert Gouws 2019-09-22 06:42:03 +12:00 committed by Erik Montnemery
parent 9d0cb899ec
commit e394be7337
2 changed files with 150 additions and 3 deletions

View File

@ -1,4 +1,5 @@
"""Support for MQTT binary sensors."""
from datetime import timedelta
import logging
import voluptuous as vol
@ -21,7 +22,9 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.helpers.event as evt
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import dt as dt_util
from . import (
ATTR_DISCOVERY_HASH,
@ -43,12 +46,14 @@ CONF_OFF_DELAY = "off_delay"
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_FORCE_UPDATE = False
CONF_EXPIRE_AFTER = "expire_after"
PLATFORM_SCHEMA = (
mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFF_DELAY): vol.All(vol.Coerce(int), vol.Range(min=0)),
@ -112,8 +117,9 @@ class MqttBinarySensor(
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None
self._sub_state = None
self._expiration_trigger = None
self._delay_listener = None
self._expired = None
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@ -153,6 +159,26 @@ class MqttBinarySensor(
def state_message_received(msg):
"""Handle a new received MQTT state message."""
payload = msg.payload
# auto-expire enabled?
expire_after = self._config.get(CONF_EXPIRE_AFTER)
if expire_after is not None and expire_after > 0:
# When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message
self._expired = False
# Reset old trigger
if self._expiration_trigger:
self._expiration_trigger()
self._expiration_trigger = None
# Set new trigger
expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after)
self._expiration_trigger = async_track_point_in_utc_time(
self.hass, self.value_is_expired, expiration_at
)
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
payload = value_template.async_render_with_possible_json_value(
@ -202,6 +228,15 @@ class MqttBinarySensor(
await MqttAttributes.async_will_remove_from_hass(self)
await MqttAvailability.async_will_remove_from_hass(self)
@callback
def value_is_expired(self, *_):
"""Triggered when value is expired."""
self._expiration_trigger = None
self._expired = True
self.async_write_ha_state()
@property
def should_poll(self):
"""Return the polling state."""
@ -231,3 +266,12 @@ class MqttBinarySensor(
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@property
def available(self) -> bool:
"""Return true if the device is available and value has not expired."""
expire_after = self._config.get(CONF_EXPIRE_AFTER)
# pylint: disable=no-member
return MqttAvailability.available.fget(self) and (
expire_after is None or not self._expired
)

View File

@ -1,7 +1,8 @@
"""The tests for the MQTT binary sensor platform."""
from datetime import timedelta
from datetime import datetime, timedelta
import json
from unittest.mock import ANY
from unittest.mock import ANY, patch
from homeassistant.components import binary_sensor, mqtt
from homeassistant.components.mqtt.discovery import async_start
@ -24,6 +25,107 @@ from tests.common import (
)
async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog):
"""Test the expiration of the value."""
assert await async_setup_component(
hass,
binary_sensor.DOMAIN,
{
binary_sensor.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "test-topic",
"expire_after": 4,
"force_update": True,
"availability_topic": "availability-topic",
}
},
)
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(hass, "availability-topic", "online")
state = hass.states.get("binary_sensor.test")
assert state.state != STATE_UNAVAILABLE
await expires_helper(hass, mqtt_mock, caplog)
async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog):
"""Test the expiration of the value."""
assert await async_setup_component(
hass,
binary_sensor.DOMAIN,
{
binary_sensor.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "test-topic",
"expire_after": 4,
"force_update": True,
}
},
)
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
await expires_helper(hass, mqtt_mock, caplog)
async def expires_helper(hass, mqtt_mock, caplog):
"""Run the basic expiry code."""
now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC)
with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now):
async_fire_time_changed(hass, now)
async_fire_mqtt_message(hass, "test-topic", "ON")
await hass.async_block_till_done()
# Value was set correctly.
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_ON
# Time jump +3s
now = now + timedelta(seconds=3)
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
# Value is not yet expired
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_ON
# Next message resets timer
with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now):
async_fire_time_changed(hass, now)
async_fire_mqtt_message(hass, "test-topic", "OFF")
await hass.async_block_till_done()
# Value was updated correctly.
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
# Time jump +3s
now = now + timedelta(seconds=3)
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
# Value is not yet expired
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
# Time jump +2s
now = now + timedelta(seconds=2)
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
# Value is expired now
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_UNAVAILABLE
async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock):
"""Test the setting of the value via MQTT."""
assert await async_setup_component(
@ -41,6 +143,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock):
)
state = hass.states.get("binary_sensor.test")
assert state.state == STATE_OFF
async_fire_mqtt_message(hass, "test-topic", "ON")