1 """Support for MQTT cover devices."""
3 from __future__
import annotations
5 from contextlib
import suppress
9 import voluptuous
as vol
15 DEVICE_CLASSES_SCHEMA,
38 percentage_to_ranged_value,
39 ranged_value_to_percentage,
42 from .
import subscription
43 from .config
import MQTT_BASE_SCHEMA
58 DEFAULT_PAYLOAD_CLOSE,
60 DEFAULT_POSITION_CLOSED,
61 DEFAULT_POSITION_OPEN,
65 from .entity
import MqttEntity, async_setup_entity_entry_helper
66 from .models
import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
67 from .schemas
import MQTT_ENTITY_COMMON_SCHEMA
68 from .util
import valid_publish_topic, valid_subscribe_topic
70 _LOGGER = logging.getLogger(__name__)
74 CONF_GET_POSITION_TOPIC =
"position_topic"
75 CONF_GET_POSITION_TEMPLATE =
"position_template"
76 CONF_SET_POSITION_TOPIC =
"set_position_topic"
77 CONF_SET_POSITION_TEMPLATE =
"set_position_template"
78 CONF_TILT_COMMAND_TOPIC =
"tilt_command_topic"
79 CONF_TILT_COMMAND_TEMPLATE =
"tilt_command_template"
80 CONF_TILT_STATUS_TOPIC =
"tilt_status_topic"
81 CONF_TILT_STATUS_TEMPLATE =
"tilt_status_template"
83 CONF_STATE_STOPPED =
"state_stopped"
84 CONF_TILT_CLOSED_POSITION =
"tilt_closed_value"
85 CONF_TILT_MAX =
"tilt_max"
86 CONF_TILT_MIN =
"tilt_min"
87 CONF_TILT_OPEN_POSITION =
"tilt_opened_value"
88 CONF_TILT_STATE_OPTIMISTIC =
"tilt_optimistic"
91 COVER_PAYLOAD =
"cover"
93 DEFAULT_NAME =
"MQTT Cover"
95 DEFAULT_STATE_STOPPED =
"stopped"
96 DEFAULT_PAYLOAD_STOP =
"STOP"
98 DEFAULT_TILT_CLOSED_POSITION = 0
99 DEFAULT_TILT_MAX = 100
101 DEFAULT_TILT_OPEN_POSITION = 100
102 DEFAULT_TILT_OPTIMISTIC =
False
105 CoverEntityFeature.OPEN_TILT
106 | CoverEntityFeature.CLOSE_TILT
107 | CoverEntityFeature.STOP_TILT
108 | CoverEntityFeature.SET_TILT_POSITION
111 MQTT_COVER_ATTRIBUTES_BLOCKED = frozenset(
113 cover.ATTR_CURRENT_POSITION,
114 cover.ATTR_CURRENT_TILT_POSITION,
122 If set position topic is set then get position topic is set as well.
124 if CONF_SET_POSITION_TOPIC
in config
and CONF_GET_POSITION_TOPIC
not in config:
126 f
"'{CONF_SET_POSITION_TOPIC}' must be set together with"
127 f
" '{CONF_GET_POSITION_TOPIC}'."
132 if CONF_VALUE_TEMPLATE
in config
and CONF_STATE_TOPIC
not in config:
134 f
"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'."
137 if CONF_GET_POSITION_TEMPLATE
in config
and CONF_GET_POSITION_TOPIC
not in config:
139 f
"'{CONF_GET_POSITION_TEMPLATE}' must be set together with"
140 f
" '{CONF_GET_POSITION_TOPIC}'."
143 if CONF_SET_POSITION_TEMPLATE
in config
and CONF_SET_POSITION_TOPIC
not in config:
145 f
"'{CONF_SET_POSITION_TEMPLATE}' must be set together with"
146 f
" '{CONF_SET_POSITION_TOPIC}'."
149 if CONF_TILT_COMMAND_TEMPLATE
in config
and CONF_TILT_COMMAND_TOPIC
not in config:
151 f
"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with"
152 f
" '{CONF_TILT_COMMAND_TOPIC}'."
155 if CONF_TILT_STATUS_TEMPLATE
in config
and CONF_TILT_STATUS_TOPIC
not in config:
157 f
"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with"
158 f
" '{CONF_TILT_STATUS_TOPIC}'."
164 _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
166 vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
167 vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA,
None),
168 vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic,
169 vol.Optional(CONF_NAME): vol.Any(cv.string,
None),
170 vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
171 vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any(
174 vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): vol.Any(
177 vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any(
180 vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int,
181 vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int,
182 vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
183 vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template,
184 vol.Optional(CONF_SET_POSITION_TOPIC): valid_publish_topic,
185 vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
186 vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string,
187 vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
188 vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string,
189 vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string,
190 vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
192 CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION
194 vol.Optional(CONF_TILT_COMMAND_TOPIC): valid_publish_topic,
195 vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int,
196 vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int,
197 vol.Optional(CONF_TILT_OPEN_POSITION, default=DEFAULT_TILT_OPEN_POSITION): int,
199 CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC
201 vol.Optional(CONF_TILT_STATUS_TOPIC): valid_subscribe_topic,
202 vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template,
203 vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
204 vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template,
205 vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template,
207 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
209 PLATFORM_SCHEMA_MODERN = vol.All(
210 _PLATFORM_SCHEMA_BASE,
214 DISCOVERY_SCHEMA = vol.All(
215 _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
222 config_entry: ConfigEntry,
223 async_add_entities: AddEntitiesCallback,
225 """Set up MQTT cover through YAML and through MQTT discovery."""
233 PLATFORM_SCHEMA_MODERN,
238 """Representation of a cover that can be controlled using MQTT."""
240 _attr_is_closed: bool |
None =
None
241 _attributes_extra_blocked: frozenset[str] = MQTT_COVER_ATTRIBUTES_BLOCKED
242 _default_name = DEFAULT_NAME
243 _entity_id_format: str = cover.ENTITY_ID_FORMAT
245 _tilt_optimistic: bool
246 _tilt_closed_percentage: int
247 _tilt_open_percentage: int
248 _pos_range: tuple[int, int]
249 _tilt_range: tuple[int, int]
253 """Return the config schema."""
254 return DISCOVERY_SCHEMA
257 """Set up cover from config."""
258 self.
_pos_range_pos_range = (config[CONF_POSITION_CLOSED] + 1, config[CONF_POSITION_OPEN])
259 self.
_tilt_range_tilt_range = (config[CONF_TILT_MIN] + 1, config[CONF_TILT_MAX])
261 self.
_tilt_range_tilt_range, config[CONF_TILT_CLOSED_POSITION]
264 self.
_tilt_range_tilt_range, config[CONF_TILT_OPEN_POSITION]
267 config.get(CONF_SET_POSITION_TOPIC)
is None
268 and config.get(CONF_GET_POSITION_TOPIC)
is None
271 config.get(CONF_COMMAND_TOPIC)
is None
272 and config.get(CONF_STATE_TOPIC)
is None
275 config.get(CONF_TILT_COMMAND_TOPIC)
is None
276 and config.get(CONF_TILT_STATUS_TOPIC)
is None
278 optimistic_position = (
279 config.get(CONF_SET_POSITION_TOPIC)
is not None
280 and config.get(CONF_GET_POSITION_TOPIC)
is None
283 config.get(CONF_COMMAND_TOPIC)
is not None
284 and config.get(CONF_STATE_TOPIC)
is None
287 config.get(CONF_TILT_COMMAND_TOPIC)
is not None
288 and config.get(CONF_TILT_STATUS_TOPIC)
is None
292 (no_position
or optimistic_position)
293 and (no_state
or optimistic_state)
294 and (no_tilt
or optimistic_tilt)
299 config[CONF_TILT_STATE_OPTIMISTIC]
300 or config.get(CONF_TILT_STATUS_TOPIC)
is None
303 template_config_attributes = {
304 "position_open": config[CONF_POSITION_OPEN],
305 "position_closed": config[CONF_POSITION_CLOSED],
306 "tilt_min": config[CONF_TILT_MIN],
307 "tilt_max": config[CONF_TILT_MAX],
311 config.get(CONF_VALUE_TEMPLATE), entity=self
312 ).async_render_with_possible_json_value
315 config.get(CONF_SET_POSITION_TEMPLATE), entity=self
319 config.get(CONF_GET_POSITION_TEMPLATE),
321 config_attributes=template_config_attributes,
322 ).async_render_with_possible_json_value
325 self.
_config_config.
get(CONF_TILT_COMMAND_TEMPLATE), entity=self
329 self.
_config_config.
get(CONF_TILT_STATUS_TEMPLATE),
331 config_attributes=template_config_attributes,
332 ).async_render_with_possible_json_value
337 if self.
_config_config.
get(CONF_COMMAND_TOPIC)
is not None:
338 if self.
_config_config.
get(CONF_PAYLOAD_OPEN)
is not None:
339 supported_features |= CoverEntityFeature.OPEN
340 if self.
_config_config.
get(CONF_PAYLOAD_CLOSE)
is not None:
341 supported_features |= CoverEntityFeature.CLOSE
342 if self.
_config_config.
get(CONF_PAYLOAD_STOP)
is not None:
343 supported_features |= CoverEntityFeature.STOP
345 if self.
_config_config.
get(CONF_SET_POSITION_TOPIC)
is not None:
346 supported_features |= CoverEntityFeature.SET_POSITION
348 if self.
_config_config.
get(CONF_TILT_COMMAND_TOPIC)
is not None:
349 supported_features |= TILT_FEATURES
355 """Update the cover state."""
366 """Handle tilt updates."""
370 _LOGGER.debug(
"Ignoring empty tilt message from '%s'", msg.topic)
377 """Handle new MQTT state messages."""
381 _LOGGER.debug(
"Ignoring empty state message from '%s'", msg.topic)
385 if payload == self.
_config_config[CONF_STATE_STOPPED]:
386 if self.
_config_config.
get(CONF_GET_POSITION_TOPIC)
is not None:
395 if self.
statestatestate
in [CoverState.CLOSED, CoverState.CLOSING]
398 elif payload == self.
_config_config[CONF_STATE_OPENING]:
399 state = CoverState.OPENING
400 elif payload == self.
_config_config[CONF_STATE_CLOSING]:
401 state = CoverState.CLOSING
402 elif payload == self.
_config_config[CONF_STATE_OPEN]:
403 state = CoverState.OPEN
404 elif payload == self.
_config_config[CONF_STATE_CLOSED]:
405 state = CoverState.CLOSED
406 elif payload == PAYLOAD_NONE:
411 "Payload is not supported (e.g. open, closed, opening, closing,"
421 """Handle new MQTT position messages."""
423 payload_dict: Any =
None
426 _LOGGER.debug(
"Ignoring empty position message from '%s'", msg.topic)
429 with suppress(*JSON_DECODE_EXCEPTIONS):
432 if payload_dict
and isinstance(payload_dict, dict):
433 if "position" not in payload_dict:
435 "Template (position_template) returned JSON without position"
439 if "tilt_position" in payload_dict:
440 if not self.
_config_config.
get(CONF_TILT_STATE_OPTIMISTIC):
444 payload = payload_dict[
"position"]
451 _LOGGER.warning(
"Payload '%s' is not numeric", payload)
455 if self.
_config_config.
get(CONF_STATE_TOPIC)
is None:
464 """(Re)Subscribe to topics."""
466 CONF_GET_POSITION_TOPIC,
469 "_attr_current_cover_position",
470 "_attr_current_cover_tilt_position",
479 {
"_attr_is_closed",
"_attr_is_closing",
"_attr_is_opening"},
482 CONF_TILT_STATUS_TOPIC,
484 {
"_attr_current_cover_tilt_position"},
488 """(Re)Subscribe to topics."""
489 subscription.async_subscribe_topics_internal(self.
hasshasshass, self.
_sub_state_sub_state)
492 """Move the cover up.
494 This method is a coroutine.
497 self.
_config_config[CONF_COMMAND_TOPIC], self.
_config_config[CONF_PAYLOAD_OPEN]
502 if self.
_config_config.
get(CONF_GET_POSITION_TOPIC):
507 """Move the cover down.
509 This method is a coroutine.
512 self.
_config_config[CONF_COMMAND_TOPIC], self.
_config_config[CONF_PAYLOAD_CLOSE]
517 if self.
_config_config.
get(CONF_GET_POSITION_TOPIC):
524 This method is a coroutine.
527 self.
_config_config[CONF_COMMAND_TOPIC], self.
_config_config[CONF_PAYLOAD_STOP]
531 """Tilt the cover open."""
532 tilt_open_position = self.
_config_config[CONF_TILT_OPEN_POSITION]
534 "tilt_position": tilt_open_position,
536 "position_open": self.
_config_config.
get(CONF_POSITION_OPEN),
537 "position_closed": self.
_config_config.
get(CONF_POSITION_CLOSED),
538 "tilt_min": self.
_config_config.
get(CONF_TILT_MIN),
539 "tilt_max": self.
_config_config.
get(CONF_TILT_MAX),
541 tilt_payload = self.
_set_tilt_template_set_tilt_template(tilt_open_position, variables=variables)
543 self.
_config_config[CONF_TILT_COMMAND_TOPIC], tilt_payload
550 """Tilt the cover closed."""
551 tilt_closed_position = self.
_config_config[CONF_TILT_CLOSED_POSITION]
553 "tilt_position": tilt_closed_position,
555 "position_open": self.
_config_config.
get(CONF_POSITION_OPEN),
556 "position_closed": self.
_config_config.
get(CONF_POSITION_CLOSED),
557 "tilt_min": self.
_config_config.
get(CONF_TILT_MIN),
558 "tilt_max": self.
_config_config.
get(CONF_TILT_MAX),
561 tilt_closed_position, variables=variables
564 self.
_config_config[CONF_TILT_COMMAND_TOPIC], tilt_payload
571 """Move the cover tilt to a specific position."""
572 tilt_percentage = kwargs[ATTR_TILT_POSITION]
579 "tilt_position": tilt_percentage,
581 "position_open": self.
_config_config.
get(CONF_POSITION_OPEN),
582 "position_closed": self.
_config_config.
get(CONF_POSITION_CLOSED),
583 "tilt_min": self.
_config_config.
get(CONF_TILT_MIN),
584 "tilt_max": self.
_config_config.
get(CONF_TILT_MAX),
586 tilt_rendered = self.
_set_tilt_template_set_tilt_template(tilt_ranged, variables=variables)
588 self.
_config_config[CONF_TILT_COMMAND_TOPIC], tilt_rendered
591 _LOGGER.debug(
"Set tilt value optimistic")
596 """Move the cover to a specific position."""
597 position_percentage = kwargs[ATTR_POSITION]
598 position_ranged = round(
602 "position": position_percentage,
604 "position_open": self.
_config_config[CONF_POSITION_OPEN],
605 "position_closed": self.
_config_config[CONF_POSITION_CLOSED],
606 "tilt_min": self.
_config_config[CONF_TILT_MIN],
607 "tilt_max": self.
_config_config[CONF_TILT_MAX],
610 position_ranged, variables=variables
613 self.
_config_config[CONF_SET_POSITION_TOPIC], position_rendered
618 if position_percentage <= self.
_config_config[CONF_POSITION_CLOSED]
625 """Toggle the entity."""
636 """Set the tilt value."""
639 payload = round(
float(_payload))
641 _LOGGER.warning(
"Payload '%s' is not numeric", _payload)
645 self.
_config_config[CONF_TILT_MIN] <= payload <= self.
_config_config[CONF_TILT_MAX]
646 or self.
_config_config[CONF_TILT_MAX] <= payload <= self.
_config_config[CONF_TILT_MIN]
652 "Payload '%s' is out of range, must be between '%s' and '%s' inclusive",
654 self.
_config_config[CONF_TILT_MIN],
655 self.
_config_config[CONF_TILT_MAX],
int|None current_cover_tilt_position(self)
current_cover_tilt_position
None async_open_cover_tilt(self, **Any kwargs)
int|None current_cover_position(self)
None async_close_cover_tilt(self, **Any kwargs)
None tilt_payload_received(self, Any _payload)
None _subscribe_topics(self)
None _position_message_received(self, ReceiveMessage msg)
VolSchemaType config_schema()
_attr_current_cover_position
None async_close_cover(self, **Any kwargs)
None _update_state(self, str|None state)
None async_toggle_tilt(self, **Any kwargs)
_attr_current_cover_tilt_position
None async_close_cover_tilt(self, **Any kwargs)
None async_open_cover(self, **Any kwargs)
None async_stop_cover(self, **Any kwargs)
None _prepare_subscribe_topics(self)
None _tilt_message_received(self, ReceiveMessage msg)
None _setup_from_config(self, ConfigType config)
None async_open_cover_tilt(self, **Any kwargs)
None async_set_cover_tilt_position(self, **Any kwargs)
None async_set_cover_position(self, **Any kwargs)
None _state_message_received(self, ReceiveMessage msg)
None async_publish_with_config(self, str topic, PublishPayloadType payload)
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
None async_write_ha_state(self)
web.Response get(self, web.Request request, str config_key)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
ConfigType validate_options(ConfigType config)
None async_setup_entity_entry_helper(HomeAssistant hass, ConfigEntry entry, type[MqttEntity]|None entity_class, str domain, AddEntitiesCallback async_add_entities, VolSchemaType discovery_schema, VolSchemaType platform_schema_modern, dict[str, type[MqttEntity]]|None schema_class_mapping=None)
float percentage_to_ranged_value(tuple[float, float] low_high_range, float percentage)
int ranged_value_to_percentage(tuple[float, float] low_high_range, float value)