mirror of
https://github.com/home-assistant/core.git
synced 2025-11-09 10:59:40 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user