Home Assistant Unofficial Reference 2024.12.1
image.py
Go to the documentation of this file.
1 """Support for Roborock image."""
2 
3 import asyncio
4 from datetime import datetime
5 import io
6 from itertools import chain
7 
8 from roborock import RoborockCommand
9 from vacuum_map_parser_base.config.color import ColorsPalette
10 from vacuum_map_parser_base.config.drawable import Drawable
11 from vacuum_map_parser_base.config.image_config import ImageConfig
12 from vacuum_map_parser_base.config.size import Sizes
13 from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser
14 
15 from homeassistant.components.image import ImageEntity
16 from homeassistant.const import EntityCategory
17 from homeassistant.core import HomeAssistant
18 from homeassistant.exceptions import HomeAssistantError
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.util import slugify
21 import homeassistant.util.dt as dt_util
22 
23 from . import RoborockConfigEntry
24 from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP
25 from .coordinator import RoborockDataUpdateCoordinator
26 from .entity import RoborockCoordinatedEntityV1
27 
28 
30  hass: HomeAssistant,
31  config_entry: RoborockConfigEntry,
32  async_add_entities: AddEntitiesCallback,
33 ) -> None:
34  """Set up Roborock image platform."""
35 
36  drawables = [
37  drawable
38  for drawable, default_value in DEFAULT_DRAWABLES.items()
39  if config_entry.options.get(DRAWABLES, {}).get(drawable, default_value)
40  ]
41  entities = list(
42  chain.from_iterable(
43  await asyncio.gather(
44  *(
45  create_coordinator_maps(coord, drawables)
46  for coord in config_entry.runtime_data.v1
47  )
48  )
49  )
50  )
51  async_add_entities(entities)
52 
53 
55  """A class to let you visualize the map."""
56 
57  _attr_has_entity_name = True
58  image_last_updated: datetime
59 
60  def __init__(
61  self,
62  unique_id: str,
63  coordinator: RoborockDataUpdateCoordinator,
64  map_flag: int,
65  starting_map: bytes,
66  map_name: str,
67  drawables: list[Drawable],
68  ) -> None:
69  """Initialize a Roborock map."""
70  RoborockCoordinatedEntityV1.__init__(self, unique_id, coordinator)
71  ImageEntity.__init__(self, coordinator.hass)
72  self._attr_name_attr_name = map_name
73  self.parserparser = RoborockMapDataParser(
74  ColorsPalette(), Sizes(), drawables, ImageConfig(), []
75  )
76  self._attr_image_last_updated_attr_image_last_updated = dt_util.utcnow()
77  self.map_flagmap_flag = map_flag
78  try:
79  self.cached_mapcached_map = self._create_image_create_image(starting_map)
80  except HomeAssistantError:
81  # If we failed to update the image on init,
82  # we set cached_map to empty bytes
83  # so that we are unavailable and can try again later.
84  self.cached_mapcached_map = b""
85  self._attr_entity_category_attr_entity_category = EntityCategory.DIAGNOSTIC
86 
87  @property
88  def available(self) -> bool:
89  """Determines if the entity is available."""
90  return self.cached_mapcached_map != b""
91 
92  @property
93  def is_selected(self) -> bool:
94  """Return if this map is the currently selected map."""
95  return self.map_flagmap_flag == self.coordinator.current_map
96 
97  def is_map_valid(self) -> bool:
98  """Update the map if it is valid.
99 
100  Update this map if it is the currently active map, and the
101  vacuum is cleaning, or if it has never been set at all.
102  """
103  return self.cached_mapcached_map == b"" or (
104  self.is_selectedis_selected
105  and self.image_last_updatedimage_last_updated is not None
106  and self.coordinator.roborock_device_info.props.status is not None
107  and bool(self.coordinator.roborock_device_info.props.status.in_cleaning)
108  )
109 
110  def _handle_coordinator_update(self) -> None:
111  # Bump last updated every third time the coordinator runs, so that async_image
112  # will be called and we will evaluate on the new coordinator data if we should
113  # update the cache.
114  if (
115  dt_util.utcnow() - self.image_last_updatedimage_last_updated
116  ).total_seconds() > IMAGE_CACHE_INTERVAL and self.is_map_validis_map_valid():
117  self._attr_image_last_updated_attr_image_last_updated = dt_util.utcnow()
119 
120  async def async_image(self) -> bytes | None:
121  """Update the image if it is not cached."""
122  if self.is_map_validis_map_valid():
123  response = await asyncio.gather(
124  *(self.cloud_apicloud_apicloud_api.get_map_v1(), self.coordinator.get_rooms()),
125  return_exceptions=True,
126  )
127  if not isinstance(response[0], bytes):
128  raise HomeAssistantError(
129  translation_domain=DOMAIN,
130  translation_key="map_failure",
131  )
132  map_data = response[0]
133  self.cached_mapcached_map = self._create_image_create_image(map_data)
134  return self.cached_mapcached_map
135 
136  def _create_image(self, map_bytes: bytes) -> bytes:
137  """Create an image using the map parser."""
138  parsed_map = self.parserparser.parse(map_bytes)
139  if parsed_map.image is None:
140  raise HomeAssistantError(
141  translation_domain=DOMAIN,
142  translation_key="map_failure",
143  )
144  img_byte_arr = io.BytesIO()
145  parsed_map.image.data.save(img_byte_arr, format="PNG")
146  return img_byte_arr.getvalue()
147 
148 
150  coord: RoborockDataUpdateCoordinator, drawables: list[Drawable]
151 ) -> list[RoborockMap]:
152  """Get the starting map information for all maps for this device.
153 
154  The following steps must be done synchronously.
155  Only one map can be loaded at a time per device.
156  """
157  entities = []
158  cur_map = coord.current_map
159  # This won't be None at this point as the coordinator will have run first.
160  assert cur_map is not None
161  # Sort the maps so that we start with the current map and we can skip the
162  # load_multi_map call.
163  maps_info = sorted(
164  coord.maps.items(), key=lambda data: data[0] == cur_map, reverse=True
165  )
166  for map_flag, map_info in maps_info:
167  # Load the map - so we can access it with get_map_v1
168  if map_flag != cur_map:
169  # Only change the map and sleep if we have multiple maps.
170  await coord.api.send_command(RoborockCommand.LOAD_MULTI_MAP, [map_flag])
171  coord.current_map = map_flag
172  # We cannot get the map until the roborock servers fully process the
173  # map change.
174  await asyncio.sleep(MAP_SLEEP)
175  # Get the map data
176  map_update = await asyncio.gather(
177  *[coord.cloud_api.get_map_v1(), coord.get_rooms()], return_exceptions=True
178  )
179  # If we fail to get the map, we should set it to empty byte,
180  # still create it, and set it as unavailable.
181  api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b""
182  entities.append(
183  RoborockMap(
184  f"{slugify(coord.duid)}_map_{map_info.name}",
185  coord,
186  map_flag,
187  api_data,
188  map_info.name,
189  drawables,
190  )
191  )
192  if len(coord.maps) != 1:
193  # Set the map back to the map the user previously had selected so that it
194  # does not change the end user's app.
195  # Only needs to happen when we changed maps above.
196  await coord.cloud_api.send_command(RoborockCommand.LOAD_MULTI_MAP, [cur_map])
197  coord.current_map = cur_map
198  return entities
None __init__(self, str unique_id, RoborockDataUpdateCoordinator coordinator, int map_flag, bytes starting_map, str map_name, list[Drawable] drawables)
Definition: image.py:68
bytes _create_image(self, bytes map_bytes)
Definition: image.py:136
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
list[RoborockMap] create_coordinator_maps(RoborockDataUpdateCoordinator coord, list[Drawable] drawables)
Definition: image.py:151
None async_setup_entry(HomeAssistant hass, RoborockConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: image.py:33