From 35287828bce9db6b9132746db182b69fde548215 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Oct 2020 11:17:23 +0200 Subject: [PATCH] Poll state when Tasmota device becomes available (#41401) --- homeassistant/components/tasmota/mixins.py | 6 +- tests/components/tasmota/conftest.py | 8 ++ .../components/tasmota/test_binary_sensor.py | 13 ++++ tests/components/tasmota/test_common.py | 74 ++++++++++++++++++- tests/components/tasmota/test_mixins.py | 53 +++++++++++++ tests/components/tasmota/test_switch.py | 13 ++++ 6 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 tests/components/tasmota/test_mixins.py diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index ec58eebd89d..3e25babf658 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -96,6 +96,8 @@ class TasmotaAvailability(TasmotaEntity): @callback def availability_updated(self, available: bool) -> None: """Handle updated availability.""" + if available and not self._available: + self._tasmota_entity.poll_status() self._available = available self.async_write_ha_state() @@ -103,13 +105,13 @@ class TasmotaAvailability(TasmotaEntity): def async_mqtt_connected(self, _): """Update state on connection/disconnection to MQTT broker.""" if not self.hass.is_stopping: + if not mqtt_connected(self.hass): + self._available = False self.async_write_ha_state() @property def available(self) -> bool: """Return if the device is available.""" - if not mqtt_connected(self.hass) and not self.hass.is_stopping: - return False return self._available diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py index 9a1907ecf3f..73a88f23646 100644 --- a/tests/components/tasmota/conftest.py +++ b/tests/components/tasmota/conftest.py @@ -9,6 +9,7 @@ from homeassistant.components.tasmota.const import ( DOMAIN, ) +from tests.async_mock import patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry @@ -24,6 +25,13 @@ def entity_reg(hass): return mock_registry(hass) +@pytest.fixture(autouse=True) +def disable_debounce(): + """Set MQTT debounce timer to zero.""" + with patch("hatasmota.mqtt.DEBOUNCE_TIMEOUT", 0): + yield + + async def setup_tasmota_helper(hass): """Set up Tasmota.""" hass.config.components.add("tasmota") diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 1b2c7757f3f..69d49a6ca8f 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -25,6 +25,7 @@ from .test_common import ( DEFAULT_CONFIG, help_test_availability, help_test_availability_discovery_update, + help_test_availability_poll_state, help_test_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, @@ -162,6 +163,18 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): ) +async def test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, setup_tasmota +): + """Test polling after MQTT connection (re)established.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + poll_topic = "tasmota_49A3BC/cmnd/STATUS" + await help_test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, binary_sensor.DOMAIN, config, poll_topic, "8" + ) + + async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog, setup_tasmota): """Test removal of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG) diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 530d5aef8d6..bb424a01175 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -59,22 +59,27 @@ DEFAULT_CONFIG = { async def help_test_availability_when_connection_lost( hass, mqtt_client_mock, mqtt_mock, domain, config ): - """Test availability after MQTT disconnection.""" + """Test availability after MQTT disconnection. + + This is a test helper for the TasmotaAvailability mixin. + """ async_fire_mqtt_message( hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", json.dumps(config), ) await hass.async_block_till_done() + + # Device online async_fire_mqtt_message( hass, get_topic_tele_will(config), config_get_state_online(config), ) - state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE + # Disconnected from MQTT server -> state changed to unavailable mqtt_mock.connected = False await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) await hass.async_block_till_done() @@ -83,12 +88,22 @@ async def help_test_availability_when_connection_lost( state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE + # Reconnected to MQTT server -> state still unavailable mqtt_mock.connected = True await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + # Receive LWT again + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -194,6 +209,61 @@ async def help_test_availability_discovery_update( assert state.state != STATE_UNAVAILABLE +async def help_test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, domain, config, poll_topic, poll_payload +): + """Test polling of state when device is available. + + This is a test helper for the TasmotaAvailability mixin. + """ + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Device online, verify poll for state + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with(poll_topic, poll_payload, 0, False) + mqtt_mock.async_publish.reset_mock() + + # Disconnected from MQTT server + mqtt_mock.connected = False + await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + assert not mqtt_mock.async_publish.called + + # Reconnected to MQTT server + mqtt_mock.connected = True + await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + assert not mqtt_mock.async_publish.called + + # Device online, verify poll for state + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with(poll_topic, poll_payload, 0, False) + + async def help_test_discovery_removal( hass, mqtt_mock, caplog, domain, config1, config2 ): diff --git a/tests/components/tasmota/test_mixins.py b/tests/components/tasmota/test_mixins.py new file mode 100644 index 00000000000..a90d4c01fd7 --- /dev/null +++ b/tests/components/tasmota/test_mixins.py @@ -0,0 +1,53 @@ +"""The tests for the Tasmota mixins.""" +import copy +import json + +from hatasmota.const import CONF_MAC +from hatasmota.utils import config_get_state_online, get_topic_tele_will + +from homeassistant.components.tasmota.const import DEFAULT_PREFIX + +from .test_common import DEFAULT_CONFIG + +from tests.async_mock import call +from tests.common import async_fire_mqtt_message + + +async def test_availability_poll_state_once( + hass, mqtt_client_mock, mqtt_mock, setup_tasmota +): + """Test several entities send a single message to update state.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + config["rl"][1] = 1 + config["swc"][0] = 1 + config["swc"][1] = 1 + poll_payload_relay = "" + poll_payload_switch = "8" + poll_topic_relay = "tasmota_49A3BC/cmnd/STATE" + poll_topic_switch = "tasmota_49A3BC/cmnd/STATUS" + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Device online, verify poll for state + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_has_calls( + [ + call(poll_topic_relay, poll_payload_relay, 0, False), + call(poll_topic_switch, poll_payload_switch, 0, False), + ], + any_order=True, + ) diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 9d5eb0c5985..47781771098 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -10,6 +10,7 @@ from .test_common import ( DEFAULT_CONFIG, help_test_availability, help_test_availability_discovery_update, + help_test_availability_poll_state, help_test_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, @@ -124,6 +125,18 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): ) +async def test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, setup_tasmota +): + """Test polling after MQTT connection (re)established.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + poll_topic = "tasmota_49A3BC/cmnd/STATE" + await help_test_availability_poll_state( + hass, mqtt_client_mock, mqtt_mock, switch.DOMAIN, config, poll_topic, "" + ) + + async def test_discovery_removal_switch(hass, mqtt_mock, caplog, setup_tasmota): """Test removal of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG)