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:
Jan Bouwhuis 2021-05-18 08:24:42 +02:00 committed by GitHub
parent 5da0191fe3
commit 9abf43f95f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 7 deletions

View file

@ -119,6 +119,8 @@ ABBREVIATIONS = {
"pl_osc_off": "payload_oscillation_off",
"pl_osc_on": "payload_oscillation_on",
"pl_paus": "payload_pause",
"pl_rst_pct": "payload_reset_percentage",
"pl_rst_pr_mode": "payload_reset_preset_mode",
"pl_stop": "payload_stop",
"pl_strt": "payload_start",
"pl_stpa": "payload_start_pause",

View file

@ -59,6 +59,7 @@ CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_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_MAX = "speed_range_max"
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_COMMAND_TEMPLATE = "preset_mode_command_template"
CONF_PRESET_MODES_LIST = "preset_modes"
CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"
CONF_SPEED_STATE_TOPIC = "speed_state_topic"
CONF_SPEED_COMMAND_TOPIC = "speed_command_topic"
CONF_SPEED_VALUE_TEMPLATE = "speed_value_template"
@ -84,6 +86,7 @@ CONF_SPEED_LIST = "speeds"
DEFAULT_NAME = "MQTT Fan"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_RESET = "None"
DEFAULT_OPTIMISTIC = False
DEFAULT_SPEED_RANGE_MIN = 1
DEFAULT_SPEED_RANGE_MAX = 100
@ -113,6 +116,13 @@ def valid_speed_range_configuration(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(
# 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,
@ -153,6 +163,12 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(
CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX
): 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_LOW_SPEED, default=SPEED_LOW): 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),
valid_fan_speed_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_HIGH": config[CONF_PAYLOAD_HIGH_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)
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)
def percentage_received(msg):
"""Handle new received MQTT message for the percentage."""
numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload)
if not numeric_val_str:
rendered_percentage_payload = self._value_templates[ATTR_PERCENTAGE](
msg.payload
)
if not rendered_percentage_payload:
_LOGGER.debug("Ignoring empty speed from '%s'", msg.topic)
return
if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]:
self._percentage = None
self._speed = None
self.async_write_ha_state()
return
try:
percentage = ranged_value_to_percentage(
self._speed_range, int(numeric_val_str)
self._speed_range, int(rendered_percentage_payload)
)
except ValueError:
_LOGGER.warning(
"'%s' received on topic %s. '%s' is not a valid speed within the speed range",
msg.payload,
msg.topic,
numeric_val_str,
rendered_percentage_payload,
)
return
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",
msg.payload,
msg.topic,
numeric_val_str,
rendered_percentage_payload,
)
return
self._percentage = percentage
@ -404,6 +430,10 @@ class MqttFan(MqttEntity, FanEntity):
def preset_mode_received(msg):
"""Handle new received MQTT message for preset mode."""
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:
_LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic)
return
@ -658,7 +688,7 @@ class MqttFan(MqttEntity, FanEntity):
if self._optimistic_preset_mode:
self._preset_mode = preset_mode
self.async_write_ha_state()
self.async_write_ha_state()
# async_set_speed is deprecated, support will be removed after a quarter (2021.7)
async def async_set_speed(self, speed: str) -> None:
@ -691,7 +721,7 @@ class MqttFan(MqttEntity, FanEntity):
if self._optimistic_speed and speed_payload:
self._speed = speed
self.async_write_ha_state()
self.async_write_ha_state()
async def async_oscillate(self, oscillating: bool) -> None:
"""Set oscillation.

View file

@ -100,6 +100,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog):
"payload_low_speed": "speed_lOw",
"payload_medium_speed": "speed_mEdium",
"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")
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")
assert "not a valid preset mode" in caplog.text
caplog.clear()
@ -191,6 +197,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog):
state = hass.states.get("fan.test")
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")
assert "not a valid speed" in caplog.text
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")
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}')
assert "Ignoring empty speed from" in caplog.text
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")
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}')
assert "Ignoring empty preset_mode from" in caplog.text
caplog.clear()
@ -1895,6 +1914,21 @@ async def test_supported_features(hass, mqtt_mock):
"speed_range_min": 0,
"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")
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):
"""Test availability after MQTT disconnection."""