Add support for off_delay to MQTT binary_sensor (#16993)

* Add support for off_delay to MQTT binary_sensor

* Fix debounce, add testcase

* Make off_delay number of seconds instead of timedelta

* Update mqtt.py

* Fix testcase, remove CONF_OFF_DELAY from const.py
This commit is contained in:
emontnemery 2018-10-11 19:14:23 +02:00 committed by Paulus Schoutsen
parent 61bf4d8a29
commit 5961f2f577
2 changed files with 72 additions and 4 deletions

View file

@ -23,11 +23,13 @@ from homeassistant.components.mqtt import (
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.helpers.event as evt
from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'MQTT Binary sensor' DEFAULT_NAME = 'MQTT Binary sensor'
CONF_OFF_DELAY = 'off_delay'
CONF_UNIQUE_ID = 'unique_id' CONF_UNIQUE_ID = 'unique_id'
DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
@ -41,6 +43,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_OFF_DELAY):
vol.All(vol.Coerce(int), vol.Range(min=0)),
# Integrations shouldn't never expose unique_id through configuration # Integrations shouldn't never expose unique_id through configuration
# this here is an exception because MQTT is a msg transport, not a protocol # this here is an exception because MQTT is a msg transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
@ -81,6 +85,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
config.get(CONF_DEVICE_CLASS), config.get(CONF_DEVICE_CLASS),
config.get(CONF_QOS), config.get(CONF_QOS),
config.get(CONF_FORCE_UPDATE), config.get(CONF_FORCE_UPDATE),
config.get(CONF_OFF_DELAY),
config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF), config.get(CONF_PAYLOAD_OFF),
config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_AVAILABLE),
@ -97,8 +102,8 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class, def __init__(self, name, state_topic, availability_topic, device_class,
qos, force_update, payload_on, payload_off, payload_available, qos, force_update, off_delay, payload_on, payload_off,
payload_not_available, value_template, payload_available, payload_not_available, value_template,
unique_id: Optional[str], device_config: Optional[ConfigType], unique_id: Optional[str], device_config: Optional[ConfigType],
discovery_hash): discovery_hash):
"""Initialize the MQTT binary sensor.""" """Initialize the MQTT binary sensor."""
@ -114,9 +119,11 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
self._payload_off = payload_off self._payload_off = payload_off
self._qos = qos self._qos = qos
self._force_update = force_update self._force_update = force_update
self._off_delay = off_delay
self._template = value_template self._template = value_template
self._unique_id = unique_id self._unique_id = unique_id
self._discovery_hash = discovery_hash self._discovery_hash = discovery_hash
self._delay_listener = None
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe mqtt events.""" """Subscribe mqtt events."""
@ -139,6 +146,20 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
self._name, self._state_topic) self._name, self._state_topic)
return return
if (self._state and self._off_delay is not None):
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None
self._state = False
self.async_schedule_update_ha_state()
if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = evt.async_call_later(
self.hass, self._off_delay, off_delay_listener)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
await mqtt.async_subscribe( await mqtt.async_subscribe(

View file

@ -1,6 +1,8 @@
"""The tests for the MQTT binary sensor platform.""" """The tests for the MQTT binary sensor platform."""
import json import json
import unittest import unittest
from unittest.mock import Mock
from datetime import timedelta
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import setup_component, async_setup_component
@ -10,10 +12,12 @@ from homeassistant.components.mqtt.discovery import async_start
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE
import homeassistant.util.dt as dt_util
from tests.common import ( from tests.common import (
get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message, get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message,
mock_component, mock_mqtt_component, async_mock_mqtt_component, fire_time_changed, mock_component, mock_mqtt_component,
MockConfigEntry) async_mock_mqtt_component, MockConfigEntry)
class TestSensorMQTT(unittest.TestCase): class TestSensorMQTT(unittest.TestCase):
@ -22,6 +26,7 @@ class TestSensorMQTT(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
"""Set up things to be run when tests are started.""" """Set up things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.hass.config_entries._async_schedule_save = Mock()
mock_mqtt_component(self.hass) mock_mqtt_component(self.hass)
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
@ -209,6 +214,48 @@ class TestSensorMQTT(unittest.TestCase):
self.hass.block_till_done() self.hass.block_till_done()
self.assertEqual(2, len(events)) self.assertEqual(2, len(events))
def test_off_delay(self):
"""Test off_delay option."""
mock_component(self.hass, 'mqtt')
assert setup_component(self.hass, binary_sensor.DOMAIN, {
binary_sensor.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'test-topic',
'payload_on': 'ON',
'payload_off': 'OFF',
'off_delay': 30,
'force_update': True
}
})
events = []
@ha.callback
def callback(event):
"""Verify event got called."""
events.append(event)
self.hass.bus.listen(EVENT_STATE_CHANGED, callback)
fire_mqtt_message(self.hass, 'test-topic', 'ON')
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual(1, len(events))
fire_mqtt_message(self.hass, 'test-topic', 'ON')
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual(2, len(events))
fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=30))
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test')
self.assertEqual(STATE_OFF, state.state)
self.assertEqual(3, len(events))
async def test_unique_id(hass): async def test_unique_id(hass):
"""Test unique id option only creates one sensor per unique_id.""" """Test unique id option only creates one sensor per unique_id."""