Add MQTT climate setting for current humidity (#84592)

* MQTT Climate: Add support for setting the current humidity via MQTT

* MQTT Climate: Add configuration constants related to setting the target humidity

* MQTT Climate: Add support for setting the humidity's state topic & template

* MQTT Climate: Add support for setting the initial humidity

* MQTT Climate: Add support for setting the humidity's command topic & template

* MQTT Climate: Add support for setting the min/max humidity

* MQTT Climate: Fix style & tests

* MQTT Climate: Set the initial humidity to None

* MQTT Climate: Rename _set_mqtt_attribute to _set_climate_attribute and handle_temperature_received to handle_climate_attribute_received

* MQTT Climate: Copy humidity range validation from MQTT Humidifier

* MQTT Climate: Remove CONF_HUMIDITY_INITIAL

* MQTT Climate: Only enable support for TARGET_HUMIDITY when the command topic is set

* MQTT Climate: Check if setting the target humidity is supported before actually setting it

* MQTT Climate: Make sure that CONF_HUMIDITY_COMMAND_TOPIC has been configured when setting CONF_HUMIDITY_STATE_TOPIC

* MQTT Climate: Fix broken tests

* MQTT Climate: Add test for optimistically setting the target humidity

* MQTT Climate: Remove references to "temperature" in handle_climate_attribute_received

* MQTT Climate: Add additional humidity-related tests

* MQTT Climate: Remove supported feature check in handle_target_humidity_received

It's not needed because this is covered by the `valid_humidity_state_configuration` validation.

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* MQTT Climate: Remove supported feature check in async_set_humidity

It is covered by the base Climate entity.

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
Mike K 2023-01-03 23:57:20 +02:00 committed by GitHub
parent 7fdf00a9fb
commit 799d527fb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 344 additions and 13 deletions

View File

@ -43,6 +43,8 @@ ABBREVIATIONS = {
"cod_arm_req": "code_arm_required",
"cod_dis_req": "code_disarm_required",
"cod_trig_req": "code_trigger_required",
"curr_hum_t": "current_humidity_topic",
"curr_hum_tpl": "current_humidity_template",
"curr_temp_t": "current_temperature_topic",
"curr_temp_tpl": "current_temperature_template",
"dev": "device",

View File

@ -13,7 +13,9 @@ from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
FAN_AUTO,
FAN_HIGH,
@ -85,6 +87,8 @@ CONF_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic"
CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template"
CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic"
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_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template"
@ -99,6 +103,12 @@ CONF_HOLD_STATE_TEMPLATE = "hold_state_template"
CONF_HOLD_STATE_TOPIC = "hold_state_topic"
CONF_HOLD_LIST = "hold_modes"
CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template"
CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"
CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template"
CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"
CONF_HUMIDITY_MAX = "max_humidity"
CONF_HUMIDITY_MIN = "min_humidity"
CONF_MODE_COMMAND_TEMPLATE = "mode_command_template"
CONF_MODE_COMMAND_TOPIC = "mode_command_topic"
CONF_MODE_LIST = "modes"
@ -164,8 +174,10 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset(
VALUE_TEMPLATE_KEYS = (
CONF_AUX_STATE_TEMPLATE,
CONF_CURRENT_HUMIDITY_TEMPLATE,
CONF_CURRENT_TEMP_TEMPLATE,
CONF_FAN_MODE_STATE_TEMPLATE,
CONF_HUMIDITY_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE,
CONF_POWER_STATE_TEMPLATE,
CONF_ACTION_TEMPLATE,
@ -178,6 +190,7 @@ VALUE_TEMPLATE_KEYS = (
COMMAND_TEMPLATE_KEYS = {
CONF_FAN_MODE_COMMAND_TEMPLATE,
CONF_HUMIDITY_COMMAND_TEMPLATE,
CONF_MODE_COMMAND_TEMPLATE,
CONF_PRESET_MODE_COMMAND_TEMPLATE,
CONF_SWING_MODE_COMMAND_TEMPLATE,
@ -191,9 +204,12 @@ TOPIC_KEYS = (
CONF_ACTION_TOPIC,
CONF_AUX_COMMAND_TOPIC,
CONF_AUX_STATE_TOPIC,
CONF_CURRENT_HUMIDITY_TOPIC,
CONF_CURRENT_TEMP_TOPIC,
CONF_FAN_MODE_COMMAND_TOPIC,
CONF_FAN_MODE_STATE_TOPIC,
CONF_HUMIDITY_COMMAND_TOPIC,
CONF_HUMIDITY_STATE_TOPIC,
CONF_MODE_COMMAND_TOPIC,
CONF_MODE_STATE_TOPIC,
CONF_POWER_COMMAND_TOPIC,
@ -218,11 +234,36 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
return config
def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
"""Validate that the target_humidity range configuration is valid, throws if it isn't."""
if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]:
raise ValueError("target_humidity_max must be > target_humidity_min")
if config[CONF_HUMIDITY_MAX] > 100:
raise ValueError("max_humidity must be <= 100")
return config
def valid_humidity_state_configuration(config: ConfigType) -> ConfigType:
"""Validate that if CONF_HUMIDITY_STATE_TOPIC is set then CONF_HUMIDITY_COMMAND_TOPIC is also set."""
if (
CONF_HUMIDITY_STATE_TOPIC in config
and CONF_HUMIDITY_COMMAND_TOPIC not in config
):
raise ValueError(
f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without {CONF_HUMIDITY_COMMAND_TOPIC}"
)
return config
_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
{
vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_CURRENT_HUMIDITY_TEMPLATE): cv.template,
vol.Optional(CONF_CURRENT_HUMIDITY_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template,
vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template,
@ -233,6 +274,16 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
): cv.ensure_list,
vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_HUMIDITY_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_HUMIDITY_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY): vol.Coerce(
float
),
vol.Optional(CONF_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY): vol.Coerce(
float
),
vol.Optional(CONF_HUMIDITY_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_HUMIDITY_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(
@ -313,6 +364,8 @@ PLATFORM_SCHEMA_MODERN = vol.All(
cv.removed(CONF_HOLD_LIST),
_PLATFORM_SCHEMA_BASE,
valid_preset_mode_configuration,
valid_humidity_range_configuration,
valid_humidity_state_configuration,
)
# Configuring MQTT Climate under the climate platform key was deprecated in HA Core 2022.6
@ -337,6 +390,8 @@ DISCOVERY_SCHEMA = vol.All(
cv.removed(CONF_HOLD_STATE_TOPIC),
cv.removed(CONF_HOLD_LIST),
valid_preset_mode_configuration,
valid_humidity_range_configuration,
valid_humidity_state_configuration,
)
@ -396,6 +451,8 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._attr_hvac_modes = config[CONF_MODE_LIST]
self._attr_min_temp = config[CONF_TEMP_MIN]
self._attr_max_temp = config[CONF_TEMP_MAX]
self._attr_min_humidity = config[CONF_HUMIDITY_MIN]
self._attr_max_humidity = config[CONF_HUMIDITY_MAX]
self._attr_precision = config.get(CONF_PRECISION, super().precision)
self._attr_fan_modes = config[CONF_FAN_MODE_LIST]
self._attr_swing_modes = config[CONF_SWING_MODE_LIST]
@ -485,6 +542,9 @@ class MqttClimate(MqttEntity, ClimateEntity):
):
support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
if self._topic[CONF_HUMIDITY_COMMAND_TOPIC] is not None:
support |= ClimateEntityFeature.TARGET_HUMIDITY
if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or (
self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None
):
@ -554,23 +614,23 @@ class MqttClimate(MqttEntity, ClimateEntity):
add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received)
@callback
def handle_temperature_received(
def handle_climate_attribute_received(
msg: ReceiveMessage, template_name: str, attr: str
) -> None:
"""Handle temperature coming via MQTT."""
"""Handle climate attributes coming via MQTT."""
payload = render_template(msg, template_name)
try:
setattr(self, attr, float(payload))
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
except ValueError:
_LOGGER.error("Could not parse temperature from %s", payload)
_LOGGER.error("Could not parse %s from %s", template_name, payload)
@callback
@log_messages(self.hass, self.entity_id)
def handle_current_temperature_received(msg: ReceiveMessage) -> None:
"""Handle current temperature coming via MQTT."""
handle_temperature_received(
handle_climate_attribute_received(
msg, CONF_CURRENT_TEMP_TEMPLATE, "_attr_current_temperature"
)
@ -582,7 +642,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
@log_messages(self.hass, self.entity_id)
def handle_target_temperature_received(msg: ReceiveMessage) -> None:
"""Handle target temperature coming via MQTT."""
handle_temperature_received(
handle_climate_attribute_received(
msg, CONF_TEMP_STATE_TEMPLATE, "_attr_target_temperature"
)
@ -594,7 +654,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
@log_messages(self.hass, self.entity_id)
def handle_temperature_low_received(msg: ReceiveMessage) -> None:
"""Handle target temperature low coming via MQTT."""
handle_temperature_received(
handle_climate_attribute_received(
msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_attr_target_temperature_low"
)
@ -606,7 +666,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
@log_messages(self.hass, self.entity_id)
def handle_temperature_high_received(msg: ReceiveMessage) -> None:
"""Handle target temperature high coming via MQTT."""
handle_temperature_received(
handle_climate_attribute_received(
msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_attr_target_temperature_high"
)
@ -614,6 +674,31 @@ class MqttClimate(MqttEntity, ClimateEntity):
topics, CONF_TEMP_HIGH_STATE_TOPIC, handle_temperature_high_received
)
@callback
@log_messages(self.hass, self.entity_id)
def handle_current_humidity_received(msg: ReceiveMessage) -> None:
"""Handle current humidity coming via MQTT."""
handle_climate_attribute_received(
msg, CONF_CURRENT_HUMIDITY_TEMPLATE, "_attr_current_humidity"
)
add_subscription(
topics, CONF_CURRENT_HUMIDITY_TOPIC, handle_current_humidity_received
)
@callback
@log_messages(self.hass, self.entity_id)
def handle_target_humidity_received(msg: ReceiveMessage) -> None:
"""Handle target humidity coming via MQTT."""
handle_climate_attribute_received(
msg, CONF_HUMIDITY_STATE_TEMPLATE, "_attr_target_humidity"
)
add_subscription(
topics, CONF_HUMIDITY_STATE_TOPIC, handle_target_humidity_received
)
@callback
def handle_mode_received(
msg: ReceiveMessage, template_name: str, attr: str, mode_list: str
@ -744,7 +829,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._config[CONF_ENCODING],
)
async def _set_temperature(
async def _set_climate_attribute(
self,
temp: float | None,
cmnd_topic: str,
@ -770,7 +855,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None:
await self.async_set_hvac_mode(operation_mode)
changed = await self._set_temperature(
changed = await self._set_climate_attribute(
kwargs.get(ATTR_TEMPERATURE),
CONF_TEMP_COMMAND_TOPIC,
CONF_TEMP_COMMAND_TEMPLATE,
@ -778,7 +863,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
"_attr_target_temperature",
)
changed |= await self._set_temperature(
changed |= await self._set_climate_attribute(
kwargs.get(ATTR_TARGET_TEMP_LOW),
CONF_TEMP_LOW_COMMAND_TOPIC,
CONF_TEMP_LOW_COMMAND_TEMPLATE,
@ -786,7 +871,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
"_attr_target_temperature_low",
)
changed |= await self._set_temperature(
changed |= await self._set_climate_attribute(
kwargs.get(ATTR_TARGET_TEMP_HIGH),
CONF_TEMP_HIGH_COMMAND_TOPIC,
CONF_TEMP_HIGH_COMMAND_TEMPLATE,
@ -798,6 +883,19 @@ class MqttClimate(MqttEntity, ClimateEntity):
return
self.async_write_ha_state()
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self._set_climate_attribute(
humidity,
CONF_HUMIDITY_COMMAND_TOPIC,
CONF_HUMIDITY_COMMAND_TEMPLATE,
CONF_HUMIDITY_STATE_TOPIC,
"_attr_target_humidity",
)
self.async_write_ha_state()
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new swing mode."""
payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode)

View File

@ -9,13 +9,17 @@ import voluptuous as vol
from homeassistant.components import climate, mqtt
from homeassistant.components.climate import (
ATTR_AUX_HEAT,
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
PRESET_ECO,
ClimateEntityFeature,
@ -67,6 +71,7 @@ DEFAULT_CONFIG = {
climate.DOMAIN: {
"name": "test",
"mode_command_topic": "mode-topic",
"target_humidity_command_topic": "humidity-topic",
"temperature_command_topic": "temperature-topic",
"temperature_low_command_topic": "temperature-low-topic",
"temperature_high_command_topic": "temperature-high-topic",
@ -108,6 +113,8 @@ async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config):
assert state.state == "off"
assert state.attributes.get("min_temp") == DEFAULT_MIN_TEMP
assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP
assert state.attributes.get("min_humidity") == DEFAULT_MIN_HUMIDITY
assert state.attributes.get("max_humidity") == DEFAULT_MAX_HUMIDITY
async def test_preset_none_in_preset_modes(hass, caplog):
@ -156,6 +163,7 @@ async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config):
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.AUX_HEAT
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.TARGET_HUMIDITY
)
assert state.attributes.get("supported_features") == support
@ -489,6 +497,21 @@ async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config):
mqtt_mock.async_publish.reset_mock()
async def test_set_target_humidity(hass, mqtt_mock_entry_with_yaml_config):
"""Test setting the target humidity."""
assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG)
await hass.async_block_till_done()
mqtt_mock = await mqtt_mock_entry_with_yaml_config()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") is None
await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 82
mqtt_mock.async_publish.assert_called_once_with("humidity-topic", "82", 0, False)
mqtt_mock.async_publish.reset_mock()
async def test_set_target_temperature_pessimistic(
hass, mqtt_mock_entry_with_yaml_config
):
@ -639,6 +662,53 @@ async def test_set_target_temperature_low_high_optimistic(
assert state.attributes.get("target_temp_high") == 25
async def test_set_target_humidity_optimistic(hass, mqtt_mock_entry_with_yaml_config):
"""Test setting the target humidity optimistic."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
config["climate"]["target_humidity_state_topic"] = "humidity-state"
config["climate"]["optimistic"] = True
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") is None
await common.async_set_humidity(hass, humidity=52, entity_id=ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 52
async_fire_mqtt_message(hass, "humidity-state", "53")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 53
async_fire_mqtt_message(hass, "humidity-state", "not a number")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 53
async def test_set_target_humidity_pessimistic(hass, mqtt_mock_entry_with_yaml_config):
"""Test setting the target humidity."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
config["climate"]["target_humidity_state_topic"] = "humidity-state"
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") is None
await common.async_set_humidity(hass, humidity=50, entity_id=ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") is None
async_fire_mqtt_message(hass, "humidity-state", "80")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 80
async_fire_mqtt_message(hass, "humidity-state", "not a number")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 80
async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config):
"""Test getting the current temperature via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
@ -652,6 +722,36 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config):
assert state.attributes.get("current_temperature") == 47
async def test_receive_mqtt_humidity(hass, mqtt_mock_entry_with_yaml_config):
"""Test getting the current humidity via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
config["climate"]["current_humidity_topic"] = "current_humidity"
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
async_fire_mqtt_message(hass, "current_humidity", "35")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("current_humidity") == 35
async def test_handle_target_humidity_received(hass, mqtt_mock_entry_with_yaml_config):
"""Test setting the target humidity via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
config["climate"]["target_humidity_state_topic"] = "humidity-state"
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") is None
async_fire_mqtt_message(hass, "humidity-state", "65")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 65
async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config):
"""Test getting the action received via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
@ -929,7 +1029,8 @@ async def test_get_target_temperature_low_high_with_templates(
async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"')
state = hass.states.get(ENTITY_CLIMATE)
# make sure, the invalid value gets logged...
assert "Could not parse temperature from" in caplog.text
assert "Could not parse temperature_low_state_template from" in caplog.text
assert "Could not parse temperature_high_state_template from" in caplog.text
# ... but the actual value stays unchanged.
assert state.attributes.get("target_temp_low") == 1031
assert state.attributes.get("target_temp_high") == 1032
@ -951,8 +1052,10 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog
config["climate"]["fan_mode_state_topic"] = "fan-state"
config["climate"]["swing_mode_state_topic"] = "swing-state"
config["climate"]["temperature_state_topic"] = "temperature-state"
config["climate"]["target_humidity_state_topic"] = "humidity-state"
config["climate"]["aux_state_topic"] = "aux-state"
config["climate"]["current_temperature_topic"] = "current-temperature"
config["climate"]["current_humidity_topic"] = "current-humidity"
config["climate"]["preset_mode_state_topic"] = "current-preset-mode"
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
@ -986,10 +1089,26 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog
async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"')
state = hass.states.get(ENTITY_CLIMATE)
# make sure, the invalid value gets logged...
assert "Could not parse temperature from -INVALID-" in caplog.text
assert "Could not parse temperature_state_template from -INVALID-" in caplog.text
# ... but the actual value stays unchanged.
assert state.attributes.get("temperature") == 1031
# Humidity - with valid value
assert state.attributes.get("humidity") is None
async_fire_mqtt_message(hass, "humidity-state", '"82"')
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 82
# Humidity - with invalid value
async_fire_mqtt_message(hass, "humidity-state", '"-INVALID-"')
state = hass.states.get(ENTITY_CLIMATE)
# make sure, the invalid value gets logged...
assert (
"Could not parse target_humidity_state_template from -INVALID-" in caplog.text
)
# ... but the actual value stays unchanged.
assert state.attributes.get("humidity") == 82
# Preset Mode
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "current-preset-mode", '{"attribute": "eco"}')
@ -1018,6 +1137,11 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("current_temperature") == 74656
# Current humidity
async_fire_mqtt_message(hass, "current-humidity", '"35"')
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("current_humidity") == 35
# Action
async_fire_mqtt_message(hass, "action", '"cooling"')
state = hass.states.get(ENTITY_CLIMATE)
@ -1040,6 +1164,7 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog)
config["climate"]["temperature_command_template"] = "temp: {{ value }}"
config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}"
config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}"
config["climate"]["target_humidity_command_template"] = "humidity: {{ value }}"
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
@ -1106,6 +1231,15 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog)
assert state.attributes.get("target_temp_low") == 20
assert state.attributes.get("target_temp_high") == 23
# Humidity
await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"humidity-topic", "humidity: 82", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("humidity") == 82
async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config):
"""Test a custom min temp."""
@ -1139,6 +1273,38 @@ async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config):
assert max_temp == 60
async def test_min_humidity_custom(hass, mqtt_mock_entry_with_yaml_config):
"""Test a custom min humidity."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
config["climate"]["min_humidity"] = 42
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
state = hass.states.get(ENTITY_CLIMATE)
min_humidity = state.attributes.get("min_humidity")
assert isinstance(min_humidity, float)
assert state.attributes.get("min_humidity") == 42
async def test_max_humidity_custom(hass, mqtt_mock_entry_with_yaml_config):
"""Test a custom max humidity."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
config["climate"]["max_humidity"] = 58
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
state = hass.states.get(ENTITY_CLIMATE)
max_humidity = state.attributes.get("max_humidity")
assert isinstance(max_humidity, float)
assert max_humidity == 58
async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config):
"""Test a custom temp step."""
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
@ -1269,6 +1435,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config):
("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"),
("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"),
("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1),
("current_humidity_topic", "60.4", ATTR_CURRENT_HUMIDITY, 60.4),
("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"),
("mode_state_topic", "cool", None, None),
("mode_state_topic", "fan_only", None, None),
@ -1276,6 +1443,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config):
("temperature_low_state_topic", "19.1", ATTR_TARGET_TEMP_LOW, 19.1),
("temperature_high_state_topic", "22.9", ATTR_TARGET_TEMP_HIGH, 22.9),
("temperature_state_topic", "19.9", ATTR_TEMPERATURE, 19.9),
("target_humidity_state_topic", "82.6", ATTR_HUMIDITY, 82.6),
],
)
async def test_encoding_subscribable_topics(
@ -1545,6 +1713,13 @@ async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config):
29.8,
"temperature_high_command_template",
),
(
climate.SERVICE_SET_HUMIDITY,
"target_humidity_command_topic",
{"humidity": "82"},
82,
"target_humidity_command_template",
),
],
)
async def test_publishing_with_custom_encoding(
@ -1578,6 +1753,62 @@ async def test_publishing_with_custom_encoding(
)
@pytest.mark.parametrize(
"config,valid",
[
(
{
"name": "test_valid_humidity_min_max",
"min_humidity": 20,
"max_humidity": 80,
},
True,
),
(
{
"name": "test_invalid_humidity_min_max_1",
"min_humidity": 0,
"max_humidity": 101,
},
False,
),
(
{
"name": "test_invalid_humidity_min_max_2",
"max_humidity": 20,
"min_humidity": 40,
},
False,
),
(
{
"name": "test_valid_humidity_state",
"target_humidity_state_topic": "humidity-state",
"target_humidity_command_topic": "humidity-command",
},
True,
),
(
{
"name": "test_invalid_humidity_state",
"target_humidity_state_topic": "humidity-state",
},
False,
),
],
)
async def test_humidity_configuration_validity(hass, config, valid):
"""Test the validity of humidity configurations."""
assert (
await async_setup_component(
hass,
mqtt.DOMAIN,
{mqtt.DOMAIN: {climate.DOMAIN: config}},
)
is valid
)
async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path):
"""Test reloading the MQTT platform."""
domain = climate.DOMAIN