From 3d434dffc77ab79c9426d12ea5126b06abd38354 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 3 Feb 2022 16:47:24 +0100 Subject: [PATCH] Add support Mqtt switch for unkown state (#65294) * Mqtt switch allow unkown state * correct type * Update discovery tests * Optimistic mode if not state_topic is configured. * Default state UNKNOWN in optimistic mode * fix discovery test --- homeassistant/components/mqtt/switch.py | 12 ++++++-- tests/components/mqtt/test_discovery.py | 14 +++++----- tests/components/mqtt/test_switch.py | 37 +++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 6576072e407..cf5422a93eb 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -51,6 +51,8 @@ DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" +PAYLOAD_NONE = "None" + PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -105,7 +107,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" - self._state = False + self._state = None self._state_on = None self._state_off = None @@ -126,7 +128,9 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): state_off = config.get(CONF_STATE_OFF) self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] - self._optimistic = config[CONF_OPTIMISTIC] + self._optimistic = ( + config[CONF_OPTIMISTIC] or config.get(CONF_STATE_TOPIC) is None + ) self._value_template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self @@ -144,6 +148,8 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._state = True elif payload == self._state_off: self._state = False + elif payload == PAYLOAD_NONE: + self._state = None self.async_write_ha_state() @@ -172,7 +178,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._state = last_state.state == STATE_ON @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index de9150de1a2..5d94f349c58 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -14,9 +14,9 @@ from homeassistant.components.mqtt.abbreviations import ( from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import ( EVENT_STATE_CHANGED, - STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) import homeassistant.core as ha @@ -649,7 +649,7 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test_topic/some/base/topic", "ON") @@ -699,7 +699,7 @@ async def test_discovery_expansion_2(hass, mqtt_mock, caplog): assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN @pytest.mark.no_fail_on_log_exception @@ -773,7 +773,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") @@ -819,7 +819,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_2( assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") @@ -895,13 +895,13 @@ async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): assert state is not None assert state.name == "Test1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes["assumed_state"] is True async_fire_mqtt_message(hass, "homeassistant/switch/bla/state", "ON") state = hass.states.get("switch.Test1") - assert state.state == "off" + assert state.state == STATE_UNKNOWN @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 9519d7321ff..3eb998193a0 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -71,7 +72,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("switch.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_DEVICE_CLASS) == "switch" assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -85,6 +86,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get("switch.test") assert state.state == STATE_OFF + async_fire_mqtt_message(hass, "state-topic", "None") + + state = hass.states.get("switch.test") + assert state.state == STATE_UNKNOWN + async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): """Test the sending MQTT commands in optimistic mode.""" @@ -132,6 +138,26 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF +async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): + """Test the initial state in optimistic mode.""" + assert await async_setup_component( + hass, + switch.DOMAIN, + { + switch.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -152,7 +178,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("switch.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "state-topic", '{"val":"beer on"}') @@ -164,6 +190,11 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): state = hass.states.get("switch.test") assert state.state == STATE_OFF + async_fire_mqtt_message(hass, "state-topic", '{"val": null}') + + state = hass.states.get("switch.test") + assert state.state == STATE_UNKNOWN + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" @@ -236,7 +267,7 @@ async def test_custom_state_payload(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("switch.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", "HIGH")