Home Assistant Unofficial Reference 2024.12.1
vacuum.py
Go to the documentation of this file.
1 """Support for MQTT vacuums."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, cast
7 
8 import voluptuous as vol
9 
10 from homeassistant.components import vacuum
12  ENTITY_ID_FORMAT,
13  STATE_CLEANING,
14  STATE_DOCKED,
15  STATE_ERROR,
16  STATE_RETURNING,
17  StateVacuumEntity,
18  VacuumEntityFeature,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import (
22  ATTR_SUPPORTED_FEATURES,
23  CONF_NAME,
24  STATE_IDLE,
25  STATE_PAUSED,
26 )
27 from homeassistant.core import HomeAssistant, callback
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 from homeassistant.helpers.json import json_dumps
31 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType
32 from homeassistant.util.json import json_loads_object
33 
34 from . import subscription
35 from .config import MQTT_BASE_SCHEMA
36 from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
37 from .entity import MqttEntity, async_setup_entity_entry_helper
38 from .models import ReceiveMessage
39 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
40 from .util import valid_publish_topic
41 
42 PARALLEL_UPDATES = 0
43 
44 BATTERY = "battery_level"
45 FAN_SPEED = "fan_speed"
46 STATE = "state"
47 
48 POSSIBLE_STATES: dict[str, str] = {
49  STATE_IDLE: STATE_IDLE,
50  STATE_DOCKED: STATE_DOCKED,
51  STATE_ERROR: STATE_ERROR,
52  STATE_PAUSED: STATE_PAUSED,
53  STATE_RETURNING: STATE_RETURNING,
54  STATE_CLEANING: STATE_CLEANING,
55 }
56 
57 CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
58 CONF_PAYLOAD_TURN_ON = "payload_turn_on"
59 CONF_PAYLOAD_TURN_OFF = "payload_turn_off"
60 CONF_PAYLOAD_RETURN_TO_BASE = "payload_return_to_base"
61 CONF_PAYLOAD_STOP = "payload_stop"
62 CONF_PAYLOAD_CLEAN_SPOT = "payload_clean_spot"
63 CONF_PAYLOAD_LOCATE = "payload_locate"
64 CONF_PAYLOAD_START = "payload_start"
65 CONF_PAYLOAD_PAUSE = "payload_pause"
66 CONF_SET_FAN_SPEED_TOPIC = "set_fan_speed_topic"
67 CONF_FAN_SPEED_LIST = "fan_speed_list"
68 CONF_SEND_COMMAND_TOPIC = "send_command_topic"
69 
70 DEFAULT_NAME = "MQTT State Vacuum"
71 DEFAULT_RETAIN = False
72 
73 DEFAULT_PAYLOAD_RETURN_TO_BASE = "return_to_base"
74 DEFAULT_PAYLOAD_STOP = "stop"
75 DEFAULT_PAYLOAD_CLEAN_SPOT = "clean_spot"
76 DEFAULT_PAYLOAD_LOCATE = "locate"
77 DEFAULT_PAYLOAD_START = "start"
78 DEFAULT_PAYLOAD_PAUSE = "pause"
79 
80 _LOGGER = logging.getLogger(__name__)
81 
82 SERVICE_TO_STRING: dict[VacuumEntityFeature, str] = {
83  VacuumEntityFeature.START: "start",
84  VacuumEntityFeature.PAUSE: "pause",
85  VacuumEntityFeature.STOP: "stop",
86  VacuumEntityFeature.RETURN_HOME: "return_home",
87  VacuumEntityFeature.FAN_SPEED: "fan_speed",
88  VacuumEntityFeature.BATTERY: "battery",
89  VacuumEntityFeature.STATUS: "status",
90  VacuumEntityFeature.SEND_COMMAND: "send_command",
91  VacuumEntityFeature.LOCATE: "locate",
92  VacuumEntityFeature.CLEAN_SPOT: "clean_spot",
93 }
94 
95 STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()}
96 DEFAULT_SERVICES = (
97  VacuumEntityFeature.START
98  | VacuumEntityFeature.STOP
99  | VacuumEntityFeature.RETURN_HOME
100  | VacuumEntityFeature.BATTERY
101  | VacuumEntityFeature.CLEAN_SPOT
102 )
103 ALL_SERVICES = (
104  DEFAULT_SERVICES
105  | VacuumEntityFeature.PAUSE
106  | VacuumEntityFeature.LOCATE
107  | VacuumEntityFeature.FAN_SPEED
108  | VacuumEntityFeature.SEND_COMMAND
109 )
110 
111 
113  services: VacuumEntityFeature,
114  service_to_string: dict[VacuumEntityFeature, str],
115 ) -> list[str]:
116  """Convert SUPPORT_* service bitmask to list of service strings."""
117  return [
118  service_to_string[service]
119  for service in service_to_string
120  if service & services
121  ]
122 
123 
124 DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES, SERVICE_TO_STRING)
125 
126 _FEATURE_PAYLOADS = {
127  VacuumEntityFeature.START: CONF_PAYLOAD_START,
128  VacuumEntityFeature.STOP: CONF_PAYLOAD_STOP,
129  VacuumEntityFeature.PAUSE: CONF_PAYLOAD_PAUSE,
130  VacuumEntityFeature.CLEAN_SPOT: CONF_PAYLOAD_CLEAN_SPOT,
131  VacuumEntityFeature.LOCATE: CONF_PAYLOAD_LOCATE,
132  VacuumEntityFeature.RETURN_HOME: CONF_PAYLOAD_RETURN_TO_BASE,
133 }
134 
135 MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
136  {
137  vacuum.ATTR_BATTERY_ICON,
138  vacuum.ATTR_BATTERY_LEVEL,
139  vacuum.ATTR_FAN_SPEED,
140  }
141 )
142 
143 MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
144 
145 
146 PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
147  {
148  vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
149  cv.ensure_list, [cv.string]
150  ),
151  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
152  vol.Optional(
153  CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT
154  ): cv.string,
155  vol.Optional(CONF_PAYLOAD_LOCATE, default=DEFAULT_PAYLOAD_LOCATE): cv.string,
156  vol.Optional(
157  CONF_PAYLOAD_RETURN_TO_BASE, default=DEFAULT_PAYLOAD_RETURN_TO_BASE
158  ): cv.string,
159  vol.Optional(CONF_PAYLOAD_START, default=DEFAULT_PAYLOAD_START): cv.string,
160  vol.Optional(CONF_PAYLOAD_PAUSE, default=DEFAULT_PAYLOAD_PAUSE): cv.string,
161  vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
162  vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic,
163  vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic,
164  vol.Optional(CONF_STATE_TOPIC): valid_publish_topic,
165  vol.Optional(CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS): vol.All(
166  cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]
167  ),
168  vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
169  vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
170  }
171 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
172 
173 DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
174 
175 
177  hass: HomeAssistant,
178  config_entry: ConfigEntry,
179  async_add_entities: AddEntitiesCallback,
180 ) -> None:
181  """Set up MQTT vacuum through YAML and through MQTT discovery."""
183  hass,
184  config_entry,
185  MqttStateVacuum,
186  vacuum.DOMAIN,
187  async_add_entities,
188  DISCOVERY_SCHEMA,
189  PLATFORM_SCHEMA_MODERN,
190  )
191 
192 
194  """Representation of a MQTT-controlled state vacuum."""
195 
196  _default_name = DEFAULT_NAME
197  _entity_id_format = ENTITY_ID_FORMAT
198  _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
199 
200  _command_topic: str | None
201  _set_fan_speed_topic: str | None
202  _send_command_topic: str | None
203  _payloads: dict[str, str | None]
204 
205  def __init__(
206  self,
207  hass: HomeAssistant,
208  config: ConfigType,
209  config_entry: ConfigEntry,
210  discovery_data: DiscoveryInfoType | None,
211  ) -> None:
212  """Initialize the vacuum."""
213  self._state_attrs: dict[str, Any] = {}
214 
215  MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
216 
217  @staticmethod
218  def config_schema() -> VolSchemaType:
219  """Return the config schema."""
220  return DISCOVERY_SCHEMA
221 
222  def _setup_from_config(self, config: ConfigType) -> None:
223  """(Re)Setup the entity."""
224 
225  def _strings_to_services(
226  strings: list[str], string_to_service: dict[str, VacuumEntityFeature]
227  ) -> VacuumEntityFeature:
228  """Convert service strings to SUPPORT_* service bitmask."""
229  services = VacuumEntityFeature.STATE
230  for string in strings:
231  services |= string_to_service[string]
232  return services
233 
234  supported_feature_strings: list[str] = config[CONF_SUPPORTED_FEATURES]
235  self._attr_supported_features_attr_supported_features = _strings_to_services(
236  supported_feature_strings, STRING_TO_SERVICE
237  )
238  self._attr_fan_speed_list_attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
239  self._command_topic_command_topic = config.get(CONF_COMMAND_TOPIC)
240  self._set_fan_speed_topic_set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
241  self._send_command_topic_send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC)
242 
243  self._payloads_payloads = {
244  key: config.get(key)
245  for key in (
246  CONF_PAYLOAD_START,
247  CONF_PAYLOAD_PAUSE,
248  CONF_PAYLOAD_STOP,
249  CONF_PAYLOAD_RETURN_TO_BASE,
250  CONF_PAYLOAD_CLEAN_SPOT,
251  CONF_PAYLOAD_LOCATE,
252  )
253  }
254 
255  def _update_state_attributes(self, payload: dict[str, Any]) -> None:
256  """Update the entity state attributes."""
257  self._state_attrs.update(payload)
258  self._attr_fan_speed_attr_fan_speed = self._state_attrs.get(FAN_SPEED, 0)
259  self._attr_battery_level_attr_battery_level = max(0, min(100, self._state_attrs.get(BATTERY, 0)))
260 
261  @callback
262  def _state_message_received(self, msg: ReceiveMessage) -> None:
263  """Handle state MQTT message."""
264  payload = json_loads_object(msg.payload)
265  if STATE in payload and (
266  (state := payload[STATE]) in POSSIBLE_STATES or state is None
267  ):
268  self._attr_state_attr_state = (
269  POSSIBLE_STATES[cast(str, state)] if payload[STATE] else None
270  )
271  del payload[STATE]
272  self._update_state_attributes_update_state_attributes(payload)
273 
274  @callback
275  def _prepare_subscribe_topics(self) -> None:
276  """(Re)Subscribe to topics."""
277  self.add_subscriptionadd_subscription(
278  CONF_STATE_TOPIC,
279  self._state_message_received_state_message_received,
280  {"_attr_battery_level", "_attr_fan_speed", "_attr_state"},
281  )
282 
283  async def _subscribe_topics(self) -> None:
284  """(Re)Subscribe to topics."""
285  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
286 
287  async def _async_publish_command(self, feature: VacuumEntityFeature) -> None:
288  """Publish a command."""
289  if self._command_topic_command_topic is None:
290  return
291  await self.async_publish_with_configasync_publish_with_config(
292  self._command_topic_command_topic, self._payloads_payloads[_FEATURE_PAYLOADS[feature]]
293  )
294  self.async_write_ha_stateasync_write_ha_state()
295 
296  async def async_start(self) -> None:
297  """Start the vacuum."""
298  await self._async_publish_command_async_publish_command(VacuumEntityFeature.START)
299 
300  async def async_pause(self) -> None:
301  """Pause the vacuum."""
302  await self._async_publish_command_async_publish_command(VacuumEntityFeature.PAUSE)
303 
304  async def async_stop(self, **kwargs: Any) -> None:
305  """Stop the vacuum."""
306  await self._async_publish_command_async_publish_command(VacuumEntityFeature.STOP)
307 
308  async def async_return_to_base(self, **kwargs: Any) -> None:
309  """Tell the vacuum to return to its dock."""
310  await self._async_publish_command_async_publish_command(VacuumEntityFeature.RETURN_HOME)
311 
312  async def async_clean_spot(self, **kwargs: Any) -> None:
313  """Perform a spot clean-up."""
314  await self._async_publish_command_async_publish_command(VacuumEntityFeature.CLEAN_SPOT)
315 
316  async def async_locate(self, **kwargs: Any) -> None:
317  """Locate the vacuum (usually by playing a song)."""
318  await self._async_publish_command_async_publish_command(VacuumEntityFeature.LOCATE)
319 
320  async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
321  """Set fan speed."""
322  if (
323  self._set_fan_speed_topic_set_fan_speed_topic is None
324  or (self.supported_featuressupported_featuressupported_features & VacuumEntityFeature.FAN_SPEED == 0)
325  or (fan_speed not in self.fan_speed_listfan_speed_list)
326  ):
327  return
328  await self.async_publish_with_configasync_publish_with_config(self._set_fan_speed_topic_set_fan_speed_topic, fan_speed)
329 
331  self,
332  command: str,
333  params: dict[str, Any] | list[Any] | None = None,
334  **kwargs: Any,
335  ) -> None:
336  """Send a command to a vacuum cleaner."""
337  if (
338  self._send_command_topic_send_command_topic is None
339  or self.supported_featuressupported_featuressupported_features & VacuumEntityFeature.SEND_COMMAND == 0
340  ):
341  return
342  if isinstance(params, dict):
343  message: dict[str, Any] = {"command": command}
344  message.update(params)
345  payload = json_dumps(message)
346  else:
347  payload = command
348  await self.async_publish_with_configasync_publish_with_config(self._send_command_topic_send_command_topic, payload)
None async_publish_with_config(self, str topic, PublishPayloadType payload)
Definition: entity.py:1377
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
Definition: entity.py:1484
None _update_state_attributes(self, dict[str, Any] payload)
Definition: vacuum.py:255
None async_send_command(self, str command, dict[str, Any]|list[Any]|None params=None, **Any kwargs)
Definition: vacuum.py:335
None _async_publish_command(self, VacuumEntityFeature feature)
Definition: vacuum.py:287
None async_set_fan_speed(self, str fan_speed, **Any kwargs)
Definition: vacuum.py:320
None _state_message_received(self, ReceiveMessage msg)
Definition: vacuum.py:262
None _setup_from_config(self, ConfigType config)
Definition: vacuum.py:222
None __init__(self, HomeAssistant hass, ConfigType config, ConfigEntry config_entry, DiscoveryInfoType|None discovery_data)
Definition: vacuum.py:211
VacuumEntityFeature supported_features(self)
Definition: __init__.py:291
int|None supported_features(self)
Definition: entity.py:861
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
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)
Definition: entity.py:245
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: vacuum.py:180
list[str] services_to_strings(VacuumEntityFeature services, dict[VacuumEntityFeature, str] service_to_string)
Definition: vacuum.py:115
str json_dumps(Any data)
Definition: json.py:149
JsonObjectType json_loads_object(bytes|bytearray|memoryview|str obj)
Definition: json.py:54