Home Assistant Unofficial Reference 2024.12.1
vacuum.py
Go to the documentation of this file.
1 """Shark IQ Wrapper."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 from typing import Any
7 
8 from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum
9 import voluptuous as vol
10 
12  STATE_CLEANING,
13  STATE_DOCKED,
14  STATE_IDLE,
15  STATE_PAUSED,
16  STATE_RETURNING,
17  StateVacuumEntity,
18  VacuumEntityFeature,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.core import HomeAssistant
22 from homeassistant.exceptions import ServiceValidationError
23 from homeassistant.helpers import entity_platform
25 from homeassistant.helpers.device_registry import DeviceInfo
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.update_coordinator import CoordinatorEntity
28 
29 from .const import DOMAIN, LOGGER, SERVICE_CLEAN_ROOM, SHARK
30 from .coordinator import SharkIqUpdateCoordinator
31 
32 OPERATING_STATE_MAP = {
33  OperatingModes.PAUSE: STATE_PAUSED,
34  OperatingModes.START: STATE_CLEANING,
35  OperatingModes.STOP: STATE_IDLE,
36  OperatingModes.RETURN: STATE_RETURNING,
37 }
38 
39 FAN_SPEEDS_MAP = {
40  "Eco": PowerModes.ECO,
41  "Normal": PowerModes.NORMAL,
42  "Max": PowerModes.MAX,
43 }
44 
45 STATE_RECHARGING_TO_RESUME = "recharging_to_resume"
46 
47 # Attributes to expose
48 ATTR_ERROR_CODE = "last_error_code"
49 ATTR_ERROR_MSG = "last_error_message"
50 ATTR_LOW_LIGHT = "low_light"
51 ATTR_RECHARGE_RESUME = "recharge_and_resume"
52 ATTR_ROOMS = "rooms"
53 
54 
56  hass: HomeAssistant,
57  config_entry: ConfigEntry,
58  async_add_entities: AddEntitiesCallback,
59 ) -> None:
60  """Set up the Shark IQ vacuum cleaner."""
61  coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
62  devices: Iterable[SharkIqVacuum] = coordinator.shark_vacs.values()
63  device_names = [d.name for d in devices]
64  LOGGER.debug(
65  "Found %d Shark IQ device(s): %s",
66  len(device_names),
67  ", ".join([d.name for d in devices]),
68  )
69  async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices])
70 
71  platform = entity_platform.async_get_current_platform()
72  platform.async_register_entity_service(
73  SERVICE_CLEAN_ROOM,
74  {
75  vol.Required(ATTR_ROOMS): vol.All(
76  cv.ensure_list, vol.Length(min=1), [cv.string]
77  ),
78  },
79  "async_clean_room",
80  )
81 
82 
83 class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuumEntity):
84  """Shark IQ vacuum entity."""
85 
86  _attr_fan_speed_list = list(FAN_SPEEDS_MAP)
87  _attr_has_entity_name = True
88  _attr_name = None
89  _attr_supported_features = (
90  VacuumEntityFeature.BATTERY
91  | VacuumEntityFeature.FAN_SPEED
92  | VacuumEntityFeature.PAUSE
93  | VacuumEntityFeature.RETURN_HOME
94  | VacuumEntityFeature.START
95  | VacuumEntityFeature.STATE
96  | VacuumEntityFeature.STOP
97  | VacuumEntityFeature.LOCATE
98  )
99  _unrecorded_attributes = frozenset({ATTR_ROOMS})
100 
101  def __init__(
102  self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator
103  ) -> None:
104  """Create a new SharkVacuumEntity."""
105  super().__init__(coordinator)
106  self.sharkiqsharkiq = sharkiq
107  self._attr_unique_id_attr_unique_id = sharkiq.serial_number
108  self._attr_device_info_attr_device_info = DeviceInfo(
109  identifiers={(DOMAIN, sharkiq.serial_number)},
110  manufacturer=SHARK,
111  model=self.modelmodel,
112  name=sharkiq.name,
113  sw_version=sharkiq.get_property_value(Properties.ROBOT_FIRMWARE_VERSION),
114  )
115 
116  def clean_spot(self, **kwargs: Any) -> None:
117  """Clean a spot. Not yet implemented."""
118  raise NotImplementedError
119 
121  self,
122  command: str,
123  params: dict[str, Any] | list[Any] | None = None,
124  **kwargs: Any,
125  ) -> None:
126  """Send a command to the vacuum. Not yet implemented."""
127  raise NotImplementedError
128 
129  @property
130  def is_online(self) -> bool:
131  """Tell us if the device is online."""
132  return self.coordinator.device_is_online(self.sharkiqsharkiq.serial_number)
133 
134  @property
135  def model(self) -> str:
136  """Vacuum model number."""
137  if self.sharkiqsharkiq.vac_model_number:
138  return self.sharkiqsharkiq.vac_model_number
139  return self.sharkiqsharkiq.oem_model_number
140 
141  @property
142  def error_code(self) -> int | None:
143  """Return the last observed error code (or None)."""
144  return self.sharkiqsharkiq.error_code
145 
146  @property
147  def error_message(self) -> str | None:
148  """Return the last observed error message (or None)."""
149  if not self.error_codeerror_code:
150  return None
151  return self.sharkiqsharkiq.error_text
152 
153  @property
154  def operating_mode(self) -> str | None:
155  """Operating mode."""
156  op_mode = self.sharkiqsharkiq.get_property_value(Properties.OPERATING_MODE)
157  return OPERATING_STATE_MAP.get(op_mode)
158 
159  @property
160  def recharging_to_resume(self) -> int | None:
161  """Return True if vacuum set to recharge and resume cleaning."""
162  return self.sharkiqsharkiq.get_property_value(Properties.RECHARGING_TO_RESUME)
163 
164  @property
165  def state(self) -> str | None:
166  """Get the current vacuum state.
167 
168  NB: Currently, we do not return an error state because they can be very, very stale.
169  In the app, these are (usually) handled by showing the robot as stopped and sending the
170  user a notification.
171  """
172  if self.sharkiqsharkiq.get_property_value(Properties.CHARGING_STATUS):
173  return STATE_DOCKED
174  return self.operating_modeoperating_mode
175 
176  @property
177  def available(self) -> bool:
178  """Determine if the sensor is available based on API results."""
179  # If the last update was successful...
180  return self.coordinator.last_update_success and self.is_onlineis_online
181 
182  @property
183  def battery_level(self) -> int | None:
184  """Get the current battery level."""
185  return self.sharkiqsharkiq.get_property_value(Properties.BATTERY_CAPACITY)
186 
187  async def async_return_to_base(self, **kwargs: Any) -> None:
188  """Have the device return to base."""
189  await self.sharkiqsharkiq.async_set_operating_mode(OperatingModes.RETURN)
190  await self.coordinator.async_refresh()
191 
192  async def async_pause(self) -> None:
193  """Pause the cleaning task."""
194  await self.sharkiqsharkiq.async_set_operating_mode(OperatingModes.PAUSE)
195  await self.coordinator.async_refresh()
196 
197  async def async_start(self) -> None:
198  """Start the device."""
199  await self.sharkiqsharkiq.async_set_operating_mode(OperatingModes.START)
200  await self.coordinator.async_refresh()
201 
202  async def async_stop(self, **kwargs: Any) -> None:
203  """Stop the device."""
204  await self.sharkiqsharkiq.async_set_operating_mode(OperatingModes.STOP)
205  await self.coordinator.async_refresh()
206 
207  async def async_locate(self, **kwargs: Any) -> None:
208  """Cause the device to generate a loud chirp."""
209  await self.sharkiqsharkiq.async_find_device()
210 
211  async def async_clean_room(self, rooms: list[str], **kwargs: Any) -> None:
212  """Clean specific rooms."""
213  rooms_to_clean = []
214  valid_rooms = self.available_roomsavailable_rooms or []
215  rooms = [room.replace("_", " ").title() for room in rooms]
216  for room in rooms:
217  if room in valid_rooms:
218  rooms_to_clean.append(room)
219  else:
221  translation_domain=DOMAIN,
222  translation_key="invalid_room",
223  translation_placeholders={"room": room},
224  )
225 
226  LOGGER.debug("Cleaning room(s): %s", rooms_to_clean)
227  await self.sharkiqsharkiq.async_clean_rooms(rooms_to_clean)
228  await self.coordinator.async_refresh()
229 
230  @property
231  def fan_speed(self) -> str | None:
232  """Return the current fan speed."""
233  fan_speed = None
234  speed_level = self.sharkiqsharkiq.get_property_value(Properties.POWER_MODE)
235  for k, val in FAN_SPEEDS_MAP.items():
236  if val == speed_level:
237  fan_speed = k
238  return fan_speed
239 
240  async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
241  """Set the fan speed."""
242  await self.sharkiqsharkiq.async_set_property_value(
243  Properties.POWER_MODE, FAN_SPEEDS_MAP.get(fan_speed.capitalize())
244  )
245  await self.coordinator.async_refresh()
246 
247  # Various attributes we want to expose
248  @property
249  def recharge_resume(self) -> bool | None:
250  """Recharge and resume mode active."""
251  return self.sharkiqsharkiq.get_property_value(Properties.RECHARGE_RESUME)
252 
253  @property
254  def rssi(self) -> int | None:
255  """Get the WiFi RSSI."""
256  return self.sharkiqsharkiq.get_property_value(Properties.RSSI)
257 
258  @property
259  def low_light(self):
260  """Let us know if the robot is operating in low-light mode."""
261  return self.sharkiqsharkiq.get_property_value(Properties.LOW_LIGHT_MISSION)
262 
263  @property
264  def available_rooms(self) -> list | None:
265  """Return a list of rooms available to clean."""
266  room_list = self.sharkiqsharkiq.get_property_value(Properties.ROBOT_ROOM_LIST)
267  if room_list:
268  return room_list.split(":")[1:]
269  return []
270 
271  @property
272  def extra_state_attributes(self) -> dict[str, Any]:
273  """Return a dictionary of device state attributes specific to sharkiq."""
274  return {
275  ATTR_ERROR_CODE: self.error_codeerror_code,
276  ATTR_ERROR_MSG: self.sharkiqsharkiq.error_text,
277  ATTR_LOW_LIGHT: self.low_lightlow_light,
278  ATTR_RECHARGE_RESUME: self.recharge_resumerecharge_resume,
279  ATTR_ROOMS: self.available_roomsavailable_rooms,
280  }
None async_clean_room(self, list[str] rooms, **Any kwargs)
Definition: vacuum.py:211
None __init__(self, SharkIqVacuum sharkiq, SharkIqUpdateCoordinator coordinator)
Definition: vacuum.py:103
None async_set_fan_speed(self, str fan_speed, **Any kwargs)
Definition: vacuum.py:240
None send_command(self, str command, dict[str, Any]|list[Any]|None params=None, **Any kwargs)
Definition: vacuum.py:125
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: vacuum.py:59