Home Assistant Unofficial Reference 2024.12.1
update.py
Go to the documentation of this file.
1 """Update entities for Shelly devices."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 import logging
8 from typing import Any, Final, cast
9 
10 from aioshelly.const import RPC_GENERATIONS
11 from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
12 from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
13 
15  ATTR_INSTALLED_VERSION,
16  ATTR_LATEST_VERSION,
17  UpdateDeviceClass,
18  UpdateEntity,
19  UpdateEntityDescription,
20  UpdateEntityFeature,
21 )
22 from homeassistant.const import EntityCategory
23 from homeassistant.core import HomeAssistant, callback
24 from homeassistant.exceptions import HomeAssistantError
25 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26 from homeassistant.helpers.restore_state import RestoreEntity
27 
28 from .const import CONF_SLEEP_PERIOD, OTA_BEGIN, OTA_ERROR, OTA_PROGRESS, OTA_SUCCESS
29 from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
30 from .entity import (
31  RestEntityDescription,
32  RpcEntityDescription,
33  ShellyRestAttributeEntity,
34  ShellyRpcAttributeEntity,
35  ShellySleepingRpcAttributeEntity,
36  async_setup_entry_rest,
37  async_setup_entry_rpc,
38 )
39 from .utils import get_device_entry_gen, get_release_url
40 
41 LOGGER = logging.getLogger(__name__)
42 
43 
44 @dataclass(frozen=True, kw_only=True)
46  """Class to describe a RPC update."""
47 
48  latest_version: Callable[[dict], Any]
49  beta: bool
50 
51 
52 @dataclass(frozen=True, kw_only=True)
54  """Class to describe a REST update."""
55 
56  latest_version: Callable[[dict], Any]
57  beta: bool
58 
59 
60 REST_UPDATES: Final = {
61  "fwupdate": RestUpdateDescription(
62  name="Firmware",
63  key="fwupdate",
64  latest_version=lambda status: status["update"]["new_version"],
65  beta=False,
66  device_class=UpdateDeviceClass.FIRMWARE,
67  entity_category=EntityCategory.CONFIG,
68  entity_registry_enabled_default=False,
69  ),
70  "fwupdate_beta": RestUpdateDescription(
71  name="Beta firmware",
72  key="fwupdate",
73  latest_version=lambda status: status["update"].get("beta_version"),
74  beta=True,
75  device_class=UpdateDeviceClass.FIRMWARE,
76  entity_category=EntityCategory.CONFIG,
77  entity_registry_enabled_default=False,
78  ),
79 }
80 
81 RPC_UPDATES: Final = {
82  "fwupdate": RpcUpdateDescription(
83  name="Firmware",
84  key="sys",
85  sub_key="available_updates",
86  latest_version=lambda status: status.get("stable", {"version": ""})["version"],
87  beta=False,
88  device_class=UpdateDeviceClass.FIRMWARE,
89  entity_category=EntityCategory.CONFIG,
90  ),
91  "fwupdate_beta": RpcUpdateDescription(
92  name="Beta firmware",
93  key="sys",
94  sub_key="available_updates",
95  latest_version=lambda status: status.get("beta", {"version": ""})["version"],
96  beta=True,
97  device_class=UpdateDeviceClass.FIRMWARE,
98  entity_category=EntityCategory.CONFIG,
99  entity_registry_enabled_default=False,
100  ),
101 }
102 
103 
105  hass: HomeAssistant,
106  config_entry: ShellyConfigEntry,
107  async_add_entities: AddEntitiesCallback,
108 ) -> None:
109  """Set up update entities for Shelly component."""
110  if get_device_entry_gen(config_entry) in RPC_GENERATIONS:
111  if config_entry.data[CONF_SLEEP_PERIOD]:
113  hass,
114  config_entry,
115  async_add_entities,
116  RPC_UPDATES,
117  RpcSleepingUpdateEntity,
118  )
119  else:
121  hass, config_entry, async_add_entities, RPC_UPDATES, RpcUpdateEntity
122  )
123  return
124 
125  if not config_entry.data[CONF_SLEEP_PERIOD]:
127  hass,
128  config_entry,
129  async_add_entities,
130  REST_UPDATES,
131  RestUpdateEntity,
132  )
133 
134 
136  """Represent a REST update entity."""
137 
138  _attr_supported_features = (
139  UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
140  )
141  entity_description: RestUpdateDescription
142 
143  def __init__(
144  self,
145  block_coordinator: ShellyBlockCoordinator,
146  attribute: str,
147  description: RestUpdateDescription,
148  ) -> None:
149  """Initialize update entity."""
150  super().__init__(block_coordinator, attribute, description)
151  self._attr_release_url_attr_release_url = get_release_url(
152  block_coordinator.device.gen,
153  block_coordinator.model,
154  description.beta,
155  )
156  self._in_progress_old_version_in_progress_old_version: str | None = None
157 
158  @property
159  def installed_version(self) -> str | None:
160  """Version currently in use."""
161  return cast(str, self.block_coordinatorblock_coordinator.device.status["update"]["old_version"])
162 
163  @property
164  def latest_version(self) -> str | None:
165  """Latest version available for install."""
166  new_version = self.entity_descriptionentity_description.latest_version(
167  self.block_coordinatorblock_coordinator.device.status,
168  )
169  if new_version:
170  return cast(str, new_version)
171 
172  return self.installed_versioninstalled_versioninstalled_versioninstalled_version
173 
174  @property
175  def in_progress(self) -> bool:
176  """Update installation in progress."""
177  return self._in_progress_old_version_in_progress_old_version == self.installed_versioninstalled_versioninstalled_versioninstalled_version
178 
179  async def async_install(
180  self, version: str | None, backup: bool, **kwargs: Any
181  ) -> None:
182  """Install the latest firmware version."""
183  self._in_progress_old_version_in_progress_old_version = self.installed_versioninstalled_versioninstalled_versioninstalled_version
184  beta = self.entity_descriptionentity_description.beta
185  update_data = self.coordinator.device.status["update"]
186  LOGGER.debug("OTA update service - update_data: %s", update_data)
187 
188  new_version = update_data["new_version"]
189  if beta:
190  new_version = update_data["beta_version"]
191 
192  LOGGER.info(
193  "Starting OTA update of device %s from '%s' to '%s'",
194  self.namename,
195  self.coordinator.device.firmware_version,
196  new_version,
197  )
198  try:
199  result = await self.coordinator.device.trigger_ota_update(beta=beta)
200  except DeviceConnectionError as err:
201  raise HomeAssistantError(f"Error starting OTA update: {err!r}") from err
202  except InvalidAuthError:
203  await self.coordinator.async_shutdown_device_and_start_reauth()
204  else:
205  LOGGER.debug("Result of OTA update call: %s", result)
206 
207  def version_is_newer(self, latest_version: str, installed_version: str) -> bool:
208  """Return True if available version is newer then installed version.
209 
210  Default strategy generate an exception with Shelly firmware format
211  thus making the entity state always true.
212  """
213  return AwesomeVersion(
214  latest_version,
215  find_first_match=True,
216  ensure_strategy=[AwesomeVersionStrategy.SEMVER],
217  ) > AwesomeVersion(
218  installed_version,
219  find_first_match=True,
220  ensure_strategy=[AwesomeVersionStrategy.SEMVER],
221  )
222 
223 
225  """Represent a RPC update entity."""
226 
227  _attr_supported_features = (
228  UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
229  )
230  entity_description: RpcUpdateDescription
231 
232  def __init__(
233  self,
234  coordinator: ShellyRpcCoordinator,
235  key: str,
236  attribute: str,
237  description: RpcUpdateDescription,
238  ) -> None:
239  """Initialize update entity."""
240  super().__init__(coordinator, key, attribute, description)
241  self._ota_in_progress_ota_in_progress = False
242  self._ota_progress_percentage_ota_progress_percentage: int | None = None
243  self._attr_release_url_attr_release_url = get_release_url(
244  coordinator.device.gen, coordinator.model, description.beta
245  )
246 
247  async def async_added_to_hass(self) -> None:
248  """When entity is added to hass."""
249  await super().async_added_to_hass()
250  self.async_on_removeasync_on_remove(
251  self.coordinator.async_subscribe_ota_events(self._ota_progress_callback_ota_progress_callback)
252  )
253 
254  @callback
255  def _ota_progress_callback(self, event: dict[str, Any]) -> None:
256  """Handle device OTA progress."""
257  if self.in_progressin_progressin_progress is not False:
258  event_type = event["event"]
259  if event_type == OTA_BEGIN:
260  self._ota_progress_percentage_ota_progress_percentage = 0
261  elif event_type == OTA_PROGRESS:
262  self._ota_progress_percentage_ota_progress_percentage = event["progress_percent"]
263  elif event_type in (OTA_ERROR, OTA_SUCCESS):
264  self._ota_in_progress_ota_in_progress = False
265  self._ota_progress_percentage_ota_progress_percentage = None
266  self.async_write_ha_stateasync_write_ha_state()
267 
268  @property
269  def installed_version(self) -> str | None:
270  """Version currently in use."""
271  return cast(str, self.coordinator.device.shelly["ver"])
272 
273  @property
274  def latest_version(self) -> str | None:
275  """Latest version available for install."""
276  new_version = self.entity_descriptionentity_description.latest_version(self.sub_statussub_status)
277  if new_version:
278  return cast(str, new_version)
279 
280  return self.installed_versioninstalled_versioninstalled_versioninstalled_version
281 
282  @property
283  def in_progress(self) -> bool:
284  """Update installation in progress."""
285  return self._ota_in_progress_ota_in_progress
286 
287  @property
288  def update_percentage(self) -> int | None:
289  """Update installation progress."""
290  return self._ota_progress_percentage_ota_progress_percentage
291 
292  async def async_install(
293  self, version: str | None, backup: bool, **kwargs: Any
294  ) -> None:
295  """Install the latest firmware version."""
296  beta = self.entity_descriptionentity_description.beta
297  update_data = self.coordinator.device.status["sys"]["available_updates"]
298  LOGGER.debug("OTA update service - update_data: %s", update_data)
299 
300  new_version = update_data.get("stable", {"version": ""})["version"]
301  if beta:
302  new_version = update_data.get("beta", {"version": ""})["version"]
303 
304  LOGGER.info(
305  "Starting OTA update of device %s from '%s' to '%s'",
306  self.coordinator.name,
307  self.coordinator.device.firmware_version,
308  new_version,
309  )
310  try:
311  await self.coordinator.device.trigger_ota_update(beta=beta)
312  except DeviceConnectionError as err:
313  raise HomeAssistantError(f"OTA update connection error: {err!r}") from err
314  except RpcCallError as err:
315  raise HomeAssistantError(f"OTA update request error: {err!r}") from err
316  except InvalidAuthError:
317  await self.coordinator.async_shutdown_device_and_start_reauth()
318  else:
319  self._ota_in_progress_ota_in_progress = True
320  self._ota_progress_percentage_ota_progress_percentage = None
321  LOGGER.debug("OTA update call for %s successful", self.coordinator.name)
322 
323 
325  ShellySleepingRpcAttributeEntity, UpdateEntity, RestoreEntity
326 ):
327  """Represent a RPC sleeping update entity."""
328 
329  entity_description: RpcUpdateDescription
330 
331  async def async_added_to_hass(self) -> None:
332  """Handle entity which will be added."""
333  await super().async_added_to_hass()
334  self.last_statelast_state = await self.async_get_last_stateasync_get_last_state()
335 
336  @property
337  def installed_version(self) -> str | None:
338  """Version currently in use."""
339  if self.coordinatorcoordinator.device.initialized:
340  return cast(str, self.coordinatorcoordinator.device.shelly["ver"])
341 
342  if self.last_statelast_state is None:
343  return None
344 
345  return self.last_statelast_state.attributes.get(ATTR_INSTALLED_VERSION)
346 
347  @property
348  def latest_version(self) -> str | None:
349  """Latest version available for install."""
350  if self.coordinatorcoordinator.device.initialized:
351  new_version = self.entity_descriptionentity_descriptionentity_description.latest_version(self.sub_statussub_status)
352  if new_version:
353  return cast(str, new_version)
354 
355  return self.installed_versioninstalled_versioninstalled_versioninstalled_version
356 
357  if self.last_statelast_state is None:
358  return None
359 
360  return self.last_statelast_state.attributes.get(ATTR_LATEST_VERSION)
361 
362  @property
363  def release_url(self) -> str | None:
364  """URL to the full release notes."""
365  if not self.coordinatorcoordinator.device.initialized:
366  return None
367 
368  return get_release_url(
369  self.coordinatorcoordinator.device.gen,
370  self.coordinatorcoordinator.model,
371  self.entity_descriptionentity_descriptionentity_description.beta,
372  )
CALLBACK_TYPE async_subscribe_ota_events(self, Callable[[dict[str, Any]], None] ota_event_callback)
Definition: coordinator.py:527
None __init__(self, ShellyBlockCoordinator block_coordinator, str attribute, RestUpdateDescription description)
Definition: update.py:148
None async_install(self, str|None version, bool backup, **Any kwargs)
Definition: update.py:181
bool version_is_newer(self, str latest_version, str installed_version)
Definition: update.py:207
None async_install(self, str|None version, bool backup, **Any kwargs)
Definition: update.py:294
None _ota_progress_callback(self, dict[str, Any] event)
Definition: update.py:255
None __init__(self, ShellyRpcCoordinator coordinator, str key, str attribute, RpcUpdateDescription description)
Definition: update.py:238
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str|UndefinedType|None name(self)
Definition: entity.py:738
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry_rpc(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[str, RpcEntityDescription] sensors, Callable sensor_class)
Definition: entity.py:142
None async_setup_entry_rest(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, Mapping[str, RestEntityDescription] sensors, Callable sensor_class)
Definition: entity.py:251
None async_setup_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: update.py:108
int get_device_entry_gen(ConfigEntry entry)
Definition: utils.py:353
str|None get_release_url(int gen, str model, bool beta)
Definition: utils.py:454