Skip to content

Commit

Permalink
Enhance MQTT cover platform (#46059)
Browse files Browse the repository at this point in the history
* Enhance MQTT cover platform

Allow combining of position and state of MQTT cover
Add template and fix optimistic in set tilt position
Add tests

* Add abbreviations

* Add tests and stopped state

* Cleanup & fix range for templates

* Apply suggestions from code review

Co-authored-by: Erik Montnemery <erik@montnemery.com>
  • Loading branch information
thecode and emontnemery committed Feb 8, 2021
1 parent 8f4ea38 commit 81c88cd
Show file tree
Hide file tree
Showing 3 changed files with 434 additions and 24 deletions.
3 changes: 3 additions & 0 deletions homeassistant/components/mqtt/abbreviations.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"set_pos_tpl": "set_position_template",
"set_pos_t": "set_position_topic",
"pos_t": "position_topic",
"pos_tpl": "position_template",
"spd_cmd_t": "speed_command_topic",
"spd_stat_t": "speed_state_topic",
"spd_val_tpl": "speed_value_template",
Expand All @@ -147,6 +148,7 @@
"stat_on": "state_on",
"stat_open": "state_open",
"stat_opening": "state_opening",
"stat_stopped": "state_stopped",
"stat_locked": "state_locked",
"stat_unlocked": "state_unlocked",
"stat_t": "state_topic",
Expand All @@ -173,6 +175,7 @@
"temp_unit": "temperature_unit",
"tilt_clsd_val": "tilt_closed_value",
"tilt_cmd_t": "tilt_command_topic",
"tilt_cmd_tpl": "tilt_command_template",
"tilt_inv_stat": "tilt_invert_state",
"tilt_max": "tilt_max",
"tilt_min": "tilt_min",
Expand Down
108 changes: 84 additions & 24 deletions homeassistant/components/mqtt/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@
_LOGGER = logging.getLogger(__name__)

CONF_GET_POSITION_TOPIC = "position_topic"
CONF_SET_POSITION_TEMPLATE = "set_position_template"
CONF_GET_POSITION_TEMPLATE = "position_template"
CONF_SET_POSITION_TOPIC = "set_position_topic"
CONF_SET_POSITION_TEMPLATE = "set_position_template"
CONF_TILT_COMMAND_TOPIC = "tilt_command_topic"
CONF_TILT_COMMAND_TEMPLATE = "tilt_command_template"
CONF_TILT_STATUS_TOPIC = "tilt_status_topic"
CONF_TILT_STATUS_TEMPLATE = "tilt_status_template"

Expand All @@ -74,6 +76,7 @@
CONF_STATE_CLOSING = "state_closing"
CONF_STATE_OPEN = "state_open"
CONF_STATE_OPENING = "state_opening"
CONF_STATE_STOPPED = "state_stopped"
CONF_TILT_CLOSED_POSITION = "tilt_closed_value"
CONF_TILT_INVERT_STATE = "tilt_invert_state"
CONF_TILT_MAX = "tilt_max"
Expand All @@ -92,6 +95,7 @@
DEFAULT_POSITION_CLOSED = 0
DEFAULT_POSITION_OPEN = 100
DEFAULT_RETAIN = False
DEFAULT_STATE_STOPPED = "stopped"
DEFAULT_TILT_CLOSED_POSITION = 0
DEFAULT_TILT_INVERT_STATE = False
DEFAULT_TILT_MAX = 100
Expand All @@ -115,8 +119,27 @@ def validate_options(value):
"""
if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value:
raise vol.Invalid(
"set_position_topic must be set together with position_topic."
"'set_position_topic' must be set together with 'position_topic'."
)

if (
CONF_GET_POSITION_TOPIC in value
and CONF_STATE_TOPIC not in value
and CONF_VALUE_TEMPLATE in value
):
_LOGGER.warning(
"using 'value_template' for 'position_topic' is deprecated "
"and will be removed from Home Assistant in version 2021.6"
"please replace it with 'position_template'"
)

if CONF_TILT_INVERT_STATE in value:
_LOGGER.warning(
"'tilt_invert_state' is deprecated "
"and will be removed from Home Assistant in version 2021.6"
"please invert tilt using 'tilt_min' & 'tilt_max'"
)

return value


Expand All @@ -143,6 +166,7 @@ def validate_options(value):
vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string,
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string,
vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(
CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION
Expand All @@ -163,6 +187,8 @@ def validate_options(value):
vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template,
}
)
.extend(MQTT_AVAILABILITY_SCHEMA.schema)
Expand Down Expand Up @@ -228,6 +254,9 @@ def _setup_from_config(self, config):
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
if set_position_template is not None:
set_position_template.hass = self.hass
set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE)
if set_tilt_template is not None:
set_tilt_template.hass = self.hass
tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE)
if tilt_status_template is not None:
tilt_status_template.hass = self.hass
Expand Down Expand Up @@ -266,17 +295,31 @@ def state_message_received(msg):
if template is not None:
payload = template.async_render_with_possible_json_value(payload)

if payload == self._config[CONF_STATE_OPEN]:
self._state = STATE_OPEN
if payload == self._config[CONF_STATE_STOPPED]:
if (
self._optimistic
or self._config.get(CONF_GET_POSITION_TOPIC) is None
):
self._state = (
STATE_CLOSED if self._state == STATE_CLOSING else STATE_OPEN
)
else:
self._state = (
STATE_CLOSED
if self._position == DEFAULT_POSITION_CLOSED
else STATE_OPEN
)
elif payload == self._config[CONF_STATE_OPENING]:
self._state = STATE_OPENING
elif payload == self._config[CONF_STATE_CLOSED]:
self._state = STATE_CLOSED
elif payload == self._config[CONF_STATE_CLOSING]:
self._state = STATE_CLOSING
elif payload == self._config[CONF_STATE_OPEN]:
self._state = STATE_OPEN
elif payload == self._config[CONF_STATE_CLOSED]:
self._state = STATE_CLOSED
else:
_LOGGER.warning(
"Payload is not supported (e.g. open, closed, opening, closing): %s",
"Payload is not supported (e.g. open, closed, opening, closing, stopped): %s",
payload,
)
return
Expand All @@ -286,9 +329,16 @@ def state_message_received(msg):
@callback
@log_messages(self.hass, self.entity_id)
def position_message_received(msg):
"""Handle new MQTT state messages."""
"""Handle new MQTT position messages."""
payload = msg.payload
template = self._config.get(CONF_VALUE_TEMPLATE)

template = self._config.get(CONF_GET_POSITION_TEMPLATE)

# To be removed in 2021.6:
# allow using `value_template` as position template if no `state_topic`
if template is None and self._config.get(CONF_STATE_TOPIC) is None:
template = self._config.get(CONF_VALUE_TEMPLATE)

if template is not None:
payload = template.async_render_with_possible_json_value(payload)

Expand All @@ -297,13 +347,14 @@ def position_message_received(msg):
float(payload), COVER_PAYLOAD
)
self._position = percentage_payload
self._state = (
STATE_CLOSED
if percentage_payload == DEFAULT_POSITION_CLOSED
else STATE_OPEN
)
if self._config.get(CONF_STATE_TOPIC) is None:
self._state = (
STATE_CLOSED
if percentage_payload == DEFAULT_POSITION_CLOSED
else STATE_OPEN
)
else:
_LOGGER.warning("Payload is not integer within range: %s", payload)
_LOGGER.warning("Payload '%s' is not numeric", payload)
return
self.async_write_ha_state()

Expand All @@ -313,13 +364,18 @@ def position_message_received(msg):
"msg_callback": position_message_received,
"qos": self._config[CONF_QOS],
}
elif self._config.get(CONF_STATE_TOPIC):

if self._config.get(CONF_STATE_TOPIC):
topics["state_topic"] = {
"topic": self._config.get(CONF_STATE_TOPIC),
"msg_callback": state_message_received,
"qos": self._config[CONF_QOS],
}
else:

if (
self._config.get(CONF_GET_POSITION_TOPIC) is None
and self._config.get(CONF_STATE_TOPIC) is None
):
# Force into optimistic mode.
self._optimistic = True

Expand Down Expand Up @@ -488,28 +544,32 @@ async def async_close_cover_tilt(self, **kwargs):

async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
position = kwargs[ATTR_TILT_POSITION]

# The position needs to be between min and max
level = self.find_in_range_from_percent(position)
set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE)
tilt = kwargs[ATTR_TILT_POSITION]
percentage_tilt = tilt
tilt = self.find_in_range_from_percent(tilt)
if set_tilt_template is not None:
tilt = set_tilt_template.async_render(parse_result=False, **kwargs)

mqtt.async_publish(
self.hass,
self._config.get(CONF_TILT_COMMAND_TOPIC),
level,
tilt,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
)
if self._tilt_optimistic:
self._tilt_value = percentage_tilt
self.async_write_ha_state()

async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
position = kwargs[ATTR_POSITION]
percentage_position = position
position = self.find_in_range_from_percent(position, COVER_PAYLOAD)
if set_position_template is not None:
position = set_position_template.async_render(parse_result=False, **kwargs)
else:
position = self.find_in_range_from_percent(position, COVER_PAYLOAD)

mqtt.async_publish(
self.hass,
Expand Down
Loading

0 comments on commit 81c88cd

Please sign in to comment.