Add support for swing horizontal mode for mqtt climate (#139303)

* Add support for swing horizontal mode for mqtt climate

* Fix import
This commit is contained in:
Jan Bouwhuis 2025-02-26 15:44:16 +01:00 committed by GitHub
parent 7e97ef588b
commit 5324f3e542
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 174 additions and 4 deletions

View File

@ -218,10 +218,16 @@ ABBREVIATIONS = {
"sup_vol": "support_volume_set",
"sup_feat": "supported_features",
"sup_clrm": "supported_color_modes",
"swing_h_mode_cmd_tpl": "swing_horizontal_mode_command_template",
"swing_h_mode_cmd_t": "swing_horizontal_mode_command_topic",
"swing_h_mode_stat_tpl": "swing_horizontal_mode_state_template",
"swing_h_mode_stat_t": "swing_horizontal_mode_state_topic",
"swing_h_modes": "swing_horizontal_modes",
"swing_mode_cmd_tpl": "swing_mode_command_template",
"swing_mode_cmd_t": "swing_mode_command_topic",
"swing_mode_stat_tpl": "swing_mode_state_template",
"swing_mode_stat_t": "swing_mode_state_topic",
"swing_modes": "swing_modes",
"temp_cmd_tpl": "temperature_command_template",
"temp_cmd_t": "temperature_command_topic",
"temp_hi_cmd_tpl": "temperature_high_command_template",

View File

@ -113,11 +113,19 @@ CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"
CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"
CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"
CONF_PRESET_MODES_LIST = "preset_modes"
CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template"
CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC = "swing_horizontal_mode_command_topic"
CONF_SWING_HORIZONTAL_MODE_LIST = "swing_horizontal_modes"
CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE = "swing_horizontal_mode_state_template"
CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC = "swing_horizontal_mode_state_topic"
CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template"
CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic"
CONF_SWING_MODE_LIST = "swing_modes"
CONF_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template"
CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic"
CONF_TEMP_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template"
CONF_TEMP_HIGH_COMMAND_TOPIC = "temperature_high_command_topic"
CONF_TEMP_HIGH_STATE_TEMPLATE = "temperature_high_state_template"
@ -145,6 +153,8 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset(
climate.ATTR_MIN_TEMP,
climate.ATTR_PRESET_MODE,
climate.ATTR_PRESET_MODES,
climate.ATTR_SWING_HORIZONTAL_MODE,
climate.ATTR_SWING_HORIZONTAL_MODES,
climate.ATTR_SWING_MODE,
climate.ATTR_SWING_MODES,
climate.ATTR_TARGET_TEMP_HIGH,
@ -162,6 +172,7 @@ VALUE_TEMPLATE_KEYS = (
CONF_MODE_STATE_TEMPLATE,
CONF_ACTION_TEMPLATE,
CONF_PRESET_MODE_VALUE_TEMPLATE,
CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE,
CONF_SWING_MODE_STATE_TEMPLATE,
CONF_TEMP_HIGH_STATE_TEMPLATE,
CONF_TEMP_LOW_STATE_TEMPLATE,
@ -174,6 +185,7 @@ COMMAND_TEMPLATE_KEYS = {
CONF_MODE_COMMAND_TEMPLATE,
CONF_POWER_COMMAND_TEMPLATE,
CONF_PRESET_MODE_COMMAND_TEMPLATE,
CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE,
CONF_SWING_MODE_COMMAND_TEMPLATE,
CONF_TEMP_COMMAND_TEMPLATE,
CONF_TEMP_HIGH_COMMAND_TEMPLATE,
@ -194,6 +206,8 @@ TOPIC_KEYS = (
CONF_POWER_COMMAND_TOPIC,
CONF_PRESET_MODE_COMMAND_TOPIC,
CONF_PRESET_MODE_STATE_TOPIC,
CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC,
CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC,
CONF_SWING_MODE_COMMAND_TOPIC,
CONF_SWING_MODE_STATE_TOPIC,
CONF_TEMP_COMMAND_TOPIC,
@ -302,6 +316,13 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(
CONF_SWING_HORIZONTAL_MODE_LIST, default=[SWING_ON, SWING_OFF]
): cv.ensure_list,
vol.Optional(CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(
@ -515,6 +536,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
_attr_fan_mode: str | None = None
_attr_hvac_mode: HVACMode | None = None
_attr_swing_horizontal_mode: str | None = None
_attr_swing_mode: str | None = None
_default_name = DEFAULT_NAME
_entity_id_format = climate.ENTITY_ID_FORMAT
@ -543,6 +565,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
if (precision := config.get(CONF_PRECISION)) is not None:
self._attr_precision = precision
self._attr_fan_modes = config[CONF_FAN_MODE_LIST]
self._attr_swing_horizontal_modes = config[CONF_SWING_HORIZONTAL_MODE_LIST]
self._attr_swing_modes = config[CONF_SWING_MODE_LIST]
self._attr_target_temperature_step = config[CONF_TEMP_STEP]
@ -568,6 +591,11 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None or self._optimistic:
self._attr_fan_mode = FAN_LOW
if (
self._topic[CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC] is None
or self._optimistic
):
self._attr_swing_horizontal_mode = SWING_OFF
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None or self._optimistic:
self._attr_swing_mode = SWING_OFF
if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic:
@ -629,6 +657,11 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
):
support |= ClimateEntityFeature.FAN_MODE
if (self._topic[CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC] is not None) or (
self._topic[CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC] is not None
):
support |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
if (self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None) or (
self._topic[CONF_SWING_MODE_COMMAND_TOPIC] is not None
):
@ -744,6 +777,16 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
),
{"_attr_fan_mode"},
)
self.add_subscription(
CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC,
partial(
self._handle_mode_received,
CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE,
"_attr_swing_horizontal_mode",
CONF_SWING_HORIZONTAL_MODE_LIST,
),
{"_attr_swing_horizontal_mode"},
)
self.add_subscription(
CONF_SWING_MODE_STATE_TOPIC,
partial(
@ -782,6 +825,20 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
self.async_write_ha_state()
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
"""Set new swing horizontal mode."""
payload = self._command_templates[CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE](
swing_horizontal_mode
)
await self._publish(CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC, payload)
if (
self._optimistic
or self._topic[CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC] is None
):
self._attr_swing_horizontal_mode = swing_horizontal_mode
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

@ -11,6 +11,7 @@ from homeassistant.components.climate import (
ATTR_HUMIDITY,
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
ATTR_SWING_HORIZONTAL_MODE,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
@ -20,10 +21,11 @@ from homeassistant.components.climate import (
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SWING_HORIZONTAL_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.components.climate.const import HVACMode
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
@ -211,6 +213,20 @@ def set_operation_mode(
hass.services.call(DOMAIN, SERVICE_SET_HVAC_MODE, data)
async def async_set_swing_horizontal_mode(
hass: HomeAssistant, swing_horizontal_mode: str, entity_id: str = ENTITY_MATCH_ALL
) -> None:
"""Set new target swing horizontal mode."""
data = {ATTR_SWING_HORIZONTAL_MODE: swing_horizontal_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
await hass.services.async_call(
DOMAIN, SERVICE_SET_SWING_HORIZONTAL_MODE, data, blocking=True
)
async def async_set_swing_mode(
hass: HomeAssistant, swing_mode: str, entity_id: str = ENTITY_MATCH_ALL
) -> None:

View File

@ -15,6 +15,7 @@ from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
ATTR_SWING_HORIZONTAL_MODE,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
@ -85,6 +86,7 @@ DEFAULT_CONFIG = {
"temperature_high_command_topic": "temperature-high-topic",
"fan_mode_command_topic": "fan-mode-topic",
"swing_mode_command_topic": "swing-mode-topic",
"swing_horizontal_mode_command_topic": "swing-horizontal-mode-topic",
"preset_mode_command_topic": "preset-mode-topic",
"preset_modes": [
"eco",
@ -111,6 +113,7 @@ async def test_setup_params(
assert state.attributes.get("temperature") == 21
assert state.attributes.get("fan_mode") == "low"
assert state.attributes.get("swing_mode") == "off"
assert state.attributes.get("swing_horizontal_mode") == "off"
assert state.state == "off"
assert state.attributes.get("min_temp") == DEFAULT_MIN_TEMP
assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP
@ -123,6 +126,7 @@ async def test_setup_params(
| ClimateEntityFeature.TARGET_HUMIDITY
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.SWING_HORIZONTAL_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
@ -159,6 +163,7 @@ async def test_supported_features(
state = hass.states.get(ENTITY_CLIMATE)
support = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.SWING_HORIZONTAL_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
@ -562,12 +567,29 @@ async def test_set_swing_mode_bad_attr(
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "off"
assert state.attributes.get("swing_horizontal_mode") == "off"
with pytest.raises(vol.Invalid) as excinfo:
await common.async_set_swing_horizontal_mode(hass, None, ENTITY_CLIMATE) # type:ignore[arg-type]
assert (
"string value is None for dictionary value @ data['swing_horizontal_mode']"
in str(excinfo.value)
)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "off"
@pytest.mark.parametrize(
"hass_config",
[
help_custom_config(
climate.DOMAIN, DEFAULT_CONFIG, ({"swing_mode_state_topic": "swing-state"},)
climate.DOMAIN,
DEFAULT_CONFIG,
(
{
"swing_mode_state_topic": "swing-state",
"swing_horizontal_mode_state_topic": "swing-horizontal-state",
},
),
)
],
)
@ -579,19 +601,32 @@ async def test_set_swing_pessimistic(
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") is None
assert state.attributes.get("swing_horizontal_mode") is None
await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") is None
await common.async_set_swing_horizontal_mode(hass, "on", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") is None
async_fire_mqtt_message(hass, "swing-state", "on")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "on"
async_fire_mqtt_message(hass, "swing-horizontal-state", "on")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "on"
async_fire_mqtt_message(hass, "swing-state", "bogus state")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "on"
async_fire_mqtt_message(hass, "swing-horizontal-state", "bogus state")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "on"
@pytest.mark.parametrize(
"hass_config",
@ -599,7 +634,13 @@ async def test_set_swing_pessimistic(
help_custom_config(
climate.DOMAIN,
DEFAULT_CONFIG,
({"swing_mode_state_topic": "swing-state", "optimistic": True},),
(
{
"swing_mode_state_topic": "swing-state",
"swing_horizontal_mode_state_topic": "swing-horizontal-state",
"optimistic": True,
},
),
)
],
)
@ -611,19 +652,32 @@ async def test_set_swing_optimistic(
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "off"
assert state.attributes.get("swing_horizontal_mode") == "off"
await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "on"
await common.async_set_swing_horizontal_mode(hass, "on", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "on"
async_fire_mqtt_message(hass, "swing-state", "off")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "off"
async_fire_mqtt_message(hass, "swing-horizontal-state", "off")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "off"
async_fire_mqtt_message(hass, "swing-state", "bogus state")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "off"
async_fire_mqtt_message(hass, "swing-horizontal-state", "bogus state")
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "off"
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
async def test_set_swing(
@ -638,6 +692,15 @@ async def test_set_swing(
mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "on", 0, False)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "on"
mqtt_mock.reset_mock()
assert state.attributes.get("swing_horizontal_mode") == "off"
await common.async_set_swing_horizontal_mode(hass, "on", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"swing-horizontal-mode-topic", "on", 0, False
)
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "on"
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@ -1337,6 +1400,7 @@ async def test_get_target_temperature_low_high_with_templates(
"temperature_low_command_topic": "temperature-low-topic",
"temperature_high_command_topic": "temperature-high-topic",
"fan_mode_command_topic": "fan-mode-topic",
"swing_horizontal_mode_command_topic": "swing-horizontal-mode-topic",
"swing_mode_command_topic": "swing-mode-topic",
"preset_mode_command_topic": "preset-mode-topic",
"preset_modes": [
@ -1359,6 +1423,7 @@ async def test_get_target_temperature_low_high_with_templates(
"action_topic": "action",
"mode_state_topic": "mode-state",
"fan_mode_state_topic": "fan-state",
"swing_horizontal_mode_state_topic": "swing-horizontal-state",
"swing_mode_state_topic": "swing-state",
"temperature_state_topic": "temperature-state",
"target_humidity_state_topic": "humidity-state",
@ -1396,6 +1461,12 @@ async def test_get_with_templates(
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_mode") == "on"
# Swing Horizontal Mode
assert state.attributes.get("swing_horizontal_mode") is None
async_fire_mqtt_message(hass, "swing-horizontal-state", '"on"')
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "on"
# Temperature - with valid value
assert state.attributes.get("temperature") is None
async_fire_mqtt_message(hass, "temperature-state", '"1031"')
@ -1495,6 +1566,7 @@ async def test_get_with_templates(
"temperature_low_command_topic": "temperature-low-topic",
"temperature_high_command_topic": "temperature-high-topic",
"fan_mode_command_topic": "fan-mode-topic",
"swing_horizontal_mode_command_topic": "swing-horizontal-mode-topic",
"swing_mode_command_topic": "swing-mode-topic",
"preset_mode_command_topic": "preset-mode-topic",
"preset_modes": [
@ -1511,6 +1583,7 @@ async def test_get_with_templates(
"power_command_template": "power: {{ value }}",
"preset_mode_command_template": "preset_mode: {{ value }}",
"mode_command_template": "mode: {{ value }}",
"swing_horizontal_mode_command_template": "swing_horizontal_mode: {{ value }}",
"swing_mode_command_template": "swing_mode: {{ value }}",
"temperature_command_template": "temp: {{ value }}",
"temperature_high_command_template": "temp_hi: {{ value }}",
@ -1580,6 +1653,15 @@ async def test_set_and_templates(
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "off"
# Swing Horizontal Mode
await common.async_set_swing_horizontal_mode(hass, "on", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"swing-horizontal-mode-topic", "swing_horizontal_mode: on", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("swing_horizontal_mode") == "on"
# Swing Mode
await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
@ -1940,6 +2022,7 @@ async def test_unique_id(
("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"),
("mode_state_topic", "cool", None, None),
("mode_state_topic", "fan_only", None, None),
("swing_horizontal_mode_state_topic", "on", ATTR_SWING_HORIZONTAL_MODE, "on"),
("swing_mode_state_topic", "on", ATTR_SWING_MODE, "on"),
("temperature_low_state_topic", "19.1", ATTR_TARGET_TEMP_LOW, 19.1),
("temperature_high_state_topic", "22.9", ATTR_TARGET_TEMP_HIGH, 22.9),
@ -2178,6 +2261,13 @@ async def test_precision_whole(
"medium",
"fan_mode_command_template",
),
(
climate.SERVICE_SET_SWING_HORIZONTAL_MODE,
"swing_horizontal_mode_command_topic",
{"swing_horizontal_mode": "on"},
"on",
"swing_horizontal_mode_command_template",
),
(
climate.SERVICE_SET_SWING_MODE,
"swing_mode_command_topic",
@ -2378,6 +2468,7 @@ async def test_unload_entry(
"current_temperature_topic": "current-temperature-topic",
"preset_mode_state_topic": "preset-mode-state-topic",
"preset_modes": ["eco", "away"],
"swing_horizontal_mode_state_topic": "swing-horizontal-mode-state-topic",
"swing_mode_state_topic": "swing-mode-state-topic",
"target_humidity_state_topic": "target-humidity-state-topic",
"temperature_high_state_topic": "temperature-high-state-topic",
@ -2399,6 +2490,7 @@ async def test_unload_entry(
("current-humidity-topic", "45", "46"),
("current-temperature-topic", "18.0", "18.1"),
("preset-mode-state-topic", "eco", "away"),
("swing-horizontal-mode-state-topic", "on", "off"),
("swing-mode-state-topic", "on", "off"),
("target-humidity-state-topic", "45", "50"),
("temperature-state-topic", "18", "19"),

View File

@ -2380,7 +2380,6 @@ ABBREVIATIONS_WHITE_LIST = [
"CONF_PRECISION",
"CONF_QOS",
"CONF_SCHEMA",
"CONF_SWING_MODE_LIST",
"CONF_TEMP_STEP",
# Removed
"CONF_WHITE_VALUE",