Consequently ignore empty MQTT state payloads and set state to unknown
on "None" payload (#117813)
* Consequently ignore empty MQTT state payloads and set state to `unknown` on "None" payload * Do not change preset mode behavior * Add device tracker ignoring empty state * Ignore empty state for lock * Resolve merge errors
This commit is contained in:
parent
c616fc036e
commit
01f3a5a97c
16 changed files with 183 additions and 26 deletions
|
@ -40,6 +40,7 @@ from .const import (
|
||||||
CONF_RETAIN,
|
CONF_RETAIN,
|
||||||
CONF_STATE_TOPIC,
|
CONF_STATE_TOPIC,
|
||||||
CONF_SUPPORTED_FEATURES,
|
CONF_SUPPORTED_FEATURES,
|
||||||
|
PAYLOAD_NONE,
|
||||||
)
|
)
|
||||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||||
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
||||||
|
@ -176,6 +177,16 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
|
||||||
def _state_message_received(self, msg: ReceiveMessage) -> None:
|
def _state_message_received(self, msg: ReceiveMessage) -> None:
|
||||||
"""Run when new MQTT message has been received."""
|
"""Run when new MQTT message has been received."""
|
||||||
payload = self._value_template(msg.payload)
|
payload = self._value_template(msg.payload)
|
||||||
|
if not payload.strip(): # No output from template, ignore
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring empty payload '%s' after rendering for topic %s",
|
||||||
|
payload,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if payload == PAYLOAD_NONE:
|
||||||
|
self._attr_state = None
|
||||||
|
return
|
||||||
if payload not in (
|
if payload not in (
|
||||||
STATE_ALARM_DISARMED,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
|
|
@ -709,13 +709,16 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
def _handle_action_received(self, msg: ReceiveMessage) -> None:
|
def _handle_action_received(self, msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving action via MQTT."""
|
"""Handle receiving action via MQTT."""
|
||||||
payload = self.render_template(msg, CONF_ACTION_TEMPLATE)
|
payload = self.render_template(msg, CONF_ACTION_TEMPLATE)
|
||||||
if not payload or payload == PAYLOAD_NONE:
|
if not payload:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Invalid %s action: %s, ignoring",
|
"Invalid %s action: %s, ignoring",
|
||||||
[e.value for e in HVACAction],
|
[e.value for e in HVACAction],
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
if payload == PAYLOAD_NONE:
|
||||||
|
self._attr_hvac_action = None
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
self._attr_hvac_action = HVACAction(str(payload))
|
self._attr_hvac_action = HVACAction(str(payload))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -733,8 +736,10 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
"""Handle receiving listed mode via MQTT."""
|
"""Handle receiving listed mode via MQTT."""
|
||||||
payload = self.render_template(msg, template_name)
|
payload = self.render_template(msg, template_name)
|
||||||
|
|
||||||
if payload not in self._config[mode_list]:
|
if payload == PAYLOAD_NONE:
|
||||||
_LOGGER.error("Invalid %s mode: %s", mode_list, payload)
|
setattr(self, attr, None)
|
||||||
|
elif payload not in self._config[mode_list]:
|
||||||
|
_LOGGER.warning("Invalid %s mode: %s", mode_list, payload)
|
||||||
else:
|
else:
|
||||||
setattr(self, attr, payload)
|
setattr(self, attr, payload)
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ from .const import (
|
||||||
DEFAULT_POSITION_CLOSED,
|
DEFAULT_POSITION_CLOSED,
|
||||||
DEFAULT_POSITION_OPEN,
|
DEFAULT_POSITION_OPEN,
|
||||||
DEFAULT_RETAIN,
|
DEFAULT_RETAIN,
|
||||||
|
PAYLOAD_NONE,
|
||||||
)
|
)
|
||||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||||
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
||||||
|
@ -350,9 +351,13 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||||
self._attr_supported_features = supported_features
|
self._attr_supported_features = supported_features
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_state(self, state: str) -> None:
|
def _update_state(self, state: str | None) -> None:
|
||||||
"""Update the cover state."""
|
"""Update the cover state."""
|
||||||
self._attr_is_closed = state == STATE_CLOSED
|
if state is None:
|
||||||
|
# Reset the state to `unknown`
|
||||||
|
self._attr_is_closed = None
|
||||||
|
else:
|
||||||
|
self._attr_is_closed = state == STATE_CLOSED
|
||||||
self._attr_is_opening = state == STATE_OPENING
|
self._attr_is_opening = state == STATE_OPENING
|
||||||
self._attr_is_closing = state == STATE_CLOSING
|
self._attr_is_closing = state == STATE_CLOSING
|
||||||
|
|
||||||
|
@ -376,7 +381,7 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||||
_LOGGER.debug("Ignoring empty state message from '%s'", msg.topic)
|
_LOGGER.debug("Ignoring empty state message from '%s'", msg.topic)
|
||||||
return
|
return
|
||||||
|
|
||||||
state: str
|
state: str | None
|
||||||
if payload == self._config[CONF_STATE_STOPPED]:
|
if payload == self._config[CONF_STATE_STOPPED]:
|
||||||
if self._config.get(CONF_GET_POSITION_TOPIC) is not None:
|
if self._config.get(CONF_GET_POSITION_TOPIC) is not None:
|
||||||
state = (
|
state = (
|
||||||
|
@ -398,6 +403,8 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||||
state = STATE_OPEN
|
state = STATE_OPEN
|
||||||
elif payload == self._config[CONF_STATE_CLOSED]:
|
elif payload == self._config[CONF_STATE_CLOSED]:
|
||||||
state = STATE_CLOSED
|
state = STATE_CLOSED
|
||||||
|
elif payload == PAYLOAD_NONE:
|
||||||
|
state = None
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
(
|
(
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -42,6 +43,8 @@ from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType
|
||||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||||
from .util import valid_subscribe_topic
|
from .util import valid_subscribe_topic
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_PAYLOAD_HOME = "payload_home"
|
CONF_PAYLOAD_HOME = "payload_home"
|
||||||
CONF_PAYLOAD_NOT_HOME = "payload_not_home"
|
CONF_PAYLOAD_NOT_HOME = "payload_not_home"
|
||||||
CONF_SOURCE_TYPE = "source_type"
|
CONF_SOURCE_TYPE = "source_type"
|
||||||
|
@ -125,6 +128,13 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
|
||||||
def message_received(msg: ReceiveMessage) -> None:
|
def message_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle new MQTT messages."""
|
"""Handle new MQTT messages."""
|
||||||
payload = self._value_template(msg.payload)
|
payload = self._value_template(msg.payload)
|
||||||
|
if not payload.strip(): # No output from template, ignore
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring empty payload '%s' after rendering for topic %s",
|
||||||
|
payload,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
return
|
||||||
if payload == self._config[CONF_PAYLOAD_HOME]:
|
if payload == self._config[CONF_PAYLOAD_HOME]:
|
||||||
self._location_name = STATE_HOME
|
self._location_name = STATE_HOME
|
||||||
elif payload == self._config[CONF_PAYLOAD_NOT_HOME]:
|
elif payload == self._config[CONF_PAYLOAD_NOT_HOME]:
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -50,6 +51,8 @@ from .models import (
|
||||||
)
|
)
|
||||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_CODE_FORMAT = "code_format"
|
CONF_CODE_FORMAT = "code_format"
|
||||||
|
|
||||||
CONF_PAYLOAD_LOCK = "payload_lock"
|
CONF_PAYLOAD_LOCK = "payload_lock"
|
||||||
|
@ -205,9 +208,15 @@ class MqttLock(MqttEntity, LockEntity):
|
||||||
)
|
)
|
||||||
def message_received(msg: ReceiveMessage) -> None:
|
def message_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle new lock state messages."""
|
"""Handle new lock state messages."""
|
||||||
if (payload := self._value_template(msg.payload)) == self._config[
|
payload = self._value_template(msg.payload)
|
||||||
CONF_PAYLOAD_RESET
|
if not payload.strip(): # No output from template, ignore
|
||||||
]:
|
_LOGGER.debug(
|
||||||
|
"Ignoring empty payload '%s' after rendering for topic %s",
|
||||||
|
payload,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if payload == self._config[CONF_PAYLOAD_RESET]:
|
||||||
# Reset the state to `unknown`
|
# Reset the state to `unknown`
|
||||||
self._attr_is_locked = None
|
self._attr_is_locked = None
|
||||||
elif payload in self._valid_states:
|
elif payload in self._valid_states:
|
||||||
|
|
|
@ -122,6 +122,13 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
|
||||||
def message_received(msg: ReceiveMessage) -> None:
|
def message_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle new MQTT messages."""
|
"""Handle new MQTT messages."""
|
||||||
payload = str(self._value_template(msg.payload))
|
payload = str(self._value_template(msg.payload))
|
||||||
|
if not payload.strip(): # No output from template, ignore
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring empty payload '%s' after rendering for topic %s",
|
||||||
|
payload,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
return
|
||||||
if payload.lower() == "none":
|
if payload.lower() == "none":
|
||||||
self._attr_current_option = None
|
self._attr_current_option = None
|
||||||
return
|
return
|
||||||
|
|
|
@ -59,6 +59,7 @@ from .const import (
|
||||||
DEFAULT_POSITION_CLOSED,
|
DEFAULT_POSITION_CLOSED,
|
||||||
DEFAULT_POSITION_OPEN,
|
DEFAULT_POSITION_OPEN,
|
||||||
DEFAULT_RETAIN,
|
DEFAULT_RETAIN,
|
||||||
|
PAYLOAD_NONE,
|
||||||
)
|
)
|
||||||
from .debug_info import log_messages
|
from .debug_info import log_messages
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
|
@ -220,13 +221,16 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||||
self._attr_supported_features = supported_features
|
self._attr_supported_features = supported_features
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_state(self, state: str) -> None:
|
def _update_state(self, state: str | None) -> None:
|
||||||
"""Update the valve state properties."""
|
"""Update the valve state properties."""
|
||||||
self._attr_is_opening = state == STATE_OPENING
|
self._attr_is_opening = state == STATE_OPENING
|
||||||
self._attr_is_closing = state == STATE_CLOSING
|
self._attr_is_closing = state == STATE_CLOSING
|
||||||
if self.reports_position:
|
if self.reports_position:
|
||||||
return
|
return
|
||||||
self._attr_is_closed = state == STATE_CLOSED
|
if state is None:
|
||||||
|
self._attr_is_closed = None
|
||||||
|
else:
|
||||||
|
self._attr_is_closed = state == STATE_CLOSED
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _process_binary_valve_update(
|
def _process_binary_valve_update(
|
||||||
|
@ -242,7 +246,9 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||||
state = STATE_OPEN
|
state = STATE_OPEN
|
||||||
elif state_payload == self._config[CONF_STATE_CLOSED]:
|
elif state_payload == self._config[CONF_STATE_CLOSED]:
|
||||||
state = STATE_CLOSED
|
state = STATE_CLOSED
|
||||||
if state is None:
|
elif state_payload == PAYLOAD_NONE:
|
||||||
|
state = None
|
||||||
|
else:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Payload received on topic '%s' is not one of "
|
"Payload received on topic '%s' is not one of "
|
||||||
"[open, closed, opening, closing], got: %s",
|
"[open, closed, opening, closing], got: %s",
|
||||||
|
@ -263,6 +269,9 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||||
state = STATE_OPENING
|
state = STATE_OPENING
|
||||||
elif state_payload == self._config[CONF_STATE_CLOSING]:
|
elif state_payload == self._config[CONF_STATE_CLOSING]:
|
||||||
state = STATE_CLOSING
|
state = STATE_CLOSING
|
||||||
|
elif state_payload == PAYLOAD_NONE:
|
||||||
|
self._attr_current_valve_position = None
|
||||||
|
return
|
||||||
if state is None or position_payload != state_payload:
|
if state is None or position_payload != state_payload:
|
||||||
try:
|
try:
|
||||||
percentage_payload = ranged_value_to_percentage(
|
percentage_payload = ranged_value_to_percentage(
|
||||||
|
|
|
@ -63,6 +63,7 @@ from .const import (
|
||||||
CONF_TEMP_STATE_TEMPLATE,
|
CONF_TEMP_STATE_TEMPLATE,
|
||||||
CONF_TEMP_STATE_TOPIC,
|
CONF_TEMP_STATE_TOPIC,
|
||||||
DEFAULT_OPTIMISTIC,
|
DEFAULT_OPTIMISTIC,
|
||||||
|
PAYLOAD_NONE,
|
||||||
)
|
)
|
||||||
from .mixins import async_setup_entity_entry_helper
|
from .mixins import async_setup_entity_entry_helper
|
||||||
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
||||||
|
@ -259,10 +260,22 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity):
|
||||||
@callback
|
@callback
|
||||||
def _handle_current_mode_received(self, msg: ReceiveMessage) -> None:
|
def _handle_current_mode_received(self, msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving operation mode via MQTT."""
|
"""Handle receiving operation mode via MQTT."""
|
||||||
|
|
||||||
payload = self.render_template(msg, CONF_MODE_STATE_TEMPLATE)
|
payload = self.render_template(msg, CONF_MODE_STATE_TEMPLATE)
|
||||||
|
|
||||||
if payload not in self._config[CONF_MODE_LIST]:
|
if not payload.strip(): # No output from template, ignore
|
||||||
_LOGGER.error("Invalid %s mode: %s", CONF_MODE_LIST, payload)
|
_LOGGER.debug(
|
||||||
|
"Ignoring empty payload '%s' for current operation "
|
||||||
|
"after rendering for topic %s",
|
||||||
|
payload,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if payload == PAYLOAD_NONE:
|
||||||
|
self._attr_current_operation = None
|
||||||
|
elif payload not in self._config[CONF_MODE_LIST]:
|
||||||
|
_LOGGER.warning("Invalid %s mode: %s", CONF_MODE_LIST, payload)
|
||||||
else:
|
else:
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert isinstance(payload, str)
|
assert isinstance(payload, str)
|
||||||
|
|
|
@ -209,6 +209,14 @@ async def test_update_state_via_state_topic(
|
||||||
async_fire_mqtt_message(hass, "alarm/state", state)
|
async_fire_mqtt_message(hass, "alarm/state", state)
|
||||||
assert hass.states.get(entity_id).state == state
|
assert hass.states.get(entity_id).state == state
|
||||||
|
|
||||||
|
# Ignore empty payload (last state is STATE_ALARM_TRIGGERED)
|
||||||
|
async_fire_mqtt_message(hass, "alarm/state", "")
|
||||||
|
assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED
|
||||||
|
|
||||||
|
# Reset state on `None` payload
|
||||||
|
async_fire_mqtt_message(hass, "alarm/state", "None")
|
||||||
|
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
|
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
|
||||||
async def test_ignore_update_state_if_unknown_via_state_topic(
|
async def test_ignore_update_state_if_unknown_via_state_topic(
|
||||||
|
|
|
@ -32,7 +32,7 @@ from homeassistant.components.mqtt.climate import (
|
||||||
MQTT_CLIMATE_ATTRIBUTES_BLOCKED,
|
MQTT_CLIMATE_ATTRIBUTES_BLOCKED,
|
||||||
VALUE_TEMPLATE_KEYS,
|
VALUE_TEMPLATE_KEYS,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, STATE_UNKNOWN, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
|
@ -245,11 +245,11 @@ async def test_set_operation_pessimistic(
|
||||||
await mqtt_mock_entry()
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
assert state.state == "unknown"
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
|
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
assert state.state == "unknown"
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, "mode-state", "cool")
|
async_fire_mqtt_message(hass, "mode-state", "cool")
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
|
@ -259,6 +259,16 @@ async def test_set_operation_pessimistic(
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
assert state.state == "cool"
|
assert state.state == "cool"
|
||||||
|
|
||||||
|
# Ignored
|
||||||
|
async_fire_mqtt_message(hass, "mode-state", "")
|
||||||
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
|
assert state.state == "cool"
|
||||||
|
|
||||||
|
# Reset with `None`
|
||||||
|
async_fire_mqtt_message(hass, "mode-state", "None")
|
||||||
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
|
@ -1011,11 +1021,7 @@ async def test_handle_action_received(
|
||||||
"""Test getting the action received via MQTT."""
|
"""Test getting the action received via MQTT."""
|
||||||
await mqtt_mock_entry()
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
# Cycle through valid modes and also check for wrong input such as "None" (str(None))
|
# Cycle through valid modes
|
||||||
async_fire_mqtt_message(hass, "action", "None")
|
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
|
||||||
hvac_action = state.attributes.get(ATTR_HVAC_ACTION)
|
|
||||||
assert hvac_action is None
|
|
||||||
# Redefine actions according to https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action
|
# Redefine actions according to https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action
|
||||||
actions = ["off", "preheating", "heating", "cooling", "drying", "idle", "fan"]
|
actions = ["off", "preheating", "heating", "cooling", "drying", "idle", "fan"]
|
||||||
assert all(elem in actions for elem in HVACAction)
|
assert all(elem in actions for elem in HVACAction)
|
||||||
|
@ -1025,6 +1031,18 @@ async def test_handle_action_received(
|
||||||
hvac_action = state.attributes.get(ATTR_HVAC_ACTION)
|
hvac_action = state.attributes.get(ATTR_HVAC_ACTION)
|
||||||
assert hvac_action == action
|
assert hvac_action == action
|
||||||
|
|
||||||
|
# Check empty payload is ignored (last action == "fan")
|
||||||
|
async_fire_mqtt_message(hass, "action", "")
|
||||||
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
|
hvac_action = state.attributes.get(ATTR_HVAC_ACTION)
|
||||||
|
assert hvac_action == "fan"
|
||||||
|
|
||||||
|
# Check "None" payload is resetting the action
|
||||||
|
async_fire_mqtt_message(hass, "action", "None")
|
||||||
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
|
hvac_action = state.attributes.get(ATTR_HVAC_ACTION)
|
||||||
|
assert hvac_action is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
|
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
|
||||||
async def test_set_preset_mode_optimistic(
|
async def test_set_preset_mode_optimistic(
|
||||||
|
@ -1170,6 +1188,10 @@ async def test_set_preset_mode_pessimistic(
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
assert state.attributes.get("preset_mode") == "comfort"
|
assert state.attributes.get("preset_mode") == "comfort"
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "preset-mode-state", "")
|
||||||
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
|
assert state.attributes.get("preset_mode") == "comfort"
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, "preset-mode-state", "None")
|
async_fire_mqtt_message(hass, "preset-mode-state", "None")
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
assert state.attributes.get("preset_mode") == "none"
|
assert state.attributes.get("preset_mode") == "none"
|
||||||
|
@ -1449,11 +1471,16 @@ async def test_get_with_templates(
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
assert state.attributes.get("hvac_action") == "cooling"
|
assert state.attributes.get("hvac_action") == "cooling"
|
||||||
|
|
||||||
# Test ignoring null values
|
# Test ignoring empty values
|
||||||
async_fire_mqtt_message(hass, "action", "null")
|
async_fire_mqtt_message(hass, "action", "")
|
||||||
state = hass.states.get(ENTITY_CLIMATE)
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
assert state.attributes.get("hvac_action") == "cooling"
|
assert state.attributes.get("hvac_action") == "cooling"
|
||||||
|
|
||||||
|
# Test resetting with null values
|
||||||
|
async_fire_mqtt_message(hass, "action", "null")
|
||||||
|
state = hass.states.get(ENTITY_CLIMATE)
|
||||||
|
assert state.attributes.get("hvac_action") is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
|
|
|
@ -123,6 +123,11 @@ async def test_state_via_state_topic(
|
||||||
state = hass.states.get("cover.test")
|
state = hass.states.get("cover.test")
|
||||||
assert state.state == STATE_OPEN
|
assert state.state == STATE_OPEN
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "state-topic", "None")
|
||||||
|
|
||||||
|
state = hass.states.get("cover.test")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
|
|
|
@ -325,6 +325,11 @@ async def test_setting_device_tracker_value_via_mqtt_message(
|
||||||
state = hass.states.get("device_tracker.test")
|
state = hass.states.get("device_tracker.test")
|
||||||
assert state.state == STATE_NOT_HOME
|
assert state.state == STATE_NOT_HOME
|
||||||
|
|
||||||
|
# Test an empty value is ignored and the state is retained
|
||||||
|
async_fire_mqtt_message(hass, "test-topic", "")
|
||||||
|
state = hass.states.get("device_tracker.test")
|
||||||
|
assert state.state == STATE_NOT_HOME
|
||||||
|
|
||||||
|
|
||||||
async def test_setting_device_tracker_value_via_mqtt_message_and_template(
|
async def test_setting_device_tracker_value_via_mqtt_message_and_template(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
|
@ -148,6 +148,12 @@ async def test_controlling_non_default_state_via_topic(
|
||||||
state = hass.states.get("lock.test")
|
state = hass.states.get("lock.test")
|
||||||
assert state.state is lock_state
|
assert state.state is lock_state
|
||||||
|
|
||||||
|
# Empty state is ignored
|
||||||
|
async_fire_mqtt_message(hass, "state-topic", "")
|
||||||
|
|
||||||
|
state = hass.states.get("lock.test")
|
||||||
|
assert state.state is lock_state
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("hass_config", "payload", "lock_state"),
|
("hass_config", "payload", "lock_state"),
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
@ -91,11 +92,15 @@ def _test_run_select_setup_params(
|
||||||
async def test_run_select_setup(
|
async def test_run_select_setup(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
topic: str,
|
topic: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that it fetches the given payload."""
|
"""Test that it fetches the given payload."""
|
||||||
await mqtt_mock_entry()
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
|
state = hass.states.get("select.test_select")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, topic, "milk")
|
async_fire_mqtt_message(hass, topic, "milk")
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -110,6 +115,15 @@ async def test_run_select_setup(
|
||||||
state = hass.states.get("select.test_select")
|
state = hass.states.get("select.test_select")
|
||||||
assert state.state == "beer"
|
assert state.state == "beer"
|
||||||
|
|
||||||
|
if caplog.at_level(logging.DEBUG):
|
||||||
|
async_fire_mqtt_message(hass, topic, "")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Ignoring empty payload" in caplog.text
|
||||||
|
|
||||||
|
state = hass.states.get("select.test_select")
|
||||||
|
assert state.state == "beer"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
|
|
|
@ -131,6 +131,11 @@ async def test_state_via_state_topic_no_position(
|
||||||
state = hass.states.get("valve.test")
|
state = hass.states.get("valve.test")
|
||||||
assert state.state == asserted_state
|
assert state.state == asserted_state
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "state-topic", "None")
|
||||||
|
|
||||||
|
state = hass.states.get("valve.test")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
|
@ -197,6 +202,7 @@ async def test_state_via_state_topic_with_template(
|
||||||
('{"position":100}', STATE_OPEN),
|
('{"position":100}', STATE_OPEN),
|
||||||
('{"position":50.0}', STATE_OPEN),
|
('{"position":50.0}', STATE_OPEN),
|
||||||
('{"position":0}', STATE_CLOSED),
|
('{"position":0}', STATE_CLOSED),
|
||||||
|
('{"position":null}', STATE_UNKNOWN),
|
||||||
('{"position":"non_numeric"}', STATE_UNKNOWN),
|
('{"position":"non_numeric"}', STATE_UNKNOWN),
|
||||||
('{"ignored":12}', STATE_UNKNOWN),
|
('{"ignored":12}', STATE_UNKNOWN),
|
||||||
],
|
],
|
||||||
|
|
|
@ -25,7 +25,12 @@ from homeassistant.components.water_heater import (
|
||||||
STATE_PERFORMANCE,
|
STATE_PERFORMANCE,
|
||||||
WaterHeaterEntityFeature,
|
WaterHeaterEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, UnitOfTemperature
|
from homeassistant.const import (
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
UnitOfTemperature,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||||
|
|
||||||
|
@ -200,7 +205,7 @@ async def test_set_operation_pessimistic(
|
||||||
await mqtt_mock_entry()
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
state = hass.states.get(ENTITY_WATER_HEATER)
|
state = hass.states.get(ENTITY_WATER_HEATER)
|
||||||
assert state.state == "unknown"
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
await common.async_set_operation_mode(hass, "eco", ENTITY_WATER_HEATER)
|
await common.async_set_operation_mode(hass, "eco", ENTITY_WATER_HEATER)
|
||||||
state = hass.states.get(ENTITY_WATER_HEATER)
|
state = hass.states.get(ENTITY_WATER_HEATER)
|
||||||
|
@ -214,6 +219,16 @@ async def test_set_operation_pessimistic(
|
||||||
state = hass.states.get(ENTITY_WATER_HEATER)
|
state = hass.states.get(ENTITY_WATER_HEATER)
|
||||||
assert state.state == "eco"
|
assert state.state == "eco"
|
||||||
|
|
||||||
|
# Empty state ignored
|
||||||
|
async_fire_mqtt_message(hass, "mode-state", "")
|
||||||
|
state = hass.states.get(ENTITY_WATER_HEATER)
|
||||||
|
assert state.state == "eco"
|
||||||
|
|
||||||
|
# Test None payload
|
||||||
|
async_fire_mqtt_message(hass, "mode-state", "None")
|
||||||
|
state = hass.states.get(ENTITY_WATER_HEATER)
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue