Home Assistant Unofficial Reference 2024.12.1
update.py
Go to the documentation of this file.
1 """Update entities for Reolink devices."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from typing import Any
7 
8 from reolink_aio.exceptions import ReolinkError
9 from reolink_aio.software_version import NewSoftwareVersion, SoftwareVersion
10 
12  UpdateDeviceClass,
13  UpdateEntity,
14  UpdateEntityDescription,
15  UpdateEntityFeature,
16 )
17 from homeassistant.core import CALLBACK_TYPE, HomeAssistant
18 from homeassistant.exceptions import HomeAssistantError
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.helpers.event import async_call_later
22  CoordinatorEntity,
23  DataUpdateCoordinator,
24 )
25 
26 from . import DEVICE_UPDATE_INTERVAL
27 from .entity import (
28  ReolinkChannelCoordinatorEntity,
29  ReolinkChannelEntityDescription,
30  ReolinkHostCoordinatorEntity,
31  ReolinkHostEntityDescription,
32 )
33 from .util import ReolinkConfigEntry, ReolinkData
34 
35 PARALLEL_UPDATES = 0
36 RESUME_AFTER_INSTALL = 15
37 POLL_AFTER_INSTALL = 120
38 POLL_PROGRESS = 2
39 
40 
41 @dataclass(frozen=True, kw_only=True)
43  UpdateEntityDescription,
44  ReolinkChannelEntityDescription,
45 ):
46  """A class that describes update entities."""
47 
48 
49 @dataclass(frozen=True, kw_only=True)
51  UpdateEntityDescription,
52  ReolinkHostEntityDescription,
53 ):
54  """A class that describes host update entities."""
55 
56 
57 UPDATE_ENTITIES = (
59  key="firmware",
60  supported=lambda api, ch: api.supported(ch, "firmware"),
61  device_class=UpdateDeviceClass.FIRMWARE,
62  ),
63 )
64 
65 HOST_UPDATE_ENTITIES = (
67  key="firmware",
68  supported=lambda api: api.supported(None, "firmware"),
69  device_class=UpdateDeviceClass.FIRMWARE,
70  ),
71 )
72 
73 
75  hass: HomeAssistant,
76  config_entry: ReolinkConfigEntry,
77  async_add_entities: AddEntitiesCallback,
78 ) -> None:
79  """Set up update entities for Reolink component."""
80  reolink_data: ReolinkData = config_entry.runtime_data
81 
82  entities: list[ReolinkUpdateEntity | ReolinkHostUpdateEntity] = [
83  ReolinkUpdateEntity(reolink_data, channel, entity_description)
84  for entity_description in UPDATE_ENTITIES
85  for channel in reolink_data.host.api.channels
86  if entity_description.supported(reolink_data.host.api, channel)
87  ]
88  entities.extend(
89  ReolinkHostUpdateEntity(reolink_data, entity_description)
90  for entity_description in HOST_UPDATE_ENTITIES
91  if entity_description.supported(reolink_data.host.api)
92  )
93  async_add_entities(entities)
94 
95 
97  CoordinatorEntity[DataUpdateCoordinator[None]], UpdateEntity
98 ):
99  """Base update entity class for Reolink."""
100 
101  _attr_release_url = "https://reolink.com/download-center/"
102 
103  def __init__(
104  self,
105  reolink_data: ReolinkData,
106  channel: int | None,
107  coordinator: DataUpdateCoordinator[None],
108  ) -> None:
109  """Initialize Reolink update entity."""
110  CoordinatorEntity.__init__(self, coordinator)
111  self._channel_channel = channel
112  self._host_host = reolink_data.host
113  self._cancel_update_cancel_update: CALLBACK_TYPE | None = None
114  self._cancel_resume_cancel_resume: CALLBACK_TYPE | None = None
115  self._cancel_progress_cancel_progress: CALLBACK_TYPE | None = None
116  self._installing_installing: bool = False
117  self._reolink_data_reolink_data = reolink_data
118 
119  @property
120  def installed_version(self) -> str | None:
121  """Version currently in use."""
122  return self._host_host.api.camera_sw_version(self._channel_channel)
123 
124  @property
125  def latest_version(self) -> str | None:
126  """Latest version available for install."""
127  new_firmware = self._host_host.api.firmware_update_available(self._channel_channel)
128  if not new_firmware:
129  return self.installed_versioninstalled_versioninstalled_versioninstalled_version
130 
131  if isinstance(new_firmware, str):
132  return new_firmware
133 
134  return new_firmware.version_string
135 
136  @property
137  def in_progress(self) -> bool:
138  """Update installation progress."""
139  return self._host_host.api.sw_upload_progress(self._channel_channel) < 100
140 
141  @property
142  def update_percentage(self) -> int:
143  """Update installation progress."""
144  return self._host_host.api.sw_upload_progress(self._channel_channel)
145 
146  @property
147  def supported_features(self) -> UpdateEntityFeature:
148  """Flag supported features."""
149  supported_features = UpdateEntityFeature.INSTALL
150  new_firmware = self._host_host.api.firmware_update_available(self._channel_channel)
151  if isinstance(new_firmware, NewSoftwareVersion):
152  supported_features |= UpdateEntityFeature.RELEASE_NOTES
153  supported_features |= UpdateEntityFeature.PROGRESS
154  return supported_features
155 
156  @property
157  def available(self) -> bool:
158  """Return True if entity is available."""
159  if self._installing_installing or self._cancel_update_cancel_update is not None:
160  return True
161  return super().available
162 
163  def version_is_newer(self, latest_version: str, installed_version: str) -> bool:
164  """Return True if latest_version is newer than installed_version."""
165  try:
166  installed = SoftwareVersion(installed_version)
167  latest = SoftwareVersion(latest_version)
168  except ReolinkError:
169  # when the online update API returns a unexpected string
170  return True
171 
172  return latest > installed
173 
174  async def async_release_notes(self) -> str | None:
175  """Return the release notes."""
176  new_firmware = self._host_host.api.firmware_update_available(self._channel_channel)
177  assert isinstance(new_firmware, NewSoftwareVersion)
178 
179  return (
180  "If the install button fails, download this"
181  f" [firmware zip file]({new_firmware.download_url})."
182  " Then, follow the installation guide (PDF in the zip file).\n\n"
183  f"## Release notes\n\n{new_firmware.release_notes}"
184  )
185 
186  async def async_install(
187  self, version: str | None, backup: bool, **kwargs: Any
188  ) -> None:
189  """Install the latest firmware version."""
190  self._installing_installing = True
191  await self._pause_update_coordinator_pause_update_coordinator()
192  self._cancel_progress_cancel_progress = async_call_later(
193  self.hasshasshass, POLL_PROGRESS, self._async_update_progress_async_update_progress
194  )
195  try:
196  await self._host_host.api.update_firmware(self._channel_channel)
197  except ReolinkError as err:
198  raise HomeAssistantError(
199  f"Error trying to update Reolink firmware: {err}"
200  ) from err
201  finally:
202  self.async_write_ha_stateasync_write_ha_state()
203  self._cancel_update_cancel_update = async_call_later(
204  self.hasshasshass, POLL_AFTER_INSTALL, self._async_update_future_async_update_future
205  )
206  self._cancel_resume_cancel_resume = async_call_later(
207  self.hasshasshass, RESUME_AFTER_INSTALL, self._resume_update_coordinator_resume_update_coordinator
208  )
209  self._installing_installing = False
210 
211  async def _pause_update_coordinator(self) -> None:
212  """Pause updating the states using the data update coordinator (during reboots)."""
213  self._reolink_data_reolink_data.device_coordinator.update_interval = None
214  self._reolink_data_reolink_data.device_coordinator.async_set_updated_data(None)
215 
216  async def _resume_update_coordinator(self, *args: Any) -> None:
217  """Resume updating the states using the data update coordinator (after reboots)."""
218  self._reolink_data_reolink_data.device_coordinator.update_interval = DEVICE_UPDATE_INTERVAL
219  try:
220  await self._reolink_data_reolink_data.device_coordinator.async_refresh()
221  finally:
222  self._cancel_resume_cancel_resume = None
223 
224  async def _async_update_progress(self, *args: Any) -> None:
225  """Request update."""
226  self.async_write_ha_stateasync_write_ha_state()
227  if self._installing_installing:
228  self._cancel_progress_cancel_progress = async_call_later(
229  self.hasshasshass, POLL_PROGRESS, self._async_update_progress_async_update_progress
230  )
231 
232  async def _async_update_future(self, *args: Any) -> None:
233  """Request update."""
234  try:
235  await self.async_updateasync_update()
236  finally:
237  self._cancel_update_cancel_update = None
238 
239  async def async_added_to_hass(self) -> None:
240  """Entity created."""
241  await super().async_added_to_hass()
242  self._host_host.firmware_ch_list.append(self._channel_channel)
243 
244  async def async_will_remove_from_hass(self) -> None:
245  """Entity removed."""
246  await super().async_will_remove_from_hass()
247  if self._channel_channel in self._host_host.firmware_ch_list:
248  self._host_host.firmware_ch_list.remove(self._channel_channel)
249  if self._cancel_update_cancel_update is not None:
250  self._cancel_update_cancel_update()
251  if self._cancel_progress_cancel_progress is not None:
252  self._cancel_progress_cancel_progress()
253  if self._cancel_resume_cancel_resume is not None:
254  self._cancel_resume_cancel_resume()
255 
256 
258  ReolinkUpdateBaseEntity,
259  ReolinkChannelCoordinatorEntity,
260 ):
261  """Base update entity class for Reolink IP cameras."""
262 
263  entity_description: ReolinkUpdateEntityDescription
264  _channel: int
265 
266  def __init__(
267  self,
268  reolink_data: ReolinkData,
269  channel: int,
270  entity_description: ReolinkUpdateEntityDescription,
271  ) -> None:
272  """Initialize Reolink update entity."""
273  self.entity_descriptionentity_description = entity_description
274  ReolinkUpdateBaseEntity.__init__(
275  self, reolink_data, channel, reolink_data.firmware_coordinator
276  )
277  ReolinkChannelCoordinatorEntity.__init__(
278  self, reolink_data, channel, reolink_data.firmware_coordinator
279  )
280 
281 
283  ReolinkUpdateBaseEntity,
284  ReolinkHostCoordinatorEntity,
285 ):
286  """Update entity class for Reolink Host."""
287 
288  entity_description: ReolinkHostUpdateEntityDescription
289 
290  def __init__(
291  self,
292  reolink_data: ReolinkData,
293  entity_description: ReolinkHostUpdateEntityDescription,
294  ) -> None:
295  """Initialize Reolink update entity."""
296  self.entity_descriptionentity_description = entity_description
297  ReolinkUpdateBaseEntity.__init__(
298  self, reolink_data, None, reolink_data.firmware_coordinator
299  )
300  ReolinkHostCoordinatorEntity.__init__(
301  self, reolink_data, reolink_data.firmware_coordinator
302  )
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597