Home Assistant Unofficial Reference 2024.12.1
update.py
Go to the documentation of this file.
1 """Matter update."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from datetime import datetime, timedelta
7 from typing import Any
8 
9 from chip.clusters import Objects as clusters
10 from matter_server.common.errors import UpdateCheckError, UpdateError
11 from matter_server.common.models import MatterSoftwareVersion, UpdateSource
12 
14  ATTR_LATEST_VERSION,
15  UpdateDeviceClass,
16  UpdateEntity,
17  UpdateEntityDescription,
18  UpdateEntityFeature,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import STATE_ON, Platform
22 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.helpers.event import async_call_later
26 from homeassistant.helpers.restore_state import ExtraStoredData
27 
28 from .entity import MatterEntity
29 from .helpers import get_matter
30 from .models import MatterDiscoverySchema
31 
32 SCAN_INTERVAL = timedelta(hours=12)
33 POLL_AFTER_INSTALL = 10
34 
35 ATTR_SOFTWARE_UPDATE = "software_update"
36 
37 
38 @dataclass
40  """Extra stored data for Matter node firmware update entity."""
41 
42  software_update: MatterSoftwareVersion | None = None
43 
44  def as_dict(self) -> dict[str, Any]:
45  """Return a dict representation of the extra data."""
46  return {
47  ATTR_SOFTWARE_UPDATE: self.software_update.as_dict()
48  if self.software_update is not None
49  else None,
50  }
51 
52  @classmethod
53  def from_dict(cls, data: dict[str, Any]) -> MatterUpdateExtraStoredData:
54  """Initialize the extra data from a dict."""
55  if data[ATTR_SOFTWARE_UPDATE] is None:
56  return cls()
57  return cls(MatterSoftwareVersion.from_dict(data[ATTR_SOFTWARE_UPDATE]))
58 
59 
61  hass: HomeAssistant,
62  config_entry: ConfigEntry,
63  async_add_entities: AddEntitiesCallback,
64 ) -> None:
65  """Set up Matter lock from Config Entry."""
66  matter = get_matter(hass)
67  matter.register_platform_handler(Platform.UPDATE, async_add_entities)
68 
69 
71  """Representation of a Matter node capable of updating."""
72 
73  # Matter attribute changes are generally not polled, but the update check
74  # itself is. The update check is not done by the device itself, but by the
75  # Matter server.
76  _attr_should_poll = True
77  _software_update: MatterSoftwareVersion | None = None
78  _cancel_update: CALLBACK_TYPE | None = None
79  _attr_supported_features = (
80  UpdateEntityFeature.INSTALL
81  | UpdateEntityFeature.PROGRESS
82  | UpdateEntityFeature.SPECIFIC_VERSION
83  | UpdateEntityFeature.RELEASE_NOTES
84  )
85 
86  @callback
87  def _update_from_device(self) -> None:
88  """Update from device."""
89 
90  self._attr_installed_version_attr_installed_version = self.get_matter_attribute_valueget_matter_attribute_value(
91  clusters.BasicInformation.Attributes.SoftwareVersionString
92  )
93  update_state: clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum = (
94  self.get_matter_attribute_valueget_matter_attribute_value(
95  clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState
96  )
97  )
98  if (
99  update_state
100  == clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kIdle
101  ):
102  self._attr_in_progress_attr_in_progress_attr_in_progress = False
103  self._attr_update_percentage_attr_update_percentage = None
104  return
105 
106  update_progress: int = self.get_matter_attribute_valueget_matter_attribute_value(
107  clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress
108  )
109 
110  self._attr_in_progress_attr_in_progress_attr_in_progress = True
111  if (
112  update_state
113  == clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kDownloading
114  and update_progress is not None
115  and update_progress > 0
116  ):
117  self._attr_update_percentage_attr_update_percentage = update_progress
118  else:
119  self._attr_update_percentage_attr_update_percentage = None
120 
121  async def async_update(self) -> None:
122  """Call when the entity needs to be updated."""
123  try:
124  update_information = await self.matter_clientmatter_client.check_node_update(
125  node_id=self._endpoint_endpoint.node.node_id
126  )
127  if not update_information:
128  self._attr_latest_version_attr_latest_version = self._attr_installed_version_attr_installed_version
129  return
130 
131  self._software_update_software_update = update_information
132  self._attr_latest_version_attr_latest_version = update_information.software_version_string
133  self._attr_release_url_attr_release_url = update_information.release_notes_url
134 
135  except UpdateCheckError as err:
136  raise HomeAssistantError(f"Error finding applicable update: {err}") from err
137 
138  async def async_release_notes(self) -> str | None:
139  """Return full release notes.
140 
141  This is suitable for a long changelog that does not fit in the release_summary
142  property. The returned string can contain markdown.
143  """
144  if self._software_update_software_update is None:
145  return None
146  if self.statestatestate != STATE_ON:
147  return None
148 
149  release_notes = ""
150 
151  # insert extra heavy warning case the update is not from the main net
152  if self._software_update_software_update.update_source != UpdateSource.MAIN_NET_DCL:
153  release_notes += (
154  "\n\n<ha-alert alert-type='warning'>"
155  f"Update provided by {self._software_update.update_source.value}. "
156  "Installing this update is at your own risk and you may run into unexpected "
157  "problems such as the need to re-add and factory reset your device.</ha-alert>\n\n"
158  )
159  return release_notes + (
160  "\n\n<ha-alert alert-type='info'>The update process can take a while, "
161  "especially for battery powered devices. Please be patient and wait until the update "
162  "process is fully completed. Do not remove power from the device while it's updating. "
163  "The device may restart during the update process and be unavailable for several minutes."
164  "</ha-alert>\n\n"
165  )
166 
167  async def async_added_to_hass(self) -> None:
168  """Call when the entity is added to hass."""
169  await super().async_added_to_hass()
170 
171  if state := await self.async_get_last_stateasync_get_last_state():
172  self._attr_latest_version_attr_latest_version = state.attributes.get(ATTR_LATEST_VERSION)
173 
174  if (extra_data := await self.async_get_last_extra_dataasync_get_last_extra_data()) and (
175  matter_extra_data := MatterUpdateExtraStoredData.from_dict(
176  extra_data.as_dict()
177  )
178  ):
179  self._software_update_software_update = matter_extra_data.software_update
180  else:
181  # Check for updates when added the first time.
182  await self.async_updateasync_update()
183 
184  @property
185  def extra_restore_state_data(self) -> MatterUpdateExtraStoredData:
186  """Return Matter specific state data to be restored."""
187  return MatterUpdateExtraStoredData(self._software_update_software_update)
188 
189  @property
190  def entity_picture(self) -> str | None:
191  """Return the entity picture to use in the frontend.
192 
193  This overrides UpdateEntity.entity_picture because the Matter brand picture
194  is not appropriate for a matter device which has its own brand.
195  """
196  return None
197 
198  async def async_install(
199  self, version: str | None, backup: bool, **kwargs: Any
200  ) -> None:
201  """Install a new software version."""
202 
203  if not self.get_matter_attribute_valueget_matter_attribute_value(
204  clusters.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible
205  ):
206  raise HomeAssistantError("Device is not ready to install updates")
207 
208  software_version: str | int | None = version
209  if self._software_update_software_update is not None and (
210  version is None or version == self._software_update_software_update.software_version_string
211  ):
212  # Update to the version previously fetched and shown.
213  # We can pass the integer version directly to speedup download.
214  software_version = self._software_update_software_update.software_version
215 
216  if software_version is None:
217  raise HomeAssistantError("No software version specified")
218 
219  self._attr_in_progress_attr_in_progress_attr_in_progress = True
220  # Immediately update the progress state change to make frontend feel responsive.
221  # Progress updates from the device usually take few seconds to come in.
222  self.async_write_ha_stateasync_write_ha_state()
223  try:
224  await self.matter_clientmatter_client.update_node(
225  node_id=self._endpoint_endpoint.node.node_id,
226  software_version=software_version,
227  )
228  except UpdateCheckError as err:
229  raise HomeAssistantError(f"Error finding applicable update: {err}") from err
230  except UpdateError as err:
231  raise HomeAssistantError(f"Error updating: {err}") from err
232  finally:
233  # Check for updates right after the update since Matter devices
234  # can have strict update paths (e.g. Eve)
235  self._cancel_update_cancel_update = async_call_later(
236  self.hasshass, POLL_AFTER_INSTALL, self._async_update_future_async_update_future
237  )
238 
239  async def _async_update_future(self, now: datetime | None = None) -> None:
240  """Request update."""
241  await self.async_updateasync_update()
242 
243  async def async_will_remove_from_hass(self) -> None:
244  """Entity removed."""
245  await super().async_will_remove_from_hass()
246  if self._cancel_update_cancel_update is not None:
247  self._cancel_update_cancel_update()
248 
249 
250 DISCOVERY_SCHEMAS = [
252  platform=Platform.UPDATE,
253  entity_description=UpdateEntityDescription(
254  key="MatterUpdate", device_class=UpdateDeviceClass.FIRMWARE, name=None
255  ),
256  entity_class=MatterUpdate,
257  required_attributes=(
258  clusters.BasicInformation.Attributes.SoftwareVersion,
259  clusters.BasicInformation.Attributes.SoftwareVersionString,
260  clusters.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible,
261  clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState,
262  clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress,
263  ),
264  ),
265 ]
Any get_matter_attribute_value(self, type[ClusterAttributeDescriptor] attribute, bool null_as_none=True)
Definition: entity.py:206
MatterUpdateExtraStoredData from_dict(cls, dict[str, Any] data)
Definition: update.py:53
None _async_update_future(self, datetime|None now=None)
Definition: update.py:239
MatterUpdateExtraStoredData extra_restore_state_data(self)
Definition: update.py:185
None async_install(self, str|None version, bool backup, **Any kwargs)
Definition: update.py:200
ExtraStoredData|None async_get_last_extra_data(self)
MatterAdapter get_matter(HomeAssistant hass)
Definition: helpers.py:35
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: update.py:64
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