diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 755df281736..98fd344a30c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -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_CURRENT_HUMIDITY_TEMPLATE, + CONF_CURRENT_HUMIDITY_TOPIC, CONF_CURRENT_TEMP_TEMPLATE, CONF_CURRENT_TEMP_TOPIC, CONF_ENCODING, @@ -94,8 +96,6 @@ CONF_AUX_COMMAND_TOPIC = "aux_command_topic" CONF_AUX_STATE_TEMPLATE = "aux_state_template" CONF_AUX_STATE_TOPIC = "aux_state_topic" -CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template" -CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic" CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" CONF_FAN_MODE_LIST = "fan_modes" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index a8d7812965c..fd259965d20 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -29,6 +29,8 @@ CONF_WS_HEADERS = "ws_headers" CONF_WILL_MESSAGE = "will_message" CONF_PAYLOAD_RESET = "payload_reset" +CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template" +CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic" CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index f00944fc091..624bf0c698b 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import humidifier from homeassistant.components.humidifier import ( + ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY, ATTR_MODE, DEFAULT_MAX_HUMIDITY, @@ -37,6 +38,8 @@ from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_CURRENT_HUMIDITY_TEMPLATE, + CONF_CURRENT_HUMIDITY_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, @@ -117,6 +120,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( ): cv.ensure_list, vol.Inclusive(CONF_MODE_COMMAND_TOPIC, "available_modes"): valid_publish_topic, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_HUMIDITY_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_HUMIDITY_TOPIC): valid_subscribe_topic, vol.Optional( CONF_DEVICE_CLASS, default=HumidifierDeviceClass.HUMIDIFIER ): vol.In( @@ -224,6 +229,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): 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, @@ -263,6 +269,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._value_templates = {} value_templates: dict[str, Template | None] = { + 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), ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE), @@ -301,6 +308,49 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): "encoding": self._config[CONF_ENCODING] or None, } + @callback + @log_messages(self.hass, self.entity_id) + def current_humidity_received(msg: ReceiveMessage) -> None: + """Handle new received MQTT message for the current humidity.""" + rendered_current_humidity_payload = self._value_templates[ + ATTR_CURRENT_HUMIDITY + ](msg.payload) + if rendered_current_humidity_payload == self._payload["HUMIDITY_RESET"]: + self._attr_current_humidity = None + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + return + if not rendered_current_humidity_payload: + _LOGGER.debug("Ignoring empty current humidity from '%s'", msg.topic) + return + try: + current_humidity = round(float(rendered_current_humidity_payload)) + except ValueError: + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid humidity", + msg.payload, + msg.topic, + rendered_current_humidity_payload, + ) + return + if current_humidity < 0 or current_humidity > 100: + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid humidity", + msg.payload, + msg.topic, + rendered_current_humidity_payload, + ) + return + 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, + } + @callback @log_messages(self.hass, self.entity_id) def target_humidity_received(msg: ReceiveMessage) -> None: diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 08050aec8a0..fecf9c33fc0 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -8,12 +8,14 @@ from voluptuous.error import MultipleInvalid from homeassistant.components import humidifier, mqtt from homeassistant.components.humidifier import ( + ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY, ATTR_MODE, DOMAIN, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, ) +from homeassistant.components.mqtt.const import CONF_CURRENT_HUMIDITY_TOPIC from homeassistant.components.mqtt.humidifier import ( CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, @@ -151,6 +153,7 @@ async def test_fail_setup_if_no_command_topic( "name": "test", "state_topic": "state-topic", "command_topic": "command-topic", + "current_humidity_topic": "current-humidity-topic", "payload_off": "StAtE_OfF", "payload_on": "StAtE_On", "target_humidity_state_topic": "humidity-state-topic", @@ -220,6 +223,26 @@ async def test_controlling_state_via_topic( assert "not a valid mode" in caplog.text caplog.clear() + async_fire_mqtt_message(hass, "current-humidity-topic", "48") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) == 48 + + async_fire_mqtt_message(hass, "current-humidity-topic", "101") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) == 48 + + async_fire_mqtt_message(hass, "current-humidity-topic", "-1.6") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) == 48 + + async_fire_mqtt_message(hass, "current-humidity-topic", "43.6") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) == 44 + + async_fire_mqtt_message(hass, "current-humidity-topic", "invalid") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) == 44 + async_fire_mqtt_message(hass, "mode-state-topic", "auto") state = hass.states.get("humidifier.test") assert state.attributes.get(humidifier.ATTR_MODE) == "auto" @@ -258,6 +281,7 @@ async def test_controlling_state_via_topic( "name": "test", "state_topic": "state-topic", "command_topic": "command-topic", + "current_humidity_topic": "current-humidity-topic", "target_humidity_state_topic": "humidity-state-topic", "target_humidity_command_topic": "humidity-command-topic", "mode_state_topic": "mode-state-topic", @@ -267,6 +291,7 @@ async def test_controlling_state_via_topic( "eco", "baby", ], + "current_humidity_template": "{{ value_json.val }}", "state_value_template": "{{ value_json.val }}", "target_humidity_state_template": "{{ value_json.val }}", "mode_state_template": "{{ value_json.val }}", @@ -312,6 +337,22 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None caplog.clear() + async_fire_mqtt_message(hass, "current-humidity-topic", '{"val": 1}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) == 1 + + async_fire_mqtt_message(hass, "current-humidity-topic", '{"val": 100}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) == 100 + + async_fire_mqtt_message(hass, "current-humidity-topic", '{"val": "None"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) is None + + async_fire_mqtt_message(hass, "current-humidity-topic", '{"otherval": 100}') + assert state.attributes.get(humidifier.ATTR_CURRENT_HUMIDITY) is None + caplog.clear() + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "low"}') assert "not a valid mode" in caplog.text caplog.clear() @@ -746,6 +787,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( ("state_topic", "ON", None, "on"), (CONF_MODE_STATE_TOPIC, "auto", ATTR_MODE, "auto"), (CONF_TARGET_HUMIDITY_STATE_TOPIC, "45", ATTR_HUMIDITY, 45), + (CONF_CURRENT_HUMIDITY_TOPIC, "39", ATTR_CURRENT_HUMIDITY, 39), ], ) async def test_encoding_subscribable_topics(