mirror of
https://github.com/home-assistant/core.git
synced 2025-07-11 15:27:08 +00:00
Mqtt fan feature for resetting current speed percentage
or preset_mode
(#50565)
* Mqtt fan resetting speed percentage or preset_mode * tests reset payload is working with val templates * Remove duplicate line for CONF_PAYLOAD_HIGH_SPEED
This commit is contained in:
parent
5da0191fe3
commit
9abf43f95f
@ -119,6 +119,8 @@ ABBREVIATIONS = {
|
|||||||
"pl_osc_off": "payload_oscillation_off",
|
"pl_osc_off": "payload_oscillation_off",
|
||||||
"pl_osc_on": "payload_oscillation_on",
|
"pl_osc_on": "payload_oscillation_on",
|
||||||
"pl_paus": "payload_pause",
|
"pl_paus": "payload_pause",
|
||||||
|
"pl_rst_pct": "payload_reset_percentage",
|
||||||
|
"pl_rst_pr_mode": "payload_reset_preset_mode",
|
||||||
"pl_stop": "payload_stop",
|
"pl_stop": "payload_stop",
|
||||||
"pl_strt": "payload_start",
|
"pl_strt": "payload_start",
|
||||||
"pl_stpa": "payload_start_pause",
|
"pl_stpa": "payload_start_pause",
|
||||||
|
@ -59,6 +59,7 @@ CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
|
|||||||
CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
|
CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
|
||||||
CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"
|
CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"
|
||||||
CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"
|
CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"
|
||||||
|
CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage"
|
||||||
CONF_SPEED_RANGE_MIN = "speed_range_min"
|
CONF_SPEED_RANGE_MIN = "speed_range_min"
|
||||||
CONF_SPEED_RANGE_MAX = "speed_range_max"
|
CONF_SPEED_RANGE_MAX = "speed_range_max"
|
||||||
CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"
|
CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"
|
||||||
@ -66,6 +67,7 @@ CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"
|
|||||||
CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"
|
CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"
|
||||||
CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"
|
CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"
|
||||||
CONF_PRESET_MODES_LIST = "preset_modes"
|
CONF_PRESET_MODES_LIST = "preset_modes"
|
||||||
|
CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"
|
||||||
CONF_SPEED_STATE_TOPIC = "speed_state_topic"
|
CONF_SPEED_STATE_TOPIC = "speed_state_topic"
|
||||||
CONF_SPEED_COMMAND_TOPIC = "speed_command_topic"
|
CONF_SPEED_COMMAND_TOPIC = "speed_command_topic"
|
||||||
CONF_SPEED_VALUE_TEMPLATE = "speed_value_template"
|
CONF_SPEED_VALUE_TEMPLATE = "speed_value_template"
|
||||||
@ -84,6 +86,7 @@ CONF_SPEED_LIST = "speeds"
|
|||||||
DEFAULT_NAME = "MQTT Fan"
|
DEFAULT_NAME = "MQTT Fan"
|
||||||
DEFAULT_PAYLOAD_ON = "ON"
|
DEFAULT_PAYLOAD_ON = "ON"
|
||||||
DEFAULT_PAYLOAD_OFF = "OFF"
|
DEFAULT_PAYLOAD_OFF = "OFF"
|
||||||
|
DEFAULT_PAYLOAD_RESET = "None"
|
||||||
DEFAULT_OPTIMISTIC = False
|
DEFAULT_OPTIMISTIC = False
|
||||||
DEFAULT_SPEED_RANGE_MIN = 1
|
DEFAULT_SPEED_RANGE_MIN = 1
|
||||||
DEFAULT_SPEED_RANGE_MAX = 100
|
DEFAULT_SPEED_RANGE_MAX = 100
|
||||||
@ -113,6 +116,13 @@ def valid_speed_range_configuration(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def valid_preset_mode_configuration(config):
|
||||||
|
"""Validate that the preset mode reset payload is not one of the preset modes."""
|
||||||
|
if config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config.get(CONF_PRESET_MODES_LIST):
|
||||||
|
raise ValueError("preset_modes must not contain payload_reset_preset_mode")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.All(
|
PLATFORM_SCHEMA = vol.All(
|
||||||
# CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and
|
# CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and
|
||||||
# Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF,
|
# Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF,
|
||||||
@ -153,6 +163,12 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX
|
CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX
|
||||||
): cv.positive_int,
|
): cv.positive_int,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PAYLOAD_RESET_PERCENTAGE, default=DEFAULT_PAYLOAD_RESET
|
||||||
|
): cv.string,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET
|
||||||
|
): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string,
|
vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string,
|
vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string,
|
vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string,
|
||||||
@ -177,6 +193,7 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema),
|
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema),
|
||||||
valid_fan_speed_configuration,
|
valid_fan_speed_configuration,
|
||||||
valid_speed_range_configuration,
|
valid_speed_range_configuration,
|
||||||
|
valid_preset_mode_configuration,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -281,6 +298,8 @@ class MqttFan(MqttEntity, FanEntity):
|
|||||||
"SPEED_MEDIUM": config[CONF_PAYLOAD_MEDIUM_SPEED],
|
"SPEED_MEDIUM": config[CONF_PAYLOAD_MEDIUM_SPEED],
|
||||||
"SPEED_HIGH": config[CONF_PAYLOAD_HIGH_SPEED],
|
"SPEED_HIGH": config[CONF_PAYLOAD_HIGH_SPEED],
|
||||||
"SPEED_OFF": config[CONF_PAYLOAD_OFF_SPEED],
|
"SPEED_OFF": config[CONF_PAYLOAD_OFF_SPEED],
|
||||||
|
"PERCENTAGE_RESET": config[CONF_PAYLOAD_RESET_PERCENTAGE],
|
||||||
|
"PRESET_MODE_RESET": config[CONF_PAYLOAD_RESET_PRESET_MODE],
|
||||||
}
|
}
|
||||||
# The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
|
# The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
|
||||||
self._feature_legacy_speeds = not self._topic[CONF_SPEED_COMMAND_TOPIC] is None
|
self._feature_legacy_speeds = not self._topic[CONF_SPEED_COMMAND_TOPIC] is None
|
||||||
@ -364,20 +383,27 @@ class MqttFan(MqttEntity, FanEntity):
|
|||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
def percentage_received(msg):
|
def percentage_received(msg):
|
||||||
"""Handle new received MQTT message for the percentage."""
|
"""Handle new received MQTT message for the percentage."""
|
||||||
numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload)
|
rendered_percentage_payload = self._value_templates[ATTR_PERCENTAGE](
|
||||||
if not numeric_val_str:
|
msg.payload
|
||||||
|
)
|
||||||
|
if not rendered_percentage_payload:
|
||||||
_LOGGER.debug("Ignoring empty speed from '%s'", msg.topic)
|
_LOGGER.debug("Ignoring empty speed from '%s'", msg.topic)
|
||||||
return
|
return
|
||||||
|
if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]:
|
||||||
|
self._percentage = None
|
||||||
|
self._speed = None
|
||||||
|
self.async_write_ha_state()
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
percentage = ranged_value_to_percentage(
|
percentage = ranged_value_to_percentage(
|
||||||
self._speed_range, int(numeric_val_str)
|
self._speed_range, int(rendered_percentage_payload)
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"'%s' received on topic %s. '%s' is not a valid speed within the speed range",
|
"'%s' received on topic %s. '%s' is not a valid speed within the speed range",
|
||||||
msg.payload,
|
msg.payload,
|
||||||
msg.topic,
|
msg.topic,
|
||||||
numeric_val_str,
|
rendered_percentage_payload,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if percentage < 0 or percentage > 100:
|
if percentage < 0 or percentage > 100:
|
||||||
@ -385,7 +411,7 @@ class MqttFan(MqttEntity, FanEntity):
|
|||||||
"'%s' received on topic %s. '%s' is not a valid speed within the speed range",
|
"'%s' received on topic %s. '%s' is not a valid speed within the speed range",
|
||||||
msg.payload,
|
msg.payload,
|
||||||
msg.topic,
|
msg.topic,
|
||||||
numeric_val_str,
|
rendered_percentage_payload,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
self._percentage = percentage
|
self._percentage = percentage
|
||||||
@ -404,6 +430,10 @@ class MqttFan(MqttEntity, FanEntity):
|
|||||||
def preset_mode_received(msg):
|
def preset_mode_received(msg):
|
||||||
"""Handle new received MQTT message for preset mode."""
|
"""Handle new received MQTT message for preset mode."""
|
||||||
preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload)
|
preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload)
|
||||||
|
if preset_mode == self._payload["PRESET_MODE_RESET"]:
|
||||||
|
self._preset_mode = None
|
||||||
|
self.async_write_ha_state()
|
||||||
|
return
|
||||||
if not preset_mode:
|
if not preset_mode:
|
||||||
_LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic)
|
_LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic)
|
||||||
return
|
return
|
||||||
|
@ -100,6 +100,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog):
|
|||||||
"payload_low_speed": "speed_lOw",
|
"payload_low_speed": "speed_lOw",
|
||||||
"payload_medium_speed": "speed_mEdium",
|
"payload_medium_speed": "speed_mEdium",
|
||||||
"payload_high_speed": "speed_High",
|
"payload_high_speed": "speed_High",
|
||||||
|
"payload_reset_percentage": "rEset_percentage",
|
||||||
|
"payload_reset_preset_mode": "rEset_preset_mode",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -168,6 +170,10 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog):
|
|||||||
state = hass.states.get("fan.test")
|
state = hass.states.get("fan.test")
|
||||||
assert state.attributes.get("preset_mode") == "silent"
|
assert state.attributes.get("preset_mode") == "silent"
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "preset-mode-state-topic", "rEset_preset_mode")
|
||||||
|
state = hass.states.get("fan.test")
|
||||||
|
assert state.attributes.get("preset_mode") is None
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, "preset-mode-state-topic", "ModeUnknown")
|
async_fire_mqtt_message(hass, "preset-mode-state-topic", "ModeUnknown")
|
||||||
assert "not a valid preset mode" in caplog.text
|
assert "not a valid preset mode" in caplog.text
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -191,6 +197,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog):
|
|||||||
state = hass.states.get("fan.test")
|
state = hass.states.get("fan.test")
|
||||||
assert state.attributes.get("speed") == fan.SPEED_OFF
|
assert state.attributes.get("speed") == fan.SPEED_OFF
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "percentage-state-topic", "rEset_percentage")
|
||||||
|
state = hass.states.get("fan.test")
|
||||||
|
assert state.attributes.get(fan.ATTR_PERCENTAGE) is None
|
||||||
|
assert state.attributes.get(fan.ATTR_SPEED) is None
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, "speed-state-topic", "speed_very_high")
|
async_fire_mqtt_message(hass, "speed-state-topic", "speed_very_high")
|
||||||
assert "not a valid speed" in caplog.text
|
assert "not a valid speed" in caplog.text
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -408,6 +419,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap
|
|||||||
state = hass.states.get("fan.test")
|
state = hass.states.get("fan.test")
|
||||||
assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
|
assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "percentage-state-topic", '{"val": "None"}')
|
||||||
|
state = hass.states.get("fan.test")
|
||||||
|
assert state.attributes.get(fan.ATTR_PERCENTAGE) is None
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}')
|
async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}')
|
||||||
assert "Ignoring empty speed from" in caplog.text
|
assert "Ignoring empty speed from" in caplog.text
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -428,6 +443,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap
|
|||||||
state = hass.states.get("fan.test")
|
state = hass.states.get("fan.test")
|
||||||
assert state.attributes.get("preset_mode") == "silent"
|
assert state.attributes.get("preset_mode") == "silent"
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "None"}')
|
||||||
|
state = hass.states.get("fan.test")
|
||||||
|
assert state.attributes.get("preset_mode") is None
|
||||||
|
|
||||||
async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}')
|
async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}')
|
||||||
assert "Ignoring empty preset_mode from" in caplog.text
|
assert "Ignoring empty preset_mode from" in caplog.text
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -1895,6 +1914,21 @@ async def test_supported_features(hass, mqtt_mock):
|
|||||||
"speed_range_min": 0,
|
"speed_range_min": 0,
|
||||||
"speed_range_max": 40,
|
"speed_range_max": 40,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"platform": "mqtt",
|
||||||
|
"name": "test7reset_payload_in_preset_modes_a",
|
||||||
|
"command_topic": "command-topic",
|
||||||
|
"preset_mode_command_topic": "preset-mode-command-topic",
|
||||||
|
"preset_modes": ["auto", "smart", "normal", "None"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "mqtt",
|
||||||
|
"name": "test7reset_payload_in_preset_modes_b",
|
||||||
|
"command_topic": "command-topic",
|
||||||
|
"preset_mode_command_topic": "preset-mode-command-topic",
|
||||||
|
"preset_modes": ["whoosh", "silent", "auto", "None"],
|
||||||
|
"payload_reset_preset_mode": "normal",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -1962,6 +1996,11 @@ async def test_supported_features(hass, mqtt_mock):
|
|||||||
state = hass.states.get("fan.test6spd_range_c")
|
state = hass.states.get("fan.test6spd_range_c")
|
||||||
assert state is None
|
assert state is None
|
||||||
|
|
||||||
|
state = hass.states.get("fan.test7reset_payload_in_preset_modes_a")
|
||||||
|
assert state is None
|
||||||
|
state = hass.states.get("fan.test7reset_payload_in_preset_modes_b")
|
||||||
|
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE
|
||||||
|
|
||||||
|
|
||||||
async def test_availability_when_connection_lost(hass, mqtt_mock):
|
async def test_availability_when_connection_lost(hass, mqtt_mock):
|
||||||
"""Test availability after MQTT disconnection."""
|
"""Test availability after MQTT disconnection."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user