diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e524502dd8d..e95729602cc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -126,6 +126,7 @@ PLATFORMS = [ "climate", "cover", "fan", + "humidifier", "light", "lock", "number", diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index a2bd7fc6b36..6bb7a92e8af 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -74,6 +74,10 @@ ABBREVIATIONS = { "hs_val_tpl": "hs_value_template", "ic": "icon", "init": "initial", + "hum_cmd_t": "target_humidity_command_topic", + "hum_cmd_tpl": "target_humidity_command_template", + "hum_stat_t": "target_humidity_state_topic", + "hum_state_tpl": "target_humidity_state_template", "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "json_attr_tpl": "json_attributes_template", @@ -81,14 +85,17 @@ ABBREVIATIONS = { "lrst_val_tpl": "last_reset_value_template", "max": "max", "min": "min", + "max_hum": "max_humidity", + "min_hum": "min_humidity", "max_mirs": "max_mireds", "min_mirs": "min_mireds", "max_temp": "max_temp", "min_temp": "min_temp", "mode_cmd_tpl": "mode_command_template", "mode_cmd_t": "mode_command_topic", - "mode_stat_tpl": "mode_state_template", "mode_stat_t": "mode_state_topic", + "mode_stat_tpl": "mode_state_template", + "modes": "modes", "name": "name", "off_dly": "off_delay", "on_cmd_type": "on_command_type", @@ -126,6 +133,8 @@ ABBREVIATIONS = { "pl_osc_off": "payload_oscillation_off", "pl_osc_on": "payload_oscillation_on", "pl_paus": "payload_pause", + "pl_rst_hum": "payload_reset_humidity", + "pl_rst_mode": "payload_reset_mode", "pl_rst_pct": "payload_reset_percentage", "pl_rst_pr_mode": "payload_reset_preset_mode", "pl_stop": "payload_stop", diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 84d85fba79c..0659baa9144 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -41,6 +41,7 @@ SUPPORTED_COMPONENTS = [ "device_automation", "device_tracker", "fan", + "humidifier", "light", "lock", "number", diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py new file mode 100644 index 00000000000..4e7e9ee5879 --- /dev/null +++ b/homeassistant/components/mqtt/humidifier.py @@ -0,0 +1,456 @@ +"""Support for MQTT humidifiers.""" +import functools +import logging + +import voluptuous as vol + +from homeassistant.components import humidifier +from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + ATTR_MODE, + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, + SUPPORT_MODES, + HumidifierEntity, +) +from homeassistant.const import ( + CONF_NAME, + CONF_OPTIMISTIC, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, + CONF_STATE, +) +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import ConfigType + +from . import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, + subscription, +) +from .. import mqtt +from .debug_info import log_messages +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper + +CONF_AVAILABLE_MODES_LIST = "modes" +CONF_COMMAND_TEMPLATE = "command_template" +CONF_DEVICE_CLASS = "device_class" +CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" +CONF_MODE_COMMAND_TOPIC = "mode_command_topic" +CONF_MODE_STATE_TOPIC = "mode_state_topic" +CONF_MODE_STATE_TEMPLATE = "mode_state_template" +CONF_PAYLOAD_RESET_MODE = "payload_reset_mode" +CONF_PAYLOAD_RESET_HUMIDITY = "payload_reset_humidity" +CONF_STATE_VALUE_TEMPLATE = "state_value_template" +CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" +CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_TARGET_HUMIDITY_MIN = "min_humidity" +CONF_TARGET_HUMIDITY_MAX = "max_humidity" +CONF_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" +CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" + +DEFAULT_NAME = "MQTT Humidifier" +DEFAULT_OPTIMISTIC = False +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_PAYLOAD_RESET = "None" + +MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED = frozenset( + { + humidifier.ATTR_HUMIDITY, + humidifier.ATTR_MAX_HUMIDITY, + humidifier.ATTR_MIN_HUMIDITY, + humidifier.ATTR_MODE, + humidifier.ATTR_AVAILABLE_MODES, + } +) + +_LOGGER = logging.getLogger(__name__) + + +def valid_mode_configuration(config): + """Validate that the mode reset payload is not one of the available modes.""" + if config.get(CONF_PAYLOAD_RESET_MODE) in config.get(CONF_AVAILABLE_MODES_LIST): + raise ValueError("modes must not contain payload_reset_mode") + return config + + +def valid_humidity_range_configuration(config): + """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + if config.get(CONF_TARGET_HUMIDITY_MIN) >= config.get(CONF_TARGET_HUMIDITY_MAX): + raise ValueError("target_humidity_max must be > target_humidity_min") + if config.get(CONF_TARGET_HUMIDITY_MAX) > 100: + raise ValueError("max_humidity must be <= 100") + + return config + + +PLATFORM_SCHEMA = vol.All( + mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + # CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together + vol.Inclusive( + CONF_AVAILABLE_MODES_LIST, "available_modes", default=[] + ): cv.ensure_list, + vol.Inclusive( + CONF_MODE_COMMAND_TOPIC, "available_modes" + ): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_HUMIDIFIER): vol.In( + [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] + ), + vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, + vol.Optional( + CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY + ): cv.positive_int, + vol.Optional( + CONF_TARGET_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY + ): cv.positive_int, + vol.Optional(CONF_TARGET_HUMIDITY_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + vol.Optional( + CONF_PAYLOAD_RESET_MODE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + } + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + valid_humidity_range_configuration, + valid_mode_configuration, +) + + +async def async_setup_platform( + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None +): + """Set up MQTT humidifier through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await _async_setup_entity(hass, async_add_entities, config) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT humidifier dynamically through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, humidifier.DOMAIN, setup, PLATFORM_SCHEMA) + + +async def _async_setup_entity( + hass, async_add_entities, config, config_entry=None, discovery_data=None +): + """Set up the MQTT humidifier.""" + async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)]) + + +class MqttHumidifier(MqttEntity, HumidifierEntity): + """A MQTT humidifier component.""" + + _attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED + + def __init__(self, hass, config, config_entry, discovery_data): + """Initialize the MQTT humidifier.""" + self._state = False + self._target_humidity = None + self._mode = None + self._supported_features = 0 + + self._topic = None + self._payload = None + self._value_templates = None + self._command_templates = None + self._optimistic = None + self._optimistic_target_humidity = None + self._optimistic_mode = None + + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_min_humidity = config.get(CONF_TARGET_HUMIDITY_MIN) + self._attr_max_humidity = config.get(CONF_TARGET_HUMIDITY_MAX) + + self._topic = { + key: config.get(key) + for key in ( + CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, + CONF_TARGET_HUMIDITY_STATE_TOPIC, + CONF_TARGET_HUMIDITY_COMMAND_TOPIC, + CONF_MODE_STATE_TOPIC, + CONF_MODE_COMMAND_TOPIC, + ) + } + self._value_templates = { + 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), + } + self._command_templates = { + CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), + ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE), + ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE), + } + self._payload = { + "STATE_ON": config[CONF_PAYLOAD_ON], + "STATE_OFF": config[CONF_PAYLOAD_OFF], + "HUMIDITY_RESET": config[CONF_PAYLOAD_RESET_HUMIDITY], + "MODE_RESET": config[CONF_PAYLOAD_RESET_MODE], + } + if CONF_MODE_COMMAND_TOPIC in config and CONF_AVAILABLE_MODES_LIST in config: + self._available_modes = config[CONF_AVAILABLE_MODES_LIST] + else: + self._available_modes = [] + if self._available_modes: + self._attr_supported_features = SUPPORT_MODES + else: + self._attr_supported_features = 0 + + optimistic = config[CONF_OPTIMISTIC] + self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None + self._optimistic_target_humidity = ( + optimistic or self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is None + ) + self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None + + for tpl_dict in [self._command_templates, self._value_templates]: + for key, tpl in tpl_dict.items(): + if tpl is None: + tpl_dict[key] = lambda value: value + else: + tpl.hass = self.hass + tpl_dict[key] = tpl.async_render_with_possible_json_value + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + topics = {} + + @callback + @log_messages(self.hass, self.entity_id) + def state_received(msg): + """Handle new received MQTT message.""" + payload = self._value_templates[CONF_STATE](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) + return + if payload == self._payload["STATE_ON"]: + self._state = True + elif payload == self._payload["STATE_OFF"]: + self._state = False + self.async_write_ha_state() + + 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], + } + + @callback + @log_messages(self.hass, self.entity_id) + def target_humidity_received(msg): + """Handle new received MQTT message for the target humidity.""" + rendered_target_humidity_payload = self._value_templates[ATTR_HUMIDITY]( + msg.payload + ) + if not rendered_target_humidity_payload: + _LOGGER.debug("Ignoring empty target humidity from '%s'", msg.topic) + return + if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: + self._target_humidity = None + self.async_write_ha_state() + return + try: + target_humidity = round(float(rendered_target_humidity_payload)) + except ValueError: + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid target humidity", + msg.payload, + msg.topic, + rendered_target_humidity_payload, + ) + return + if ( + target_humidity < self._attr_min_humidity + or target_humidity > self._attr_max_humidity + ): + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid target humidity", + msg.payload, + msg.topic, + rendered_target_humidity_payload, + ) + return + self._target_humidity = target_humidity + self.async_write_ha_state() + + 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], + } + self._target_humidity = None + + @callback + @log_messages(self.hass, self.entity_id) + def mode_received(msg): + """Handle new received MQTT message for mode.""" + mode = self._value_templates[ATTR_MODE](msg.payload) + if mode == self._payload["MODE_RESET"]: + self._mode = None + self.async_write_ha_state() + return + if not mode: + _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) + return + if mode not in self.available_modes: + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid mode", + msg.payload, + msg.topic, + mode, + ) + return + + self._mode = mode + self.async_write_ha_state() + + 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], + } + self._mode = None + + self._sub_state = await subscription.async_subscribe_topics( + self.hass, self._sub_state, topics + ) + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + + @property + def available_modes(self) -> list: + """Get the list of available modes.""" + return self._available_modes + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def target_humidity(self): + """Return the current target humidity.""" + return self._target_humidity + + @property + def mode(self): + """Return the current mode.""" + return self._mode + + async def async_turn_on( + self, + **kwargs, + ) -> None: + """Turn on the entity. + + This method is a coroutine. + """ + mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) + mqtt.async_publish( + self.hass, + self._topic[CONF_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + if self._optimistic: + self._state = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn off the entity. + + This method is a coroutine. + """ + mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) + mqtt.async_publish( + self.hass, + self._topic[CONF_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + if self._optimistic: + self._state = False + self.async_write_ha_state() + + async def async_set_humidity(self, humidity: int) -> None: + """Set the target humidity of the humidifier. + + This method is a coroutine. + """ + mqtt_payload = self._command_templates[ATTR_HUMIDITY](humidity) + mqtt.async_publish( + self.hass, + self._topic[CONF_TARGET_HUMIDITY_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + + if self._optimistic_target_humidity: + self._target_humidity = humidity + self.async_write_ha_state() + + async def async_set_mode(self, mode: str) -> None: + """Set the mode of the fan. + + This method is a coroutine. + """ + if mode not in self.available_modes: + _LOGGER.warning("'%s'is not a valid mode", mode) + return + + mqtt_payload = self._command_templates[ATTR_MODE](mode) + + mqtt.async_publish( + self.hass, + self._topic[CONF_MODE_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + + if self._optimistic_mode: + self._mode = mode + self.async_write_ha_state() diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py new file mode 100644 index 00000000000..4ae834be5da --- /dev/null +++ b/tests/components/mqtt/test_humidifier.py @@ -0,0 +1,1052 @@ +"""Test MQTT humidifiers.""" +from unittest.mock import patch + +import pytest +from voluptuous.error import MultipleInvalid + +from homeassistant.components import humidifier +from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + ATTR_MODE, + DOMAIN, + SERVICE_SET_HUMIDITY, + SERVICE_SET_MODE, +) +from homeassistant.components.mqtt.humidifier import MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message + +DEFAULT_CONFIG = { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + } +} + + +async def async_turn_on( + hass, + entity_id=ENTITY_MATCH_ALL, +) -> None: + """Turn all or specified humidifier on.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: + """Turn all or specified humidier off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + +async def async_set_mode(hass, entity_id=ENTITY_MATCH_ALL, mode: str = None) -> None: + """Set mode for all or specified humidifier.""" + data = { + key: value + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_MODE, mode)] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_SET_MODE, data, blocking=True) + + +async def async_set_humidity( + hass, entity_id=ENTITY_MATCH_ALL, humidity: int = None +) -> None: + """Set target humidity for all or specified humidifier.""" + data = { + key: value + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_HUMIDITY, humidity)] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) + + +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): + """Test if command fails with command topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + {humidifier.DOMAIN: {"platform": "mqtt", "name": "test"}}, + ) + await hass.async_block_till_done() + assert hass.states.get("humidifier.test") is None + + +async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "comfort", + "home", + "eco", + "sleep", + "baby", + ], + "payload_reset_humidity": "rEset_humidity", + "payload_reset_mode": "rEset_mode", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "StAtE_On") + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "state-topic", "StAtE_OfF") + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "humidity-state-topic", "0") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + + async_fire_mqtt_message(hass, "humidity-state-topic", "25") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 25 + + async_fire_mqtt_message(hass, "humidity-state-topic", "50") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 50 + + async_fire_mqtt_message(hass, "humidity-state-topic", "100") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + + async_fire_mqtt_message(hass, "humidity-state-topic", "101") + assert "not a valid target humidity" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "humidity-state-topic", "invalid") + assert "not a valid target humidity" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", "auto") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message(hass, "mode-state-topic", "eco") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + + async_fire_mqtt_message(hass, "mode-state-topic", "baby") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "baby" + + async_fire_mqtt_message(hass, "mode-state-topic", "ModeUnknown") + assert "not a valid mode" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", "rEset_mode") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) is None + + async_fire_mqtt_message(hass, "humidity-state-topic", "rEset_humidity") + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + + +async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): + """Test the controlling state via topic and JSON message.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.val }}", + "target_humidity_state_template": "{{ value_json.val }}", + "mode_state_template": "{{ value_json.val }}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + 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, "state-topic", '{"val":"OFF"}') + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"val": 1}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 1 + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"val": 100}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"val": "None"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + + async_fire_mqtt_message(hass, "humidity-state-topic", '{"otherval": 100}') + assert "Ignoring empty target humidity from" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "low"}') + assert "not a valid mode" in caplog.text + caplog.clear() + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "auto"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "eco"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "baby"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "baby" + + async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "None"}') + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) is None + + async_fire_mqtt_message(hass, "mode-state-topic", '{"otherval": 100}') + assert "Ignoring empty mode from" in caplog.text + caplog.clear() + + +async def test_controlling_state_via_topic_and_json_message_shared_topic( + hass, mqtt_mock, caplog +): + """Test the controlling state via topic and JSON message using a shared topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "shared-state-topic", + "target_humidity_command_topic": "percentage-command-topic", + "mode_state_topic": "shared-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.state }}", + "target_humidity_state_template": "{{ value_json.humidity }}", + "mode_state_template": "{{ value_json.mode }}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","mode":"eco","humidity": 50}', + ) + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 50 + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","mode":"auto","humidity": 10}', + ) + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 10 + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"OFF","mode":"auto","humidity": 0}', + ) + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"humidity": 100}', + ) + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + assert "Ignoring empty mode from" in caplog.text + assert "Ignoring empty state from" in caplog.text + caplog.clear() + + +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): + """Test optimistic mode without state topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "eco", + "auto", + "baby", + ], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "StAtE_On", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_off(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "StAtE_OfF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", -1) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", 101) + + await async_set_humidity(hass, "humidifier.test", 100) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "100", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 0) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + await async_set_mode(hass, "humidifier.test", "auto") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "auto", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "eco") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): + """Testing command templates with optimistic mode without state topic.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "command_template": "state: {{ value }}", + "target_humidity_command_topic": "humidity-command-topic", + "target_humidity_command_template": "humidity: {{ value }}", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "mode: {{ value }}", + "modes": [ + "auto", + "eco", + "sleep", + ], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "state: ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_off(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "state: OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", -1) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", 101) + + await async_set_humidity(hass, "humidifier.test", 100) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "humidity: 100", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 0) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "humidity: 0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 0 + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + await async_set_mode(hass, "humidifier.test", "eco") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "mode: eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "eco" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "auto") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "mode: auto", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.attributes.get(humidifier.ATTR_MODE) == "auto" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): + """Test optimistic mode with state topic and turn on attributes.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "mode_state_topic": "mode-state-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "optimistic": True, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with("command-topic", "ON", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_off(hass, "humidifier.test") + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 33) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "33", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 50) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "50", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 100) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "100", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_humidity(hass, "humidifier.test", 0) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-command-topic", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + with pytest.raises(MultipleInvalid): + await async_set_humidity(hass, "humidifier.test", 101) + + await async_set_mode(hass, "humidifier.test", "low") + assert "not a valid mode" in caplog.text + caplog.clear() + + await async_set_mode(hass, "humidifier.test", "eco") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "baby") + mqtt_mock.async_publish.assert_called_once_with( + "mode-command-topic", "baby", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_mode(hass, "humidifier.test", "freaking-high") + assert "not a valid mode" in caplog.text + caplog.clear() + + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_attributes(hass, mqtt_mock, caplog): + """Test attributes.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "mode_command_topic": "mode-command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "modes": [ + "eco", + "baby", + ], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(humidifier.ATTR_AVAILABLE_MODES) == [ + "eco", + "baby", + ] + assert state.attributes.get(humidifier.ATTR_MIN_HUMIDITY) == 0 + assert state.attributes.get(humidifier.ATTR_MAX_HUMIDITY) == 100 + + await async_turn_on(hass, "humidifier.test") + state = hass.states.get("humidifier.test") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + assert state.attributes.get(humidifier.ATTR_MODE) is None + + await async_turn_off(hass, "humidifier.test") + state = hass.states.get("humidifier.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + assert state.attributes.get(humidifier.ATTR_MODE) is None + + +async def test_invalid_configurations(hass, mqtt_mock, caplog): + """Test invalid configurations.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: [ + { + "platform": "mqtt", + "name": "test_valid_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + { + "platform": "mqtt", + "name": "test_valid_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "humidifier", + }, + { + "platform": "mqtt", + "name": "test_valid_3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "dehumidifier", + }, + { + "platform": "mqtt", + "name": "test_invalid_device_class", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "notsupporedSpeci@l", + }, + { + "platform": "mqtt", + "name": "test_mode_command_without_modes", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + }, + { + "platform": "mqtt", + "name": "test_invalid_humidity_min_max_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "min_humidity": 0, + "max_humidity": 101, + }, + { + "platform": "mqtt", + "name": "test_invalid_humidity_min_max_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "max_humidity": 20, + "min_humidity": 40, + }, + { + "platform": "mqtt", + "name": "test_invalid_mode_is_reset", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "None"], + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get("humidifier.test_valid_1") is not None + assert hass.states.get("humidifier.test_valid_2") is not None + assert hass.states.get("humidifier.test_valid_3") is not None + assert hass.states.get("humidifier.test_invalid_device_class") is None + assert hass.states.get("humidifier.test_mode_command_without_modes") is None + assert "not all values in the same group of inclusion" in caplog.text + caplog.clear() + + assert hass.states.get("humidifier.test_invalid_humidity_min_max_1") is None + assert hass.states.get("humidifier.test_invalid_humidity_min_max_2") is None + assert hass.states.get("humidifier.test_invalid_mode_is_reset") is None + + +async def test_supported_features(hass, mqtt_mock): + """Test supported features.""" + assert await async_setup_component( + hass, + humidifier.DOMAIN, + { + humidifier.DOMAIN: [ + { + "platform": "mqtt", + "name": "test1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + { + "platform": "mqtt", + "name": "test2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + { + "platform": "mqtt", + "name": "test3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + { + "platform": "mqtt", + "name": "test4", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + { + "platform": "mqtt", + "name": "test5", + "command_topic": "command-topic", + }, + { + "platform": "mqtt", + "name": "test6", + "target_humidity_command_topic": "humidity-command-topic", + }, + ] + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test1") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + + state = hass.states.get("humidifier.test2") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES + + state = hass.states.get("humidifier.test3") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + + state = hass.states.get("humidifier.test4") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES + + state = hass.states.get("humidifier.test5") + assert state is None + + state = hass.states.get("humidifier.test6") + assert state is None + + +async def test_availability_when_connection_lost(hass, mqtt_mock): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + ) + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, + mqtt_mock, + humidifier.DOMAIN, + DEFAULT_CONFIG, + MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique_id option only creates one fan per id.""" + config = { + humidifier.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, humidifier.DOMAIN, config) + + +async def test_discovery_removal_humidifier(hass, mqtt_mock, caplog): + """Test removal of discovered humidifier.""" + data = '{ "name": "test", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + await help_test_discovery_removal(hass, mqtt_mock, caplog, humidifier.DOMAIN, data) + + +async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): + """Test update of discovered humidifier.""" + data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + await help_test_discovery_update( + hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + ) + + +async def test_discovery_update_unchanged_humidifier(hass, mqtt_mock, caplog): + """Test update of discovered humidifier.""" + data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + with patch( + "homeassistant.components.mqtt.fan.MqttFan.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, discovery_update + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + await help_test_discovery_broken( + hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT fan device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT fan device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + )