Add action topic to MQTT humidifier (#95212)
* Add action topic to MQTT humidifier * Add tests
This commit is contained in:
parent
e9495c9cc6
commit
0856121046
4 changed files with 135 additions and 44 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue