Home Assistant Unofficial Reference 2024.12.1
update.py
Go to the documentation of this file.
1 """Update platform for ESPHome."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import Any
7 
8 from aioesphomeapi import (
9  DeviceInfo as ESPHomeDeviceInfo,
10  EntityInfo,
11  UpdateCommand,
12  UpdateInfo,
13  UpdateState,
14 )
15 
17  UpdateDeviceClass,
18  UpdateEntity,
19  UpdateEntityFeature,
20 )
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.helpers import device_registry as dr
25 from homeassistant.helpers.device_registry import DeviceInfo
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.update_coordinator import CoordinatorEntity
28 from homeassistant.util.enum import try_parse_enum
29 
30 from .coordinator import ESPHomeDashboardCoordinator
31 from .dashboard import async_get_dashboard
32 from .domain_data import DomainData
33 from .entity import (
34  EsphomeEntity,
35  convert_api_error_ha_error,
36  esphome_state_property,
37  platform_async_setup_entry,
38 )
39 from .entry_data import RuntimeEntryData
40 
41 KEY_UPDATE_LOCK = "esphome_update_lock"
42 
43 NO_FEATURES = UpdateEntityFeature(0)
44 
45 
47  hass: HomeAssistant,
48  entry: ConfigEntry,
49  async_add_entities: AddEntitiesCallback,
50 ) -> None:
51  """Set up ESPHome update based on a config entry."""
53  hass,
54  entry,
55  async_add_entities,
56  info_type=UpdateInfo,
57  entity_type=ESPHomeUpdateEntity,
58  state_type=UpdateState,
59  )
60 
61  if (dashboard := async_get_dashboard(hass)) is None:
62  return
63  entry_data = DomainData.get(hass).get_entry_data(entry)
64  assert entry_data.device_info is not None
65  device_name = entry_data.device_info.name
66  unsubs: list[CALLBACK_TYPE] = []
67 
68  @callback
69  def _async_setup_update_entity() -> None:
70  """Set up the update entity."""
71  nonlocal unsubs
72  assert dashboard is not None
73  # Keep listening until device is available
74  if not entry_data.available or not dashboard.last_update_success:
75  return
76 
77  # Do not add Dashboard Entity if this device is not known to the ESPHome dashboard.
78  if dashboard.data is None or dashboard.data.get(device_name) is None:
79  return
80 
81  for unsub in unsubs:
82  unsub()
83  unsubs.clear()
84 
85  async_add_entities([ESPHomeDashboardUpdateEntity(entry_data, dashboard)])
86 
87  if (
88  entry_data.available
89  and dashboard.last_update_success
90  and dashboard.data is not None
91  and dashboard.data.get(device_name)
92  ):
93  _async_setup_update_entity()
94  return
95 
96  unsubs = [
97  entry_data.async_subscribe_device_updated(_async_setup_update_entity),
98  dashboard.async_add_listener(_async_setup_update_entity),
99  ]
100 
101 
103  CoordinatorEntity[ESPHomeDashboardCoordinator], UpdateEntity
104 ):
105  """Defines an ESPHome update entity."""
106 
107  _attr_has_entity_name = True
108  _attr_device_class = UpdateDeviceClass.FIRMWARE
109  _attr_title = "ESPHome"
110  _attr_name = "Firmware"
111  _attr_release_url = "https://esphome.io/changelog/"
112  _attr_entity_registry_enabled_default = False
113 
114  def __init__(
115  self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboardCoordinator
116  ) -> None:
117  """Initialize the update entity."""
118  super().__init__(coordinator=coordinator)
119  assert entry_data.device_info is not None
120  self._entry_data_entry_data = entry_data
121  self._attr_unique_id_attr_unique_id = entry_data.device_info.mac_address
122  self._attr_device_info_attr_device_info = DeviceInfo(
123  connections={
124  (dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
125  }
126  )
127  self._update_attrs_update_attrs()
128 
129  @callback
130  def _update_attrs(self) -> None:
131  """Update the supported features."""
132  # If the device has deep sleep, we can't assume we can install updates
133  # as the ESP will not be connectable (by design).
134  coordinator = self.coordinator
135  device_info = self._device_info_device_info
136  # Install support can change at run time
137  if (
138  coordinator.last_update_success
139  and coordinator.supports_update
140  and not device_info.has_deep_sleep
141  ):
142  self._attr_supported_features_attr_supported_features = UpdateEntityFeature.INSTALL
143  else:
144  self._attr_supported_features_attr_supported_features = NO_FEATURES
145  self._attr_installed_version_attr_installed_version = device_info.esphome_version
146  device = coordinator.data.get(device_info.name)
147  assert device is not None
148  self._attr_latest_version_attr_latest_version = device["current_version"]
149 
150  @callback
151  def _handle_coordinator_update(self) -> None:
152  """Handle updated data from the coordinator."""
153  self._update_attrs_update_attrs()
155 
156  @property
157  def _device_info(self) -> ESPHomeDeviceInfo:
158  """Return the device info."""
159  assert self._entry_data_entry_data.device_info is not None
160  return self._entry_data_entry_data.device_info
161 
162  @property
163  def available(self) -> bool:
164  """Return if update is available.
165 
166  During deep sleep the ESP will not be connectable (by design)
167  and thus, even when unavailable, we'll show it as available.
168  """
169  return super().available and (
170  self._entry_data_entry_data.available
171  or self._entry_data_entry_data.expected_disconnect
172  or self._device_info_device_info.has_deep_sleep
173  )
174 
175  @callback
177  self, static_info: list[EntityInfo] | None = None
178  ) -> None:
179  """Handle updated data from the device."""
180  self._update_attrs_update_attrs()
181  self.async_write_ha_stateasync_write_ha_state()
182 
183  async def async_added_to_hass(self) -> None:
184  """Handle entity added to Home Assistant."""
185  await super().async_added_to_hass()
186  entry_data = self._entry_data_entry_data
187  self.async_on_removeasync_on_remove(
188  entry_data.async_subscribe_static_info_updated(self._handle_device_update_handle_device_update)
189  )
190  self.async_on_removeasync_on_remove(
191  entry_data.async_subscribe_device_updated(self._handle_device_update_handle_device_update)
192  )
193 
194  async def async_install(
195  self, version: str | None, backup: bool, **kwargs: Any
196  ) -> None:
197  """Install an update."""
198  async with self.hasshasshass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
199  coordinator = self.coordinator
200  api = coordinator.api
201  device = coordinator.data.get(self._device_info_device_info.name)
202  assert device is not None
203  try:
204  if not await api.compile(device["configuration"]):
205  raise HomeAssistantError(
206  f"Error compiling {device['configuration']}; "
207  "Try again in ESPHome dashboard for more information."
208  )
209  if not await api.upload(device["configuration"], "OTA"):
210  raise HomeAssistantError(
211  f"Error updating {device['configuration']} via OTA; "
212  "Try again in ESPHome dashboard for more information."
213  )
214  finally:
215  await self.coordinator.async_request_refresh()
216 
217 
218 class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
219  """A update implementation for esphome."""
220 
221  _attr_supported_features = (
222  UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
223  )
224 
225  @callback
226  def _on_static_info_update(self, static_info: EntityInfo) -> None:
227  """Set attrs from static info."""
228  super()._on_static_info_update(static_info)
229  static_info = self._static_info_static_info
230  self._attr_device_class_attr_device_class = try_parse_enum(
231  UpdateDeviceClass, static_info.device_class
232  )
233 
234  @property
235  @esphome_state_property
236  def installed_version(self) -> str | None:
237  """Return the installed version."""
238  return self._state_state.current_version
239 
240  @property
241  @esphome_state_property
242  def in_progress(self) -> bool:
243  """Return if the update is in progress."""
244  return self._state_state.in_progress
245 
246  @property
247  @esphome_state_property
248  def latest_version(self) -> str | None:
249  """Return the latest version."""
250  return self._state_state.latest_version
251 
252  @property
253  @esphome_state_property
254  def release_summary(self) -> str | None:
255  """Return the release summary."""
256  return self._state_state.release_summary
257 
258  @property
259  @esphome_state_property
260  def release_url(self) -> str | None:
261  """Return the release URL."""
262  return self._state_state.release_url
263 
264  @property
265  @esphome_state_property
266  def title(self) -> str | None:
267  """Return the title of the update."""
268  return self._state_state.title
269 
270  @property
271  @esphome_state_property
272  def update_percentage(self) -> int | None:
273  """Return if the update is in progress."""
274  if self._state_state.has_progress:
275  return int(self._state_state.progress)
276  return None
277 
278  @convert_api_error_ha_error
279  async def async_update(self) -> None:
280  """Command device to check for update."""
281  if self.availableavailable:
282  self._client_client.update_command(key=self._key_key, command=UpdateCommand.CHECK)
283 
284  @convert_api_error_ha_error
285  async def async_install(
286  self, version: str | None, backup: bool, **kwargs: Any
287  ) -> None:
288  """Command device to install update."""
289  self._client_client.update_command(key=self._key_key, command=UpdateCommand.INSTALL)
None __init__(self, RuntimeEntryData entry_data, ESPHomeDashboardCoordinator coordinator)
Definition: update.py:116
None async_install(self, str|None version, bool backup, **Any kwargs)
Definition: update.py:196
None _handle_device_update(self, list[EntityInfo]|None static_info=None)
Definition: update.py:178
None async_install(self, str|None version, bool backup, **Any kwargs)
Definition: update.py:287
None _on_static_info_update(self, EntityInfo static_info)
Definition: update.py:226
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
ESPHomeDashboardCoordinator|None async_get_dashboard(HomeAssistant hass)
Definition: dashboard.py:134
None platform_async_setup_entry(HomeAssistant hass, ESPHomeConfigEntry entry, AddEntitiesCallback async_add_entities, *type[_InfoT] info_type, type[_EntityT] entity_type, type[_StateT] state_type)
Definition: entity.py:89
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: update.py:50