1 """Support for MQTT fans."""
3 from __future__
import annotations
5 from collections.abc
import Callable
10 import voluptuous
as vol
36 percentage_to_ranged_value,
37 ranged_value_to_percentage,
41 from .
import subscription
42 from .config
import MQTT_RW_SCHEMA
44 CONF_COMMAND_TEMPLATE,
47 CONF_STATE_VALUE_TEMPLATE,
50 from .entity
import MqttEntity, async_setup_entity_entry_helper
57 from .schemas
import MQTT_ENTITY_COMMON_SCHEMA
58 from .util
import valid_publish_topic, valid_subscribe_topic
62 CONF_DIRECTION_STATE_TOPIC =
"direction_state_topic"
63 CONF_DIRECTION_COMMAND_TOPIC =
"direction_command_topic"
64 CONF_DIRECTION_VALUE_TEMPLATE =
"direction_value_template"
65 CONF_DIRECTION_COMMAND_TEMPLATE =
"direction_command_template"
66 CONF_PERCENTAGE_STATE_TOPIC =
"percentage_state_topic"
67 CONF_PERCENTAGE_COMMAND_TOPIC =
"percentage_command_topic"
68 CONF_PERCENTAGE_VALUE_TEMPLATE =
"percentage_value_template"
69 CONF_PERCENTAGE_COMMAND_TEMPLATE =
"percentage_command_template"
70 CONF_PAYLOAD_RESET_PERCENTAGE =
"payload_reset_percentage"
71 CONF_SPEED_RANGE_MIN =
"speed_range_min"
72 CONF_SPEED_RANGE_MAX =
"speed_range_max"
73 CONF_PRESET_MODE_STATE_TOPIC =
"preset_mode_state_topic"
74 CONF_PRESET_MODE_COMMAND_TOPIC =
"preset_mode_command_topic"
75 CONF_PRESET_MODE_VALUE_TEMPLATE =
"preset_mode_value_template"
76 CONF_PRESET_MODE_COMMAND_TEMPLATE =
"preset_mode_command_template"
77 CONF_PRESET_MODES_LIST =
"preset_modes"
78 CONF_PAYLOAD_RESET_PRESET_MODE =
"payload_reset_preset_mode"
79 CONF_OSCILLATION_STATE_TOPIC =
"oscillation_state_topic"
80 CONF_OSCILLATION_COMMAND_TOPIC =
"oscillation_command_topic"
81 CONF_OSCILLATION_VALUE_TEMPLATE =
"oscillation_value_template"
82 CONF_OSCILLATION_COMMAND_TEMPLATE =
"oscillation_command_template"
83 CONF_PAYLOAD_OSCILLATION_ON =
"payload_oscillation_on"
84 CONF_PAYLOAD_OSCILLATION_OFF =
"payload_oscillation_off"
86 DEFAULT_NAME =
"MQTT Fan"
87 DEFAULT_PAYLOAD_ON =
"ON"
88 DEFAULT_PAYLOAD_OFF =
"OFF"
89 DEFAULT_PAYLOAD_RESET =
"None"
90 DEFAULT_SPEED_RANGE_MIN = 1
91 DEFAULT_SPEED_RANGE_MAX = 100
93 OSCILLATE_ON_PAYLOAD =
"oscillate_on"
94 OSCILLATE_OFF_PAYLOAD =
"oscillate_off"
96 MQTT_FAN_ATTRIBUTES_BLOCKED = frozenset(
100 fan.ATTR_PERCENTAGE_STEP,
102 fan.ATTR_PRESET_MODE,
103 fan.ATTR_PRESET_MODES,
107 _LOGGER = logging.getLogger(__name__)
111 """Validate that the fan speed_range configuration is valid, throws if it isn't."""
112 if config[CONF_SPEED_RANGE_MIN] == 0:
113 raise vol.Invalid(
"speed_range_min must be > 0")
114 if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]:
115 raise vol.Invalid(
"speed_range_max must be > speed_range_min")
120 """Validate that the preset mode reset payload is not one of the preset modes."""
121 if config[CONF_PAYLOAD_RESET_PRESET_MODE]
in config[CONF_PRESET_MODES_LIST]:
122 raise vol.Invalid(
"preset_modes must not contain payload_reset_preset_mode")
126 _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
128 vol.Optional(CONF_NAME): vol.Any(cv.string,
None),
129 vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
130 vol.Optional(CONF_DIRECTION_COMMAND_TOPIC): valid_publish_topic,
131 vol.Optional(CONF_DIRECTION_COMMAND_TEMPLATE): cv.template,
132 vol.Optional(CONF_DIRECTION_STATE_TOPIC): valid_subscribe_topic,
133 vol.Optional(CONF_DIRECTION_VALUE_TEMPLATE): cv.template,
134 vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): valid_publish_topic,
135 vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template,
136 vol.Optional(CONF_OSCILLATION_STATE_TOPIC): valid_subscribe_topic,
137 vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template,
138 vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): valid_publish_topic,
139 vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template,
140 vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): valid_subscribe_topic,
141 vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template,
145 CONF_PRESET_MODE_COMMAND_TOPIC,
"preset_modes"
146 ): valid_publish_topic,
148 CONF_PRESET_MODES_LIST,
"preset_modes", default=[]
150 vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template,
151 vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): valid_subscribe_topic,
152 vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template,
154 CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN
157 CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX
160 CONF_PAYLOAD_RESET_PERCENTAGE, default=DEFAULT_PAYLOAD_RESET
163 CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET
165 vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
166 vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
168 CONF_PAYLOAD_OSCILLATION_OFF, default=OSCILLATE_OFF_PAYLOAD
171 CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD
173 vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
175 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
177 PLATFORM_SCHEMA_MODERN = vol.All(
178 _PLATFORM_SCHEMA_BASE,
179 valid_speed_range_configuration,
180 valid_preset_mode_configuration,
183 DISCOVERY_SCHEMA = vol.All(
184 _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
185 valid_speed_range_configuration,
186 valid_preset_mode_configuration,
192 config_entry: ConfigEntry,
193 async_add_entities: AddEntitiesCallback,
195 """Set up MQTT fan through YAML and through MQTT discovery."""
203 PLATFORM_SCHEMA_MODERN,
208 """A MQTT fan component."""
210 _attr_percentage: int |
None =
None
211 _attr_preset_mode: str |
None =
None
213 _default_name = DEFAULT_NAME
214 _entity_id_format = fan.ENTITY_ID_FORMAT
215 _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED
217 _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]]
218 _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]]
219 _feature_percentage: bool
220 _feature_preset_mode: bool
221 _topic: dict[str, Any]
223 _optimistic_direction: bool
224 _optimistic_oscillation: bool
225 _optimistic_percentage: bool
226 _optimistic_preset_mode: bool
227 _payload: dict[str, Any]
228 _speed_range: tuple[int, int]
229 _enable_turn_on_off_backwards_compatibility =
False
233 """Return the config schema."""
234 return DISCOVERY_SCHEMA
237 """(Re)Setup the entity."""
239 config[CONF_SPEED_RANGE_MIN],
240 config[CONF_SPEED_RANGE_MAX],
247 CONF_DIRECTION_STATE_TOPIC,
248 CONF_DIRECTION_COMMAND_TOPIC,
249 CONF_PERCENTAGE_STATE_TOPIC,
250 CONF_PERCENTAGE_COMMAND_TOPIC,
251 CONF_PRESET_MODE_STATE_TOPIC,
252 CONF_PRESET_MODE_COMMAND_TOPIC,
253 CONF_OSCILLATION_STATE_TOPIC,
254 CONF_OSCILLATION_COMMAND_TOPIC,
258 "STATE_ON": config[CONF_PAYLOAD_ON],
259 "STATE_OFF": config[CONF_PAYLOAD_OFF],
260 "OSCILLATE_ON_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_ON],
261 "OSCILLATE_OFF_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_OFF],
262 "PERCENTAGE_RESET": config[CONF_PAYLOAD_RESET_PERCENTAGE],
263 "PRESET_MODE_RESET": config[CONF_PAYLOAD_RESET_PRESET_MODE],
279 optimistic = config[CONF_OPTIMISTIC]
283 optimistic
or self.
_topic_topic[CONF_DIRECTION_STATE_TOPIC]
is None
286 optimistic
or self.
_topic_topic[CONF_OSCILLATION_STATE_TOPIC]
is None
289 optimistic
or self.
_topic_topic[CONF_PERCENTAGE_STATE_TOPIC]
is None
292 optimistic
or self.
_topic_topic[CONF_PRESET_MODE_STATE_TOPIC]
is None
296 FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
299 self.
_topic_topic[CONF_OSCILLATION_COMMAND_TOPIC]
is not None
300 and FanEntityFeature.OSCILLATE
303 self.
_topic_topic[CONF_DIRECTION_COMMAND_TOPIC]
is not None
304 and FanEntityFeature.DIRECTION
311 command_templates: dict[str, Template |
None] = {
312 CONF_STATE: config.get(CONF_COMMAND_TEMPLATE),
313 ATTR_DIRECTION: config.get(CONF_DIRECTION_COMMAND_TEMPLATE),
314 ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE),
315 ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE),
316 ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE),
320 for key, tpl
in command_templates.items()
323 value_templates: dict[str, Template |
None] = {
324 CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
325 ATTR_DIRECTION: config.get(CONF_DIRECTION_VALUE_TEMPLATE),
326 ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE),
327 ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE),
328 ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE),
333 ).async_render_with_possible_json_value
334 for key, tpl
in value_templates.items()
339 """Handle new received MQTT message."""
342 _LOGGER.debug(
"Ignoring empty state from '%s'", msg.topic)
344 if payload == self.
_payload_payload[
"STATE_ON"]:
346 elif payload == self.
_payload_payload[
"STATE_OFF"]:
348 elif payload == PAYLOAD_NONE:
353 """Handle new received MQTT message for the percentage."""
354 rendered_percentage_payload = self.
_value_templates_value_templates[ATTR_PERCENTAGE](
357 if not rendered_percentage_payload:
358 _LOGGER.debug(
"Ignoring empty speed from '%s'", msg.topic)
360 if rendered_percentage_payload == self.
_payload_payload[
"PERCENTAGE_RESET"]:
370 "'%s' received on topic %s. '%s' is not a valid speed within"
375 rendered_percentage_payload,
378 if percentage < 0
or percentage > 100:
381 "'%s' received on topic %s. '%s' is not a valid speed within"
386 rendered_percentage_payload,
393 """Handle new received MQTT message for preset mode."""
395 if preset_mode == self.
_payload_payload[
"PRESET_MODE_RESET"]:
399 _LOGGER.debug(
"Ignoring empty preset_mode from '%s'", msg.topic)
403 "'%s' received on topic %s. '%s' is not a valid preset mode",
414 """Handle new received MQTT message for the oscillation."""
415 payload = self.
_value_templates_value_templates[ATTR_OSCILLATING](msg.payload)
417 _LOGGER.debug(
"Ignoring empty oscillation from '%s'", msg.topic)
419 if payload == self.
_payload_payload[
"OSCILLATE_ON_PAYLOAD"]:
421 elif payload == self.
_payload_payload[
"OSCILLATE_OFF_PAYLOAD"]:
426 """Handle new received MQTT message for the direction."""
427 direction = self.
_value_templates_value_templates[ATTR_DIRECTION](msg.payload)
429 _LOGGER.debug(
"Ignoring empty direction from '%s'", msg.topic)
435 """(Re)Subscribe to topics."""
438 CONF_PERCENTAGE_STATE_TOPIC, self.
_percentage_received_percentage_received, {
"_attr_percentage"}
441 CONF_PRESET_MODE_STATE_TOPIC,
443 {
"_attr_preset_mode"},
446 CONF_OSCILLATION_STATE_TOPIC,
448 {
"_attr_oscillating"},
452 CONF_DIRECTION_STATE_TOPIC,
454 {
"_attr_current_direction"},
458 """(Re)Subscribe to topics."""
459 subscription.async_subscribe_topics_internal(self.
hasshasshass, self.
_sub_state_sub_state)
463 """Return true if device is on."""
469 percentage: int |
None =
None,
470 preset_mode: str |
None =
None,
473 """Turn on the entity.
475 This method is a coroutine.
479 self.
_config_config[CONF_COMMAND_TOPIC], mqtt_payload
490 """Turn off the entity.
492 This method is a coroutine.
496 self.
_config_config[CONF_COMMAND_TOPIC], mqtt_payload
503 """Set the percentage of the fan.
505 This method is a coroutine.
507 percentage_payload = math.ceil(
510 mqtt_payload = self.
_command_templates_command_templates[ATTR_PERCENTAGE](percentage_payload)
512 self.
_config_config[CONF_PERCENTAGE_COMMAND_TOPIC], mqtt_payload
519 """Set the preset mode of the fan.
521 This method is a coroutine.
523 mqtt_payload = self.
_command_templates_command_templates[ATTR_PRESET_MODE](preset_mode)
525 self.
_config_config[CONF_PRESET_MODE_COMMAND_TOPIC], mqtt_payload
534 This method is a coroutine.
538 self.
_payload_payload[
"OSCILLATE_ON_PAYLOAD"]
542 self.
_payload_payload[
"OSCILLATE_OFF_PAYLOAD"]
545 self.
_config_config[CONF_OSCILLATION_COMMAND_TOPIC], mqtt_payload
554 This method is a coroutine.
558 self.
_config_config[CONF_DIRECTION_COMMAND_TOPIC], mqtt_payload
list[str]|None preset_modes(self)
None async_set_percentage(self, int percentage)
None async_set_preset_mode(self, str preset_mode)
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_turn_off(self, **Any kwargs)
None _subscribe_topics(self)
None async_oscillate(self, bool oscillating)
None _setup_from_config(self, ConfigType config)
None async_set_direction(self, str direction)
None async_set_preset_mode(self, str preset_mode)
None _prepare_subscribe_topics(self)
None _oscillation_received(self, ReceiveMessage msg)
None async_turn_on(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
None _percentage_received(self, ReceiveMessage msg)
None _direction_received(self, ReceiveMessage msg)
VolSchemaType config_schema()
None _preset_mode_received(self, ReceiveMessage msg)
None _state_received(self, ReceiveMessage msg)
None async_set_percentage(self, int percentage)
None async_write_ha_state(self)
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)
ConfigType valid_speed_range_configuration(ConfigType config)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
ConfigType valid_preset_mode_configuration(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)
int int_states_in_range(tuple[float, float] low_high_range)