Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Roborock Coordinator."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import logging
7 
8 from propcache import cached_property
9 from roborock import HomeDataRoom
10 from roborock.code_mappings import RoborockCategory
11 from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo
12 from roborock.exceptions import RoborockException
13 from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
14 from roborock.roborock_typing import DeviceProp
15 from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1
16 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
17 from roborock.version_a01_apis import RoborockClientA01
18 
19 from homeassistant.const import ATTR_CONNECTIONS
20 from homeassistant.core import HomeAssistant
21 from homeassistant.helpers import device_registry as dr
22 from homeassistant.helpers.device_registry import DeviceInfo
23 from homeassistant.helpers.typing import StateType
24 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
25 from homeassistant.util import slugify
26 
27 from .const import DOMAIN
28 from .models import RoborockA01HassDeviceInfo, RoborockHassDeviceInfo, RoborockMapInfo
29 
30 SCAN_INTERVAL = timedelta(seconds=30)
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 
36  """Class to manage fetching data from the API."""
37 
38  def __init__(
39  self,
40  hass: HomeAssistant,
41  device: HomeDataDevice,
42  device_networking: NetworkInfo,
43  product_info: HomeDataProduct,
44  cloud_api: RoborockMqttClientV1,
45  home_data_rooms: list[HomeDataRoom],
46  ) -> None:
47  """Initialize."""
48  super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
49  self.roborock_device_inforoborock_device_info = RoborockHassDeviceInfo(
50  device,
51  device_networking,
52  product_info,
53  DeviceProp(),
54  )
55  device_data = DeviceData(device, product_info.model, device_networking.ip)
56  self.apiapi: RoborockLocalClientV1 | RoborockMqttClientV1 = RoborockLocalClientV1(
57  device_data, queue_timeout=5
58  )
59  self.cloud_apicloud_api = cloud_api
60  self.device_infodevice_info = DeviceInfo(
61  name=self.roborock_device_inforoborock_device_info.device.name,
62  identifiers={(DOMAIN, self.roborock_device_inforoborock_device_info.device.duid)},
63  manufacturer="Roborock",
64  model=self.roborock_device_inforoborock_device_info.product.model,
65  model_id=self.roborock_device_inforoborock_device_info.product.model,
66  sw_version=self.roborock_device_inforoborock_device_info.device.fv,
67  )
68  self.current_mapcurrent_map: int | None = None
69 
70  if mac := self.roborock_device_inforoborock_device_info.network_info.mac:
71  self.device_infodevice_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, mac)}
72  # Maps from map flag to map name
73  self.maps: dict[int, RoborockMapInfo] = {}
74  self._home_data_rooms_home_data_rooms = {str(room.id): room.name for room in home_data_rooms}
75 
76  async def verify_api(self) -> None:
77  """Verify that the api is reachable. If it is not, switch clients."""
78  if isinstance(self.apiapi, RoborockLocalClientV1):
79  try:
80  await self.apiapi.ping()
81  except RoborockException:
82  _LOGGER.warning(
83  "Using the cloud API for device %s. This is not recommended as it can lead to rate limiting. We recommend making your vacuum accessible by your Home Assistant instance",
84  self.roborock_device_inforoborock_device_info.device.duid,
85  )
86  await self.apiapi.async_disconnect()
87  # We use the cloud api if the local api fails to connect.
88  self.apiapi = self.cloud_apicloud_api
89  # Right now this should never be called if the cloud api is the primary api,
90  # but in the future if it is, a new else should be added.
91 
92  async def release(self) -> None:
93  """Disconnect from API."""
94  await self.apiapi.async_release()
95  await self.cloud_apicloud_api.async_release()
96 
97  async def _update_device_prop(self) -> None:
98  """Update device properties."""
99  device_prop = await self.apiapi.get_prop()
100  if device_prop:
101  if self.roborock_device_inforoborock_device_info.props:
102  self.roborock_device_inforoborock_device_info.props.update(device_prop)
103  else:
104  self.roborock_device_inforoborock_device_info.props = device_prop
105 
106  async def _async_update_data(self) -> DeviceProp:
107  """Update data via library."""
108  try:
109  # Update device props and standard api information
110  await self._update_device_prop_update_device_prop()
111  # Set the new map id from the updated device props
112  self._set_current_map_set_current_map()
113  # Get the rooms for that map id.
114  await self.get_roomsget_rooms()
115  except RoborockException as ex:
116  raise UpdateFailed(ex) from ex
117  return self.roborock_device_inforoborock_device_info.props
118 
119  def _set_current_map(self) -> None:
120  if (
121  self.roborock_device_inforoborock_device_info.props.status is not None
122  and self.roborock_device_inforoborock_device_info.props.status.map_status is not None
123  ):
124  # The map status represents the map flag as flag * 4 + 3 -
125  # so we have to invert that in order to get the map flag that we can use to set the current map.
126  self.current_mapcurrent_map = (
127  self.roborock_device_inforoborock_device_info.props.status.map_status - 3
128  ) // 4
129 
130  async def get_maps(self) -> None:
131  """Add a map to the coordinators mapping."""
132  maps = await self.apiapi.get_multi_maps_list()
133  if maps and maps.map_info:
134  for roborock_map in maps.map_info:
135  self.maps[roborock_map.mapFlag] = RoborockMapInfo(
136  flag=roborock_map.mapFlag, name=roborock_map.name, rooms={}
137  )
138 
139  async def get_rooms(self) -> None:
140  """Get all of the rooms for the current map."""
141  # The api is only able to access rooms for the currently selected map
142  # So it is important this is only called when you have the map you care
143  # about selected.
144  if self.current_mapcurrent_map in self.maps:
145  iot_rooms = await self.apiapi.get_room_mapping()
146  if iot_rooms is not None:
147  for room in iot_rooms:
148  self.maps[self.current_mapcurrent_map].rooms[room.segment_id] = (
149  self._home_data_rooms_home_data_rooms.get(room.iot_id, "Unknown")
150  )
151 
152  @cached_property
153  def duid(self) -> str:
154  """Get the unique id of the device as specified by Roborock."""
155  return self.roborock_device_inforoborock_device_info.device.duid
156 
157  @cached_property
158  def duid_slug(self) -> str:
159  """Get the slug of the duid."""
160  return slugify(self.duidduid)
161 
162 
164  DataUpdateCoordinator[
165  dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType]
166  ]
167 ):
168  """Class to manage fetching data from the API for A01 devices."""
169 
170  def __init__(
171  self,
172  hass: HomeAssistant,
173  device: HomeDataDevice,
174  product_info: HomeDataProduct,
175  api: RoborockClientA01,
176  ) -> None:
177  """Initialize."""
178  super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
179  self.apiapi = api
180  self.device_infodevice_info = DeviceInfo(
181  name=device.name,
182  identifiers={(DOMAIN, device.duid)},
183  manufacturer="Roborock",
184  model=product_info.model,
185  sw_version=device.fv,
186  )
187  self.request_protocolsrequest_protocols: list[
188  RoborockDyadDataProtocol | RoborockZeoProtocol
189  ] = []
190  if product_info.category == RoborockCategory.WET_DRY_VAC:
191  self.request_protocolsrequest_protocols = [
192  RoborockDyadDataProtocol.STATUS,
193  RoborockDyadDataProtocol.POWER,
194  RoborockDyadDataProtocol.MESH_LEFT,
195  RoborockDyadDataProtocol.BRUSH_LEFT,
196  RoborockDyadDataProtocol.ERROR,
197  RoborockDyadDataProtocol.TOTAL_RUN_TIME,
198  ]
199  elif product_info.category == RoborockCategory.WASHING_MACHINE:
200  self.request_protocolsrequest_protocols = [
201  RoborockZeoProtocol.STATE,
202  RoborockZeoProtocol.COUNTDOWN,
203  RoborockZeoProtocol.WASHING_LEFT,
204  RoborockZeoProtocol.ERROR,
205  ]
206  else:
207  _LOGGER.warning("The device you added is not yet supported")
208  self.roborock_device_inforoborock_device_info = RoborockA01HassDeviceInfo(device, product_info)
209 
211  self,
212  ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType]:
213  return await self.apiapi.update_values(self.request_protocolsrequest_protocols)
214 
215  async def release(self) -> None:
216  """Disconnect from API."""
217  await self.apiapi.async_release()
218 
219  @cached_property
220  def duid(self) -> str:
221  """Get the unique id of the device as specified by Roborock."""
222  return self.roborock_device_inforoborock_device_info.device.duid
223 
224  @cached_property
225  def duid_slug(self) -> str:
226  """Get the slug of the duid."""
227  return slugify(self.duidduid)
dict[RoborockDyadDataProtocol|RoborockZeoProtocol, StateType] _async_update_data(self)
Definition: coordinator.py:212
None __init__(self, HomeAssistant hass, HomeDataDevice device, HomeDataProduct product_info, RoborockClientA01 api)
Definition: coordinator.py:176
None __init__(self, HomeAssistant hass, HomeDataDevice device, NetworkInfo device_networking, HomeDataProduct product_info, RoborockMqttClientV1 cloud_api, list[HomeDataRoom] home_data_rooms)
Definition: coordinator.py:46
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88