Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Roborock component."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Coroutine
7 from dataclasses import dataclass
8 from datetime import timedelta
9 import logging
10 from typing import Any
11 
12 from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials
13 from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData
14 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
15 from roborock.version_a01_apis import RoborockMqttClientA01
16 from roborock.web_api import RoborockApiClient
17 
18 from homeassistant.config_entries import ConfigEntry
19 from homeassistant.const import CONF_USERNAME
20 from homeassistant.core import HomeAssistant
21 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
22 
23 from .const import CONF_BASE_URL, CONF_USER_DATA, DOMAIN, PLATFORMS
24 from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
25 
26 SCAN_INTERVAL = timedelta(seconds=30)
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 type RoborockConfigEntry = ConfigEntry[RoborockCoordinators]
31 
32 
33 @dataclass
35  """Roborock coordinators type."""
36 
37  v1: list[RoborockDataUpdateCoordinator]
38  a01: list[RoborockDataUpdateCoordinatorA01]
39 
40  def values(
41  self,
42  ) -> list[RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01]:
43  """Return all coordinators."""
44  return self.v1 + self.a01
45 
46 
47 async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool:
48  """Set up roborock from a config entry."""
49 
50  entry.async_on_unload(entry.add_update_listener(update_listener))
51 
52  user_data = UserData.from_dict(entry.data[CONF_USER_DATA])
53  api_client = RoborockApiClient(entry.data[CONF_USERNAME], entry.data[CONF_BASE_URL])
54  _LOGGER.debug("Getting home data")
55  try:
56  home_data = await api_client.get_home_data_v2(user_data)
57  except RoborockInvalidCredentials as err:
59  "Invalid credentials",
60  translation_domain=DOMAIN,
61  translation_key="invalid_credentials",
62  ) from err
63  except RoborockException as err:
64  raise ConfigEntryNotReady(
65  "Failed to get Roborock home data",
66  translation_domain=DOMAIN,
67  translation_key="home_data_fail",
68  ) from err
69  _LOGGER.debug("Got home data %s", home_data)
70  all_devices: list[HomeDataDevice] = home_data.devices + home_data.received_devices
71  device_map: dict[str, HomeDataDevice] = {
72  device.duid: device for device in all_devices
73  }
74  product_info: dict[str, HomeDataProduct] = {
75  product.id: product for product in home_data.products
76  }
77  # Get a Coordinator if the device is available or if we have connected to the device before
78  coordinators = await asyncio.gather(
80  hass, device_map, user_data, product_info, home_data.rooms
81  ),
82  return_exceptions=True,
83  )
84  # Valid coordinators are those where we had networking cached or we could get networking
85  v1_coords = [
86  coord
87  for coord in coordinators
88  if isinstance(coord, RoborockDataUpdateCoordinator)
89  ]
90  a01_coords = [
91  coord
92  for coord in coordinators
93  if isinstance(coord, RoborockDataUpdateCoordinatorA01)
94  ]
95  if len(v1_coords) + len(a01_coords) == 0:
96  raise ConfigEntryNotReady(
97  "No devices were able to successfully setup",
98  translation_domain=DOMAIN,
99  translation_key="no_coordinators",
100  )
101  valid_coordinators = RoborockCoordinators(v1_coords, a01_coords)
102 
103  async def on_unload() -> None:
104  release_tasks = set()
105  for coordinator in valid_coordinators.values():
106  release_tasks.add(coordinator.release())
107  await asyncio.gather(*release_tasks)
108 
109  entry.async_on_unload(on_unload)
110  entry.runtime_data = valid_coordinators
111 
112  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
113 
114  return True
115 
116 
118  hass: HomeAssistant,
119  device_map: dict[str, HomeDataDevice],
120  user_data: UserData,
121  product_info: dict[str, HomeDataProduct],
122  home_data_rooms: list[HomeDataRoom],
123 ) -> list[
124  Coroutine[
125  Any,
126  Any,
127  RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None,
128  ]
129 ]:
130  """Create a list of setup functions that can later be called asynchronously."""
131  return [
132  setup_device(
133  hass, user_data, device, product_info[device.product_id], home_data_rooms
134  )
135  for device in device_map.values()
136  ]
137 
138 
139 async def setup_device(
140  hass: HomeAssistant,
141  user_data: UserData,
142  device: HomeDataDevice,
143  product_info: HomeDataProduct,
144  home_data_rooms: list[HomeDataRoom],
145 ) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None:
146  """Set up a coordinator for a given device."""
147  if device.pv == "1.0":
148  return await setup_device_v1(
149  hass, user_data, device, product_info, home_data_rooms
150  )
151  if device.pv == "A01":
152  return await setup_device_a01(hass, user_data, device, product_info)
153  _LOGGER.warning(
154  "Not adding device %s because its protocol version %s or category %s is not supported",
155  device.duid,
156  device.pv,
157  product_info.category.name,
158  )
159  return None
160 
161 
162 async def setup_device_v1(
163  hass: HomeAssistant,
164  user_data: UserData,
165  device: HomeDataDevice,
166  product_info: HomeDataProduct,
167  home_data_rooms: list[HomeDataRoom],
168 ) -> RoborockDataUpdateCoordinator | None:
169  """Set up a device Coordinator."""
170  mqtt_client = await hass.async_add_executor_job(
171  RoborockMqttClientV1, user_data, DeviceData(device, product_info.model)
172  )
173  try:
174  networking = await mqtt_client.get_networking()
175  if networking is None:
176  # If the api does not return an error but does return None for
177  # get_networking - then we need to go through cache checking.
178  raise RoborockException("Networking request returned None.") # noqa: TRY301
179  except RoborockException as err:
180  _LOGGER.warning(
181  "Not setting up %s because we could not get the network information of the device. "
182  "Please confirm it is online and the Roborock servers can communicate with it",
183  device.name,
184  )
185  _LOGGER.debug(err)
186  await mqtt_client.async_release()
187  raise
188  coordinator = RoborockDataUpdateCoordinator(
189  hass, device, networking, product_info, mqtt_client, home_data_rooms
190  )
191  # Verify we can communicate locally - if we can't, switch to cloud api
192  await coordinator.verify_api()
193  coordinator.api.is_available = True
194  try:
195  await coordinator.get_maps()
196  except RoborockException as err:
197  _LOGGER.warning("Failed to get map data")
198  _LOGGER.debug(err)
199  try:
200  await coordinator.async_config_entry_first_refresh()
201  except ConfigEntryNotReady as ex:
202  await coordinator.release()
203  if isinstance(coordinator.api, RoborockMqttClientV1):
204  _LOGGER.warning(
205  "Not setting up %s because the we failed to get data for the first time using the online client. "
206  "Please ensure your Home Assistant instance can communicate with this device. "
207  "You may need to open firewall instances on your Home Assistant network and on your Vacuum's network",
208  device.name,
209  )
210  # Most of the time if we fail to connect using the mqtt client, the problem is due to firewall,
211  # but in case if it isn't, the error can be included in debug logs for the user to grab.
212  if coordinator.last_exception:
213  _LOGGER.debug(coordinator.last_exception)
214  raise coordinator.last_exception from ex
215  elif coordinator.last_exception:
216  # If this is reached, we have verified that we can communicate with the Vacuum locally,
217  # so if there is an error here - it is not a communication issue but some other problem
218  extra_error = f"Please create an issue with the following error included: {coordinator.last_exception}"
219  _LOGGER.warning(
220  "Not setting up %s because the coordinator failed to get data for the first time using the "
221  "offline client %s",
222  device.name,
223  extra_error,
224  )
225  raise coordinator.last_exception from ex
226  return coordinator
227 
228 
230  hass: HomeAssistant,
231  user_data: UserData,
232  device: HomeDataDevice,
233  product_info: HomeDataProduct,
234 ) -> RoborockDataUpdateCoordinatorA01 | None:
235  """Set up a A01 protocol device."""
236  mqtt_client = RoborockMqttClientA01(
237  user_data, DeviceData(device, product_info.name), product_info.category
238  )
239  coord = RoborockDataUpdateCoordinatorA01(hass, device, product_info, mqtt_client)
240  await coord.async_config_entry_first_refresh()
241  return coord
242 
243 
244 async def async_unload_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool:
245  """Handle removal of an entry."""
246  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
247 
248 
249 async def update_listener(hass: HomeAssistant, entry: RoborockConfigEntry) -> None:
250  """Handle options update."""
251  # Reload entry to update data
252  await hass.config_entries.async_reload(entry.entry_id)
list[RoborockDataUpdateCoordinator|RoborockDataUpdateCoordinatorA01] values(self)
Definition: __init__.py:42
None on_unload(HomeAssistant hass, GatewayId gateway_id, Callable fnct)
Definition: helpers.py:45
bool async_setup_entry(HomeAssistant hass, RoborockConfigEntry entry)
Definition: __init__.py:47
None update_listener(HomeAssistant hass, RoborockConfigEntry entry)
Definition: __init__.py:249
list[ Coroutine[ Any, Any, RoborockDataUpdateCoordinator|RoborockDataUpdateCoordinatorA01|None,]] build_setup_functions(HomeAssistant hass, dict[str, HomeDataDevice] device_map, UserData user_data, dict[str, HomeDataProduct] product_info, list[HomeDataRoom] home_data_rooms)
Definition: __init__.py:129
RoborockDataUpdateCoordinator|None setup_device_v1(HomeAssistant hass, UserData user_data, HomeDataDevice device, HomeDataProduct product_info, list[HomeDataRoom] home_data_rooms)
Definition: __init__.py:168
RoborockDataUpdateCoordinatorA01|None setup_device_a01(HomeAssistant hass, UserData user_data, HomeDataDevice device, HomeDataProduct product_info)
Definition: __init__.py:234
bool async_unload_entry(HomeAssistant hass, RoborockConfigEntry entry)
Definition: __init__.py:244
RoborockDataUpdateCoordinator|RoborockDataUpdateCoordinatorA01|None setup_device(HomeAssistant hass, UserData user_data, HomeDataDevice device, HomeDataProduct product_info, list[HomeDataRoom] home_data_rooms)
Definition: __init__.py:145