1 """Support for MQTT valve devices."""
3 from __future__
import annotations
5 from contextlib
import suppress
9 import voluptuous
as vol
13 DEVICE_CLASSES_SCHEMA,
31 percentage_to_ranged_value,
32 ranged_value_to_percentage,
35 from .
import subscription
36 from .config
import MQTT_BASE_SCHEMA
38 CONF_COMMAND_TEMPLATE,
52 DEFAULT_PAYLOAD_CLOSE,
54 DEFAULT_POSITION_CLOSED,
55 DEFAULT_POSITION_OPEN,
59 from .entity
import MqttEntity, async_setup_entity_entry_helper
60 from .models
import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
61 from .schemas
import MQTT_ENTITY_COMMON_SCHEMA
62 from .util
import valid_publish_topic, valid_subscribe_topic
64 _LOGGER = logging.getLogger(__name__)
68 CONF_REPORTS_POSITION =
"reports_position"
70 DEFAULT_NAME =
"MQTT Valve"
72 MQTT_VALVE_ATTRIBUTES_BLOCKED = frozenset(
74 valve.ATTR_CURRENT_POSITION,
86 CONF_PAYLOAD_CLOSE: DEFAULT_PAYLOAD_CLOSE,
87 CONF_PAYLOAD_OPEN: DEFAULT_PAYLOAD_OPEN,
88 CONF_STATE_OPEN: ValveState.OPEN,
89 CONF_STATE_CLOSED: ValveState.CLOSED,
92 RESET_CLOSING_OPENING =
"reset_opening_closing"
96 """Validate config options and set defaults."""
97 if config[CONF_REPORTS_POSITION]
and any(key
in config
for key
in NO_POSITION_KEYS):
99 "Options `payload_open`, `payload_close`, `state_open` and "
100 "`state_closed` are not allowed if the valve reports a position."
102 return {**DEFAULTS, **config}
105 _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
107 vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
108 vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
109 vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA,
None),
110 vol.Optional(CONF_NAME): vol.Any(cv.string,
None),
111 vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
112 vol.Optional(CONF_PAYLOAD_CLOSE): vol.Any(cv.string,
None),
113 vol.Optional(CONF_PAYLOAD_OPEN): vol.Any(cv.string,
None),
114 vol.Optional(CONF_PAYLOAD_STOP): vol.Any(cv.string,
None),
115 vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int,
116 vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int,
117 vol.Optional(CONF_REPORTS_POSITION, default=
False): cv.boolean,
118 vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
119 vol.Optional(CONF_STATE_CLOSED): cv.string,
120 vol.Optional(CONF_STATE_CLOSING, default=ValveState.CLOSING): cv.string,
121 vol.Optional(CONF_STATE_OPEN): cv.string,
122 vol.Optional(CONF_STATE_OPENING, default=ValveState.OPENING): cv.string,
123 vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
124 vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
126 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
128 PLATFORM_SCHEMA_MODERN = vol.All(_PLATFORM_SCHEMA_BASE, _validate_and_add_defaults)
130 DISCOVERY_SCHEMA = vol.All(
131 _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
132 _validate_and_add_defaults,
138 config_entry: ConfigEntry,
139 async_add_entities: AddEntitiesCallback,
141 """Set up MQTT valve through YAML and through MQTT discovery."""
149 PLATFORM_SCHEMA_MODERN,
154 """Representation of a valve that can be controlled using MQTT."""
156 _attr_is_closed: bool |
None =
None
157 _attributes_extra_blocked: frozenset[str] = MQTT_VALVE_ATTRIBUTES_BLOCKED
158 _default_name = DEFAULT_NAME
159 _entity_id_format: str = valve.ENTITY_ID_FORMAT
161 _range: tuple[int, int]
162 _tilt_optimistic: bool
166 """Return the config schema."""
167 return DISCOVERY_SCHEMA
170 """Set up valve from config."""
173 self.
_config_config[CONF_POSITION_CLOSED] + 1,
174 self.
_config_config[CONF_POSITION_OPEN],
176 no_state_topic = config.get(CONF_STATE_TOPIC)
is None
177 self.
_optimistic_optimistic = config[CONF_OPTIMISTIC]
or no_state_topic
180 template_config_attributes = {
181 "position_open": config[CONF_POSITION_OPEN],
182 "position_closed": config[CONF_POSITION_CLOSED],
186 config.get(CONF_VALUE_TEMPLATE), entity=self
187 ).async_render_with_possible_json_value
190 config.get(CONF_COMMAND_TEMPLATE), entity=self
194 config.get(CONF_VALUE_TEMPLATE),
196 config_attributes=template_config_attributes,
197 ).async_render_with_possible_json_value
202 if CONF_COMMAND_TOPIC
in config:
203 if config[CONF_PAYLOAD_OPEN]
is not None:
204 supported_features |= ValveEntityFeature.OPEN
205 if config[CONF_PAYLOAD_CLOSE]
is not None:
206 supported_features |= ValveEntityFeature.CLOSE
208 if config[CONF_REPORTS_POSITION]:
209 supported_features |= ValveEntityFeature.SET_POSITION
210 if config.get(CONF_PAYLOAD_STOP)
is not None:
211 supported_features |= ValveEntityFeature.STOP
217 """Update the valve state properties."""
229 self, msg: ReceiveMessage, state_payload: str
231 """Process an update for a valve that does not report the position."""
232 state: str |
None =
None
233 if state_payload == self.
_config_config[CONF_STATE_OPENING]:
234 state = ValveState.OPENING
235 elif state_payload == self.
_config_config[CONF_STATE_CLOSING]:
236 state = ValveState.CLOSING
237 elif state_payload == self.
_config_config[CONF_STATE_OPEN]:
238 state = ValveState.OPEN
239 elif state_payload == self.
_config_config[CONF_STATE_CLOSED]:
240 state = ValveState.CLOSED
241 elif state_payload == PAYLOAD_NONE:
245 "Payload received on topic '%s' is not one of "
246 "[open, closed, opening, closing], got: %s",
255 self, msg: ReceiveMessage, position_payload: str, state_payload: str
257 """Process an update for a valve that reports the position."""
258 state: str |
None =
None
259 position_set: bool =
False
260 if state_payload == self.
_config_config[CONF_STATE_OPENING]:
261 state = ValveState.OPENING
262 elif state_payload == self.
_config_config[CONF_STATE_CLOSING]:
263 state = ValveState.CLOSING
264 elif state_payload == PAYLOAD_NONE:
267 if state
is None or position_payload != state_payload:
274 "Ignoring non numeric payload '%s' received on topic '%s'",
279 percentage_payload =
min(
max(percentage_payload, 0), 100)
282 if state
is None and percentage_payload
in (0, 100):
283 state = RESET_CLOSING_OPENING
285 if state_payload
and state
is None and not position_set:
287 "Payload received on topic '%s' is not one of "
288 "[opening, closing], got: %s",
299 """Handle new MQTT state messages."""
301 payload_dict: Any =
None
302 position_payload: Any = payload
303 state_payload: Any = payload
306 _LOGGER.debug(
"Ignoring empty state message from '%s'", msg.topic)
309 with suppress(*JSON_DECODE_EXCEPTIONS):
311 if isinstance(payload_dict, dict):
312 if self.
reports_positionreports_position
and "position" not in payload_dict:
314 "Missing required `position` attribute in json payload "
315 "on topic '%s', got: %s",
320 if not self.
reports_positionreports_position
and "state" not in payload_dict:
322 "Missing required `state` attribute in json payload "
323 " on topic '%s', got: %s",
328 position_payload = payload_dict.get(
"position")
329 state_payload = payload_dict.get(
"state")
331 if self.
_config_config[CONF_REPORTS_POSITION]:
338 """(Re)Subscribe to topics."""
343 "_attr_current_valve_position",
351 """(Re)Subscribe to topics."""
352 subscription.async_subscribe_topics_internal(self.
hasshasshass, self.
_sub_state_sub_state)
355 """Move the valve up.
357 This method is a coroutine.
360 self.
_config_config.
get(CONF_PAYLOAD_OPEN, DEFAULT_PAYLOAD_OPEN)
369 """Move the valve down.
371 This method is a coroutine.
374 self.
_config_config.
get(CONF_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_CLOSE)
383 """Stop valve positioning.
385 This method is a coroutine.
391 """Move the valve to a specific position."""
392 percentage_position = position
393 scaled_position = round(
397 "position": percentage_position,
398 "position_open": self.
_config_config[CONF_POSITION_OPEN],
399 "position_closed": self.
_config_config[CONF_POSITION_CLOSED],
401 rendered_position = self.
_command_template_command_template(scaled_position, variables=variables)
403 self.
_config_config[CONF_COMMAND_TOPIC], rendered_position
408 if percentage_position == self.
_config_config[CONF_POSITION_CLOSED]
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 _process_position_valve_update(self, ReceiveMessage msg, str position_payload, str state_payload)
None _subscribe_topics(self)
None _state_message_received(self, ReceiveMessage msg)
None _process_binary_valve_update(self, ReceiveMessage msg, str state_payload)
None async_close_valve(self)
None async_stop_valve(self)
_attr_current_valve_position
None async_open_valve(self)
None _update_state(self, str|None state)
None _setup_from_config(self, ConfigType config)
None _prepare_subscribe_topics(self)
VolSchemaType config_schema()
None async_set_valve_position(self, int position)
bool reports_position(self)
None async_write_ha_state(self)
web.Response get(self, web.Request request, str config_key)
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)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
ConfigType _validate_and_add_defaults(ConfigType config)
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)