Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions homeassistant/components/mqtt/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
CONF_RETAIN,
CONF_STATE_TOPIC,
CONF_SUPPORTED_FEATURES,
PAYLOAD_NONE,
)
from .mixins import MqttEntity, async_setup_entity_entry_helper
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
Expand Down Expand Up @@ -176,6 +177,16 @@ def _setup_from_config(self, config: ConfigType) -> None:
def _state_message_received(self, msg: ReceiveMessage) -> None:
"""Run when new MQTT message has been received."""
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 (
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_HOME,
Expand Down
11 changes: 8 additions & 3 deletions homeassistant/components/mqtt/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,13 +709,16 @@ def _setup_from_config(self, config: ConfigType) -> None:
def _handle_action_received(self, msg: ReceiveMessage) -> None:
"""Handle receiving action via MQTT."""
payload = self.render_template(msg, CONF_ACTION_TEMPLATE)
if not payload or payload == PAYLOAD_NONE:
if not payload:
_LOGGER.debug(
"Invalid %s action: %s, ignoring",
[e.value for e in HVACAction],
payload,
)
return
if payload == PAYLOAD_NONE:
self._attr_hvac_action = None
return
try:
self._attr_hvac_action = HVACAction(str(payload))
except ValueError:
Expand All @@ -733,8 +736,10 @@ def _handle_mode_received(
"""Handle receiving listed mode via MQTT."""
payload = self.render_template(msg, template_name)

if payload not in self._config[mode_list]:
_LOGGER.error("Invalid %s mode: %s", mode_list, payload)
if payload == PAYLOAD_NONE:
setattr(self, attr, None)
elif payload not in self._config[mode_list]:
_LOGGER.warning("Invalid %s mode: %s", mode_list, payload)
else:
setattr(self, attr, payload)

Expand Down
13 changes: 10 additions & 3 deletions homeassistant/components/mqtt/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
DEFAULT_POSITION_CLOSED,
DEFAULT_POSITION_OPEN,
DEFAULT_RETAIN,
PAYLOAD_NONE,
)
from .mixins import MqttEntity, async_setup_entity_entry_helper
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
Expand Down Expand Up @@ -350,9 +351,13 @@ def _setup_from_config(self, config: ConfigType) -> None:
self._attr_supported_features = supported_features

@callback
def _update_state(self, state: str) -> None:
def _update_state(self, state: str | None) -> None:
"""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_closing = state == STATE_CLOSING

Expand All @@ -376,7 +381,7 @@ def _state_message_received(self, msg: ReceiveMessage) -> None:
_LOGGER.debug("Ignoring empty state message from '%s'", msg.topic)
return

state: str
state: str | None
if payload == self._config[CONF_STATE_STOPPED]:
if self._config.get(CONF_GET_POSITION_TOPIC) is not None:
state = (
Expand All @@ -398,6 +403,8 @@ def _state_message_received(self, msg: ReceiveMessage) -> None:
state = STATE_OPEN
elif payload == self._config[CONF_STATE_CLOSED]:
state = STATE_CLOSED
elif payload == PAYLOAD_NONE:
state = None
else:
_LOGGER.warning(
(
Expand Down
10 changes: 10 additions & 0 deletions homeassistant/components/mqtt/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from collections.abc import Callable
import logging
from typing import TYPE_CHECKING

import voluptuous as vol
Expand Down Expand Up @@ -42,6 +43,8 @@
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_subscribe_topic

_LOGGER = logging.getLogger(__name__)

CONF_PAYLOAD_HOME = "payload_home"
CONF_PAYLOAD_NOT_HOME = "payload_not_home"
CONF_SOURCE_TYPE = "source_type"
Expand Down Expand Up @@ -125,6 +128,13 @@ def _prepare_subscribe_topics(self) -> None:
def message_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""
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]:
self._location_name = STATE_HOME
elif payload == self._config[CONF_PAYLOAD_NOT_HOME]:
Expand Down
15 changes: 12 additions & 3 deletions homeassistant/components/mqtt/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from collections.abc import Callable
import logging
import re
from typing import Any

Expand Down Expand Up @@ -50,6 +51,8 @@
)
from .schemas import MQTT_ENTITY_COMMON_SCHEMA

_LOGGER = logging.getLogger(__name__)

CONF_CODE_FORMAT = "code_format"

CONF_PAYLOAD_LOCK = "payload_lock"
Expand Down Expand Up @@ -205,9 +208,15 @@ def _prepare_subscribe_topics(self) -> None:
)
def message_received(msg: ReceiveMessage) -> None:
"""Handle new lock state messages."""
if (payload := self._value_template(msg.payload)) == self._config[
CONF_PAYLOAD_RESET
]:
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_RESET]:
# Reset the state to `unknown`
self._attr_is_locked = None
elif payload in self._valid_states:
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/mqtt/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ def _prepare_subscribe_topics(self) -> None:
def message_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""
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":
self._attr_current_option = None
return
Expand Down
15 changes: 12 additions & 3 deletions homeassistant/components/mqtt/valve.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
DEFAULT_POSITION_CLOSED,
DEFAULT_POSITION_OPEN,
DEFAULT_RETAIN,
PAYLOAD_NONE,
)
from .debug_info import log_messages
from .mixins import (
Expand Down Expand Up @@ -220,13 +221,16 @@ def _setup_from_config(self, config: ConfigType) -> None:
self._attr_supported_features = supported_features

@callback
def _update_state(self, state: str) -> None:
def _update_state(self, state: str | None) -> None:
"""Update the valve state properties."""
self._attr_is_opening = state == STATE_OPENING
self._attr_is_closing = state == STATE_CLOSING
if self.reports_position:
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
def _process_binary_valve_update(
Expand All @@ -242,7 +246,9 @@ def _process_binary_valve_update(
state = STATE_OPEN
elif state_payload == self._config[CONF_STATE_CLOSED]:
state = STATE_CLOSED
if state is None:
elif state_payload == PAYLOAD_NONE:
state = None
else:
_LOGGER.warning(
"Payload received on topic '%s' is not one of "
"[open, closed, opening, closing], got: %s",
Expand All @@ -263,6 +269,9 @@ def _process_position_valve_update(
state = STATE_OPENING
elif state_payload == self._config[CONF_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:
try:
percentage_payload = ranged_value_to_percentage(
Expand Down
17 changes: 15 additions & 2 deletions homeassistant/components/mqtt/water_heater.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
CONF_TEMP_STATE_TEMPLATE,
CONF_TEMP_STATE_TOPIC,
DEFAULT_OPTIMISTIC,
PAYLOAD_NONE,
)
from .mixins import async_setup_entity_entry_helper
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
Expand Down Expand Up @@ -259,10 +260,22 @@ def _setup_from_config(self, config: ConfigType) -> None:
@callback
def _handle_current_mode_received(self, msg: ReceiveMessage) -> None:
"""Handle receiving operation mode via MQTT."""

payload = self.render_template(msg, CONF_MODE_STATE_TEMPLATE)

if payload not in self._config[CONF_MODE_LIST]:
_LOGGER.error("Invalid %s mode: %s", CONF_MODE_LIST, payload)
if not payload.strip(): # No output from template, ignore
_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:
if TYPE_CHECKING:
assert isinstance(payload, str)
Expand Down
8 changes: 8 additions & 0 deletions tests/components/mqtt/test_alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ async def test_update_state_via_state_topic(
async_fire_mqtt_message(hass, "alarm/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])
async def test_ignore_update_state_if_unknown_via_state_topic(
Expand Down
47 changes: 37 additions & 10 deletions tests/components/mqtt/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
MQTT_CLIMATE_ATTRIBUTES_BLOCKED,
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.exceptions import ServiceValidationError

Expand Down Expand Up @@ -245,11 +245,11 @@ async def test_set_operation_pessimistic(
await mqtt_mock_entry()

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)
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "unknown"
assert state.state == STATE_UNKNOWN

async_fire_mqtt_message(hass, "mode-state", "cool")
state = hass.states.get(ENTITY_CLIMATE)
Expand All @@ -259,6 +259,16 @@ async def test_set_operation_pessimistic(
state = hass.states.get(ENTITY_CLIMATE)
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(
"hass_config",
Expand Down Expand Up @@ -1011,11 +1021,7 @@ async def test_handle_action_received(
"""Test getting the action received via MQTT."""
await mqtt_mock_entry()

# Cycle through valid modes and also check for wrong input such as "None" (str(None))
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
# Cycle through valid modes
# Redefine actions according to https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action
actions = ["off", "preheating", "heating", "cooling", "drying", "idle", "fan"]
assert all(elem in actions for elem in HVACAction)
Expand All @@ -1025,6 +1031,18 @@ async def test_handle_action_received(
hvac_action = state.attributes.get(ATTR_HVAC_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])
async def test_set_preset_mode_optimistic(
Expand Down Expand Up @@ -1170,6 +1188,10 @@ async def test_set_preset_mode_pessimistic(
state = hass.states.get(ENTITY_CLIMATE)
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")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
Expand Down Expand Up @@ -1449,11 +1471,16 @@ async def test_get_with_templates(
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("hvac_action") == "cooling"

# Test ignoring null values
async_fire_mqtt_message(hass, "action", "null")
# Test ignoring empty values
async_fire_mqtt_message(hass, "action", "")
state = hass.states.get(ENTITY_CLIMATE)
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(
"hass_config",
Expand Down
5 changes: 5 additions & 0 deletions tests/components/mqtt/test_cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ async def test_state_via_state_topic(
state = hass.states.get("cover.test")
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(
"hass_config",
Expand Down
Loading