Add action topic to MQTT humidifier (#95212)

* Add action topic to MQTT humidifier

* Add tests
This commit is contained in:
Jan Bouwhuis 2023-06-28 14:21:15 +02:00 committed by GitHub
parent e9495c9cc6
commit 0856121046
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 44 deletions

View file

@ -52,6 +52,8 @@ from homeassistant.util.unit_conversion import TemperatureConverter
from . import subscription
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
from .const import (
CONF_ACTION_TEMPLATE,
CONF_ACTION_TOPIC,
CONF_CURRENT_HUMIDITY_TEMPLATE,
CONF_CURRENT_HUMIDITY_TOPIC,
CONF_CURRENT_TEMP_TEMPLATE,
@ -90,8 +92,6 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT HVAC"
CONF_ACTION_TEMPLATE = "action_template"
CONF_ACTION_TOPIC = "action_topic"
CONF_AUX_COMMAND_TOPIC = "aux_command_topic"
CONF_AUX_STATE_TEMPLATE = "aux_state_template"
CONF_AUX_STATE_TOPIC = "aux_state_topic"

View file

@ -29,6 +29,8 @@ CONF_WS_HEADERS = "ws_headers"
CONF_WILL_MESSAGE = "will_message"
CONF_PAYLOAD_RESET = "payload_reset"
CONF_ACTION_TEMPLATE = "action_template"
CONF_ACTION_TOPIC = "action_topic"
CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"
CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"
CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template"

View file

@ -10,11 +10,13 @@ import voluptuous as vol
from homeassistant.components import humidifier
from homeassistant.components.humidifier import (
ATTR_ACTION,
ATTR_CURRENT_HUMIDITY,
ATTR_HUMIDITY,
ATTR_MODE,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
HumidifierAction,
HumidifierDeviceClass,
HumidifierEntity,
HumidifierEntityFeature,
@ -36,6 +38,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import subscription
from .config import MQTT_RW_SCHEMA
from .const import (
CONF_ACTION_TEMPLATE,
CONF_ACTION_TOPIC,
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_CURRENT_HUMIDITY_TEMPLATE,
@ -114,6 +118,8 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
{
vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic,
# CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together
vol.Inclusive(
CONF_AVAILABLE_MODES_LIST, "available_modes", default=[]
@ -163,6 +169,17 @@ DISCOVERY_SCHEMA = vol.All(
valid_mode_configuration,
)
TOPICS = (
CONF_ACTION_TOPIC,
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
CONF_CURRENT_HUMIDITY_TOPIC,
CONF_TARGET_HUMIDITY_STATE_TOPIC,
CONF_TARGET_HUMIDITY_COMMAND_TOPIC,
CONF_MODE_STATE_TOPIC,
CONF_MODE_COMMAND_TOPIC,
)
async def async_setup_entry(
hass: HomeAssistant,
@ -224,18 +241,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
self._attr_min_humidity = config[CONF_TARGET_HUMIDITY_MIN]
self._attr_max_humidity = config[CONF_TARGET_HUMIDITY_MAX]
self._topic = {
key: config.get(key)
for key in (
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
CONF_CURRENT_HUMIDITY_TOPIC,
CONF_TARGET_HUMIDITY_STATE_TOPIC,
CONF_TARGET_HUMIDITY_COMMAND_TOPIC,
CONF_MODE_STATE_TOPIC,
CONF_MODE_COMMAND_TOPIC,
)
}
self._topic = {key: config.get(key) for key in TOPICS}
self._payload = {
"STATE_ON": config[CONF_PAYLOAD_ON],
"STATE_OFF": config[CONF_PAYLOAD_OFF],
@ -248,6 +254,8 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
self._attr_available_modes = []
if self._attr_available_modes:
self._attr_supported_features = HumidifierEntityFeature.MODES
if CONF_MODE_STATE_TOPIC in config:
self._attr_mode = None
optimistic: bool = config[CONF_OPTIMISTIC]
self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None
@ -269,6 +277,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
self._value_templates = {}
value_templates: dict[str, Template | None] = {
ATTR_ACTION: config.get(CONF_ACTION_TEMPLATE),
ATTR_CURRENT_HUMIDITY: config.get(CONF_CURRENT_HUMIDITY_TEMPLATE),
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE),
@ -280,6 +289,22 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
entity=self,
).async_render_with_possible_json_value
def add_subscription(
self,
topics: dict[str, dict[str, Any]],
topic: str,
msg_callback: Callable[[ReceiveMessage], None],
) -> None:
"""Add a subscription."""
qos: int = self._config[CONF_QOS]
if topic in self._topic and self._topic[topic] is not None:
topics[topic] = {
"topic": self._topic[topic],
"msg_callback": msg_callback,
"qos": qos,
"encoding": self._config[CONF_ENCODING] or None,
}
def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
topics: dict[str, Any] = {}
@ -300,13 +325,29 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
self._attr_is_on = None
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if self._topic[CONF_STATE_TOPIC] is not None:
topics[CONF_STATE_TOPIC] = {
"topic": self._topic[CONF_STATE_TOPIC],
"msg_callback": state_received,
"qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None,
}
self.add_subscription(topics, CONF_STATE_TOPIC, state_received)
@callback
@log_messages(self.hass, self.entity_id)
def action_received(msg: ReceiveMessage) -> None:
"""Handle new received MQTT message."""
action_payload = self._value_templates[ATTR_ACTION](msg.payload)
if not action_payload or action_payload == PAYLOAD_NONE:
_LOGGER.debug("Ignoring empty action from '%s'", msg.topic)
return
try:
self._attr_action = HumidifierAction(str(action_payload))
except ValueError:
_LOGGER.error(
"'%s' received on topic %s. '%s' is not a valid action",
msg.payload,
msg.topic,
action_payload,
)
return
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
self.add_subscription(topics, CONF_ACTION_TOPIC, action_received)
@callback
@log_messages(self.hass, self.entity_id)
@ -343,13 +384,9 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
self._attr_current_humidity = current_humidity
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if self._topic[CONF_CURRENT_HUMIDITY_TOPIC] is not None:
topics[CONF_CURRENT_HUMIDITY_TOPIC] = {
"topic": self._topic[CONF_CURRENT_HUMIDITY_TOPIC],
"msg_callback": current_humidity_received,
"qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None,
}
self.add_subscription(
topics, CONF_CURRENT_HUMIDITY_TOPIC, current_humidity_received
)
@callback
@log_messages(self.hass, self.entity_id)
@ -389,14 +426,9 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
self._attr_target_humidity = target_humidity
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None:
topics[CONF_TARGET_HUMIDITY_STATE_TOPIC] = {
"topic": self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC],
"msg_callback": target_humidity_received,
"qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None,
}
self._attr_target_humidity = None
self.add_subscription(
topics, CONF_TARGET_HUMIDITY_STATE_TOPIC, target_humidity_received
)
@callback
@log_messages(self.hass, self.entity_id)
@ -422,14 +454,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
self._attr_mode = mode
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if self._topic[CONF_MODE_STATE_TOPIC] is not None:
topics[CONF_MODE_STATE_TOPIC] = {
"topic": self._topic[CONF_MODE_STATE_TOPIC],
"msg_callback": mode_received,
"qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None,
}
self._attr_mode = None
self.add_subscription(topics, CONF_MODE_STATE_TOPIC, mode_received)
self._sub_state = subscription.async_prepare_subscribe_topics(
self.hass, self._sub_state, topics

View file

@ -14,6 +14,7 @@ from homeassistant.components.humidifier import (
DOMAIN,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
HumidifierAction,
)
from homeassistant.components.mqtt.const import CONF_CURRENT_HUMIDITY_TOPIC
from homeassistant.components.mqtt.humidifier import (
@ -151,6 +152,7 @@ async def test_fail_setup_if_no_command_topic(
mqtt.DOMAIN: {
humidifier.DOMAIN: {
"name": "test",
"action_topic": "action-topic",
"state_topic": "state-topic",
"command_topic": "command-topic",
"current_humidity_topic": "current-humidity-topic",
@ -186,14 +188,17 @@ async def test_controlling_state_via_topic(
state = hass.states.get("humidifier.test")
assert state.state == STATE_UNKNOWN
assert not state.attributes.get(ATTR_ASSUMED_STATE)
assert not state.attributes.get(humidifier.ATTR_ACTION)
async_fire_mqtt_message(hass, "state-topic", "StAtE_On")
state = hass.states.get("humidifier.test")
assert state.state == STATE_ON
assert not state.attributes.get(humidifier.ATTR_ACTION)
async_fire_mqtt_message(hass, "state-topic", "StAtE_OfF")
state = hass.states.get("humidifier.test")
assert state.state == STATE_OFF
assert not state.attributes.get(humidifier.ATTR_ACTION)
async_fire_mqtt_message(hass, "humidity-state-topic", "0")
state = hass.states.get("humidifier.test")
@ -270,6 +275,34 @@ async def test_controlling_state_via_topic(
async_fire_mqtt_message(hass, "state-topic", "None")
state = hass.states.get("humidifier.test")
assert state.state == STATE_UNKNOWN
assert not state.attributes.get(humidifier.ATTR_ACTION)
# Turn un the humidifier
async_fire_mqtt_message(hass, "state-topic", "StAtE_On")
state = hass.states.get("humidifier.test")
assert state.state == STATE_ON
assert not state.attributes.get(humidifier.ATTR_ACTION)
async_fire_mqtt_message(hass, "action-topic", HumidifierAction.DRYING.value)
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.DRYING
async_fire_mqtt_message(hass, "action-topic", HumidifierAction.HUMIDIFYING.value)
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.HUMIDIFYING
async_fire_mqtt_message(hass, "action-topic", HumidifierAction.HUMIDIFYING.value)
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.HUMIDIFYING
async_fire_mqtt_message(hass, "action-topic", "invalid_action")
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.HUMIDIFYING
async_fire_mqtt_message(hass, "state-topic", "StAtE_OfF")
state = hass.states.get("humidifier.test")
assert state.state == STATE_OFF
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.OFF
@pytest.mark.parametrize(
@ -279,6 +312,7 @@ async def test_controlling_state_via_topic(
mqtt.DOMAIN: {
humidifier.DOMAIN: {
"name": "test",
"action_topic": "action-topic",
"state_topic": "state-topic",
"command_topic": "command-topic",
"current_humidity_topic": "current-humidity-topic",
@ -292,6 +326,7 @@ async def test_controlling_state_via_topic(
"baby",
],
"current_humidity_template": "{{ value_json.val }}",
"action_template": "{{ value_json.val }}",
"state_value_template": "{{ value_json.val }}",
"target_humidity_state_template": "{{ value_json.val }}",
"mode_state_template": "{{ value_json.val }}",
@ -381,6 +416,35 @@ async def test_controlling_state_via_topic_and_json_message(
state = hass.states.get("humidifier.test")
assert state.state == STATE_UNKNOWN
# Make sure the humidifier is ON
async_fire_mqtt_message(hass, "state-topic", '{"val":"ON"}')
state = hass.states.get("humidifier.test")
assert state.state == STATE_ON
async_fire_mqtt_message(hass, "action-topic", '{"val": "drying"}')
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.DRYING
async_fire_mqtt_message(hass, "action-topic", '{"val": "humidifying"}')
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.HUMIDIFYING
async_fire_mqtt_message(hass, "action-topic", '{"val": null}')
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.HUMIDIFYING
async_fire_mqtt_message(hass, "action-topic", '{"otherval": "idle"}')
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.HUMIDIFYING
async_fire_mqtt_message(hass, "action-topic", '{"val": "idle"}')
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.IDLE
async_fire_mqtt_message(hass, "action-topic", '{"val": "off"}')
state = hass.states.get("humidifier.test")
assert state.attributes.get(humidifier.ATTR_ACTION) == HumidifierAction.OFF
@pytest.mark.parametrize(
"hass_config",