MQTT climate preset_modes rework (#66062)

* MQTT climate preset_modes rework

* Set deprection date to 2022.9 (6 months)

* add valid_preset_mode_configuration for discovery

* Update deprecation date
This commit is contained in:
Jan Bouwhuis 2022-02-17 13:51:35 +01:00 committed by GitHub
parent 4236764fd5
commit 83846bb5cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 427 additions and 59 deletions

View file

@ -82,9 +82,34 @@ DEFAULT_CONFIG = {
"temperature_high_command_topic": "temperature-high-topic",
"fan_mode_command_topic": "fan-mode-topic",
"swing_mode_command_topic": "swing-mode-topic",
"aux_command_topic": "aux-topic",
"preset_mode_command_topic": "preset-mode-topic",
"preset_modes": [
"eco",
"away",
"boost",
"comfort",
"home",
"sleep",
"activity",
],
}
}
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
DEFAULT_LEGACY_CONFIG = {
CLIMATE_DOMAIN: {
"platform": "mqtt",
"name": "test",
"mode_command_topic": "mode-topic",
"temperature_command_topic": "temperature-topic",
"temperature_low_command_topic": "temperature-low-topic",
"temperature_high_command_topic": "temperature-high-topic",
"fan_mode_command_topic": "fan-mode-topic",
"swing_mode_command_topic": "swing-mode-topic",
"aux_command_topic": "aux-topic",
"away_mode_command_topic": "away-mode-topic",
"hold_command_topic": "hold-topic",
"aux_command_topic": "aux-topic",
}
}
@ -103,6 +128,42 @@ async def test_setup_params(hass, mqtt_mock):
assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP
async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog):
"""Test the preset mode payload reset configuration."""
config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN])
config["preset_modes"].append("none")
assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config})
await hass.async_block_till_done()
assert "Invalid config for [climate.mqtt]: not a valid value" in caplog.text
state = hass.states.get(ENTITY_CLIMATE)
assert state is None
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
@pytest.mark.parametrize(
"parameter,config_value",
[
("away_mode_command_topic", "away-mode-command-topic"),
("away_mode_state_topic", "away-mode-state-topic"),
("away_mode_state_template", "{{ value_json }}"),
("hold_mode_command_topic", "hold-mode-command-topic"),
("hold_mode_command_template", "hold-mode-command-template"),
("hold_mode_state_topic", "hold-mode-state-topic"),
("hold_mode_state_template", "{{ value_json }}"),
],
)
async def test_preset_modes_deprecation_guard(
hass, mqtt_mock, caplog, parameter, config_value
):
"""Test the configuration for invalid legacy parameters."""
config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN])
config[parameter] = config_value
assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config})
await hass.async_block_till_done()
state = hass.states.get(ENTITY_CLIMATE)
assert state is None
async def test_supported_features(hass, mqtt_mock):
"""Test the supported_features."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
@ -469,9 +530,99 @@ async def test_handle_action_received(hass, mqtt_mock):
assert hvac_action == action
async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog):
"""Test setting of the preset mode."""
config = copy.deepcopy(DEFAULT_CONFIG)
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"preset-mode-topic", "away", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "away"
await common.async_set_preset_mode(hass, "eco", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"preset-mode-topic", "eco", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "eco"
await common.async_set_preset_mode(hass, "none", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"preset-mode-topic", "none", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
await common.async_set_preset_mode(hass, "comfort", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"preset-mode-topic", "comfort", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "comfort"
await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE)
assert "'invalid' is not a valid preset mode" in caplog.text
async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog):
"""Test setting of the preset mode."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["climate"]["preset_mode_state_topic"] = "preset-mode-state"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "preset-mode-state", "away")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "away"
async_fire_mqtt_message(hass, "preset-mode-state", "eco")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "eco"
async_fire_mqtt_message(hass, "preset-mode-state", "none")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "preset-mode-state", "comfort")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "comfort"
async_fire_mqtt_message(hass, "preset-mode-state", "None")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "preset-mode-state", "home")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "home"
async_fire_mqtt_message(hass, "preset-mode-state", "nonsense")
assert (
"'nonsense' received on topic preset-mode-state. 'nonsense' is not a valid preset mode"
in caplog.text
)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "home"
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_away_mode_pessimistic(hass, mqtt_mock):
"""Test setting of the away mode."""
config = copy.deepcopy(DEFAULT_CONFIG)
config = copy.deepcopy(DEFAULT_LEGACY_CONFIG)
config["climate"]["away_mode_state_topic"] = "away-state"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
@ -496,9 +647,10 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock):
assert state.attributes.get("preset_mode") == "none"
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_away_mode(hass, mqtt_mock):
"""Test setting of the away mode."""
config = copy.deepcopy(DEFAULT_CONFIG)
config = copy.deepcopy(DEFAULT_LEGACY_CONFIG)
config["climate"]["payload_on"] = "AN"
config["climate"]["payload_off"] = "AUS"
@ -537,9 +689,10 @@ async def test_set_away_mode(hass, mqtt_mock):
assert state.attributes.get("preset_mode") == "away"
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_hold_pessimistic(hass, mqtt_mock):
"""Test setting the hold mode in pessimistic mode."""
config = copy.deepcopy(DEFAULT_CONFIG)
config = copy.deepcopy(DEFAULT_LEGACY_CONFIG)
config["climate"]["hold_state_topic"] = "hold-state"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
@ -560,9 +713,10 @@ async def test_set_hold_pessimistic(hass, mqtt_mock):
assert state.attributes.get("preset_mode") == "none"
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_hold(hass, mqtt_mock):
"""Test setting the hold mode."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_CLIMATE)
@ -591,9 +745,10 @@ async def test_set_hold(hass, mqtt_mock):
assert state.attributes.get("preset_mode") == "none"
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_preset_away(hass, mqtt_mock):
"""Test setting the hold mode and away mode."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_CLIMATE)
@ -624,9 +779,10 @@ async def test_set_preset_away(hass, mqtt_mock):
assert state.attributes.get("preset_mode") == "hold-on-again"
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_preset_away_pessimistic(hass, mqtt_mock):
"""Test setting the hold mode and away mode in pessimistic mode."""
config = copy.deepcopy(DEFAULT_CONFIG)
config = copy.deepcopy(DEFAULT_LEGACY_CONFIG)
config["climate"]["hold_state_topic"] = "hold-state"
config["climate"]["away_mode_state_topic"] = "away-state"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
@ -674,9 +830,10 @@ async def test_set_preset_away_pessimistic(hass, mqtt_mock):
assert state.attributes.get("preset_mode") == "hold-on-again"
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_preset_mode_twice(hass, mqtt_mock):
"""Test setting of the same mode twice only publishes once."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_CLIMATE)
@ -804,21 +961,19 @@ async def test_get_with_templates(hass, mqtt_mock, caplog):
# By default, just unquote the JSON-strings
config["climate"]["value_template"] = "{{ value_json }}"
config["climate"]["action_template"] = "{{ value_json }}"
# Something more complicated for hold mode
config["climate"]["hold_state_template"] = "{{ value_json.attribute }}"
# Rendering to a bool for aux heat
config["climate"]["aux_state_template"] = "{{ value == 'switchmeon' }}"
# Rendering preset_mode
config["climate"]["preset_mode_value_template"] = "{{ value_json.attribute }}"
config["climate"]["action_topic"] = "action"
config["climate"]["mode_state_topic"] = "mode-state"
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"]["away_mode_state_topic"] = "away-state"
config["climate"]["hold_state_topic"] = "hold-state"
config["climate"]["aux_state_topic"] = "aux-state"
config["climate"]["current_temperature_topic"] = "current-temperature"
config["climate"]["preset_mode_state_topic"] = "current-preset-mode"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
@ -854,31 +1009,18 @@ async def test_get_with_templates(hass, mqtt_mock, caplog):
# ... but the actual value stays unchanged.
assert state.attributes.get("temperature") == 1031
# Away Mode
# Preset Mode
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "away-state", '"ON"')
async_fire_mqtt_message(hass, "current-preset-mode", '{"attribute": "eco"}')
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "away"
# Away Mode with JSON values
async_fire_mqtt_message(hass, "away-state", "false")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "away-state", "true")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "away"
# Hold Mode
assert state.attributes.get("preset_mode") == "eco"
# Test with an empty json
async_fire_mqtt_message(
hass,
"hold-state",
"""
{ "attribute": "somemode" }
""",
hass, "current-preset-mode", '{"other_attribute": "some_value"}'
)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "somemode"
assert "Ignoring empty preset_mode from 'current-preset-mode'"
assert state.attributes.get("preset_mode") == "eco"
# Aux mode
assert state.attributes.get("aux_heat") == "off"
@ -911,12 +1053,60 @@ async def test_get_with_templates(hass, mqtt_mock, caplog):
)
async def test_set_with_templates(hass, mqtt_mock, caplog):
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog):
"""Test getting various for hold and away mode attributes with templates."""
config = copy.deepcopy(DEFAULT_LEGACY_CONFIG)
config["climate"]["mode_state_topic"] = "mode-state"
# By default, just unquote the JSON-strings
config["climate"]["value_template"] = "{{ value_json }}"
# Something more complicated for hold mode
config["climate"]["hold_state_template"] = "{{ value_json.attribute }}"
config["climate"]["away_mode_state_topic"] = "away-state"
config["climate"]["hold_state_topic"] = "hold-state"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
# Operation Mode
state = hass.states.get(ENTITY_CLIMATE)
async_fire_mqtt_message(hass, "mode-state", '"cool"')
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "cool"
# Away Mode
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "away-state", '"ON"')
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "away"
# Away Mode with JSON values
async_fire_mqtt_message(hass, "away-state", "false")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "none"
async_fire_mqtt_message(hass, "away-state", "true")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "away"
# Hold Mode
async_fire_mqtt_message(
hass,
"hold-state",
"""
{ "attribute": "somemode" }
""",
)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "somemode"
async def test_set_and_templates(hass, mqtt_mock, caplog):
"""Test setting various attributes with templates."""
config = copy.deepcopy(DEFAULT_CONFIG)
# Create simple templates
config["climate"]["fan_mode_command_template"] = "fan_mode: {{ value }}"
config["climate"]["hold_command_template"] = "hold: {{ value }}"
config["climate"]["preset_mode_command_template"] = "preset_mode: {{ value }}"
config["climate"]["mode_command_template"] = "mode: {{ value }}"
config["climate"]["swing_mode_command_template"] = "swing_mode: {{ value }}"
config["climate"]["temperature_command_template"] = "temp: {{ value }}"
@ -935,11 +1125,12 @@ async def test_set_with_templates(hass, mqtt_mock, caplog):
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("fan_mode") == "high"
# Hold Mode
# Preset Mode
await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE)
mqtt_mock.async_publish.call_count == 2
mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False)
mqtt_mock.async_publish.assert_any_call("hold-topic", "hold: eco", 0, False)
mqtt_mock.async_publish.call_count == 1
mqtt_mock.async_publish.assert_any_call(
"preset-mode-topic", "preset_mode: eco", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == PRESET_ECO
@ -987,6 +1178,26 @@ async def test_set_with_templates(hass, mqtt_mock, caplog):
assert state.attributes.get("target_temp_high") == 23
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplog):
"""Test setting various attributes on hold and away mode with templates."""
config = copy.deepcopy(DEFAULT_LEGACY_CONFIG)
# Create simple templates
config["climate"]["hold_command_template"] = "hold: {{ value }}"
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
# Hold Mode
await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE)
mqtt_mock.async_publish.call_count == 2
mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False)
mqtt_mock.async_publish.assert_any_call("hold-topic", "hold: eco", 0, False)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == PRESET_ECO
async def test_min_temp_custom(hass, mqtt_mock):
"""Test a custom min temp."""
config = copy.deepcopy(DEFAULT_CONFIG)
@ -1118,9 +1329,11 @@ async def test_unique_id(hass, mqtt_mock):
("action_topic", "heating", ATTR_HVAC_ACTION, "heating"),
("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"),
("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"),
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
("away_mode_state_topic", "ON", ATTR_PRESET_MODE, "away"),
("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1),
("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"),
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
("hold_state_topic", "mode1", ATTR_PRESET_MODE, "mode1"),
("mode_state_topic", "cool", None, None),
("mode_state_topic", "fan_only", None, None),
@ -1135,7 +1348,11 @@ async def test_encoding_subscribable_topics(
):
"""Test handling of incoming encoded payload."""
config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN])
config["hold_modes"] = ["mode1", "mode2"]
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
if topic in ["hold_state_topic", "away_mode_state_topic"]:
config["hold_modes"] = ["mode1", "mode2"]
del config["preset_modes"]
del config["preset_mode_command_topic"]
await help_test_encoding_subscribable_topics(
hass,
mqtt_mock,
@ -1317,6 +1534,13 @@ async def test_precision_whole(hass, mqtt_mock):
"cool",
"mode_command_template",
),
(
climate.SERVICE_SET_PRESET_MODE,
"preset_mode_command_topic",
{"preset_mode": "sleep"},
"sleep",
"preset_mode_command_template",
),
(
climate.SERVICE_SET_PRESET_MODE,
"away_mode_command_topic",
@ -1334,8 +1558,8 @@ async def test_precision_whole(hass, mqtt_mock):
(
climate.SERVICE_SET_PRESET_MODE,
"hold_command_topic",
{"preset_mode": "some_hold_mode"},
"some_hold_mode",
{"preset_mode": "comfort"},
"comfort",
"hold_command_template",
),
(
@ -1402,7 +1626,10 @@ async def test_publishing_with_custom_encoding(
):
"""Test publishing MQTT payload with different encoding."""
domain = climate.DOMAIN
config = DEFAULT_CONFIG[domain]
config = copy.deepcopy(DEFAULT_CONFIG[domain])
if topic != "preset_mode_command_topic":
del config["preset_mode_command_topic"]
del config["preset_modes"]
await help_test_publishing_with_custom_encoding(
hass,