1 """Component to allow for providing device or service updates."""
3 from __future__
import annotations
5 from datetime
import timedelta
6 from enum
import StrEnum
7 from functools
import lru_cache
9 from typing
import Any, Final, final
11 from awesomeversion
import AwesomeVersion, AwesomeVersionCompareException
12 from propcache
import cached_property
13 import voluptuous
as vol
30 ATTR_DISPLAY_PRECISION,
32 ATTR_INSTALLED_VERSION,
38 ATTR_UPDATE_PERCENTAGE,
46 _LOGGER = logging.getLogger(__name__)
48 DATA_COMPONENT: HassKey[EntityComponent[UpdateEntity]] =
HassKey(DOMAIN)
49 ENTITY_ID_FORMAT: Final = DOMAIN +
".{}"
50 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
51 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
56 """Device class for update."""
61 DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(UpdateDeviceClass))
66 "ATTR_INSTALLED_VERSION",
67 "ATTR_LATEST_VERSION",
69 "DEVICE_CLASSES_SCHEMA",
71 "PLATFORM_SCHEMA_BASE",
77 "UpdateEntityDescription",
78 "UpdateEntityFeature",
84 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
85 """Set up Select entities."""
86 component = hass.data[DATA_COMPONENT] = EntityComponent[UpdateEntity](
87 _LOGGER, DOMAIN, hass, SCAN_INTERVAL
89 await component.async_setup(config)
91 component.async_register_entity_service(
94 vol.Optional(ATTR_VERSION): cv.string,
95 vol.Optional(ATTR_BACKUP, default=
False): cv.boolean,
98 [UpdateEntityFeature.INSTALL],
101 component.async_register_entity_service(
106 component.async_register_entity_service(
112 websocket_api.async_register_command(hass, websocket_release_notes)
118 """Set up a config entry."""
123 """Unload a config entry."""
127 async
def async_install(entity: UpdateEntity, service_call: ServiceCall) ->
None:
128 """Service call wrapper to validate the call."""
130 if (version := service_call.data.get(ATTR_VERSION))
is None and (
131 entity.installed_version == entity.latest_version
132 or entity.latest_version
is None
139 and UpdateEntityFeature.SPECIFIC_VERSION
not in entity.supported_features_compat
142 f
"Installing a specific version is not supported for {entity.entity_id}"
147 backup := service_call.data[ATTR_BACKUP]
148 )
and UpdateEntityFeature.BACKUP
not in entity.supported_features_compat:
152 if entity.in_progress
is not False:
154 f
"Update installation already in progress for {entity.entity_id}"
157 await entity.async_install_with_progress(version, backup)
160 async
def async_skip(entity: UpdateEntity, service_call: ServiceCall) ->
None:
161 """Service call wrapper to validate the call."""
162 if entity.auto_update:
164 f
"Skipping update is not supported for {entity.entity_id}"
166 await entity.async_skip()
170 """Service call wrapper to validate the call."""
171 if entity.auto_update:
173 f
"Clearing skipped update is not supported for {entity.entity_id}"
175 await entity.async_clear_skipped()
179 """A class that describes update entities."""
181 device_class: UpdateDeviceClass |
None =
None
182 display_precision: int = 0
183 entity_category: EntityCategory |
None = EntityCategory.CONFIG
186 @lru_cache(maxsize=256)
188 """Return True if latest_version is newer than installed_version."""
189 return AwesomeVersion(latest_version) > installed_version
192 CACHED_PROPERTIES_WITH_ATTR_ = {
201 "supported_features",
209 metaclass=ABCCachedProperties,
210 cached_properties=CACHED_PROPERTIES_WITH_ATTR_,
212 """Representation of an update entity."""
214 _entity_component_unrecorded_attributes = frozenset(
216 ATTR_DISPLAY_PRECISION,
219 ATTR_RELEASE_SUMMARY,
220 ATTR_UPDATE_PERCENTAGE,
224 entity_description: UpdateEntityDescription
225 _attr_auto_update: bool =
False
226 _attr_installed_version: str |
None =
None
227 _attr_device_class: UpdateDeviceClass |
None
228 _attr_display_precision: int
229 _attr_in_progress: bool | int =
False
230 _attr_latest_version: str |
None =
None
231 _attr_release_summary: str |
None =
None
232 _attr_release_url: str |
None =
None
233 _attr_state:
None =
None
235 _attr_title: str |
None =
None
236 _attr_update_percentage: int | float |
None =
None
237 __skipped_version: str |
None =
None
238 __in_progress: bool =
False
242 """Indicate if the device or service has auto update enabled."""
243 return self._attr_auto_update
247 """Version installed and in use."""
248 return self._attr_installed_version
251 """Return True if an unnamed entity should be named by its device class.
253 For updates this is True if the entity has a device class.
259 """Return the class of this entity."""
260 if hasattr(self,
"_attr_device_class"):
261 return self._attr_device_class
262 if hasattr(self,
"entity_description"):
263 return self.entity_description.device_class
268 """Return number of decimal digits for display of update progress."""
269 if hasattr(self,
"_attr_display_precision"):
270 return self._attr_display_precision
271 if hasattr(self,
"entity_description"):
272 return self.entity_description.display_precision
277 """Return the category of the entity, if any."""
278 if hasattr(self,
"_attr_entity_category"):
279 return self._attr_entity_category
280 if hasattr(self,
"entity_description"):
281 return self.entity_description.entity_category
283 return EntityCategory.CONFIG
284 return EntityCategory.DIAGNOSTIC
288 """Return the entity picture to use in the frontend.
290 Update entities return the brand icon based on the integration
294 f
"https://brands.home-assistant.io/_/{self.platform.platform_name}/icon.png"
299 """Update installation progress.
301 Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used.
303 Should return a boolean (True if in progress, False if not).
309 """Latest version available for install."""
310 return self._attr_latest_version
314 """Summary of the release notes or changelog.
316 This is not suitable for long changelogs, but merely suitable
317 for a short excerpt update description of max 255 characters.
319 return self._attr_release_summary
323 """URL to the full release notes of the latest version available."""
324 return self._attr_release_url
328 """Flag supported features."""
329 return self._attr_supported_features
333 """Title of the software.
335 This helps to differentiate between the device or entity name
336 versus the title of the software installed.
338 return self._attr_title
342 """Return the supported features as UpdateEntityFeature.
344 Remove this compatibility shim in 2025.1 or later.
347 if type(features)
is int:
355 """Update installation progress.
357 Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used.
359 Can either return a number to indicate the progress from 0 to 100% or None.
361 return self._attr_update_percentage
365 """Skip the current offered version to update."""
375 """Clear the skipped version."""
380 self, version: str |
None, backup: bool, **kwargs: Any
382 """Install an update.
384 Version can be specified to install a specific version. When `None`, the
385 latest version needs to be installed.
387 The backup parameter indicates a backup should be taken before
388 installing the update.
390 await self.
hasshass.async_add_executor_job(self.
installinstall, version, backup)
392 def install(self, version: str |
None, backup: bool, **kwargs: Any) ->
None:
393 """Install an update.
395 Version can be specified to install a specific version. When `None`, the
396 latest version needs to be installed.
398 The backup parameter indicates a backup should be taken before
399 installing the update.
401 raise NotImplementedError
404 """Return full release notes.
406 This is suitable for a long changelog that does not fit in the release_summary
407 property. The returned string can contain markdown.
409 return await self.
hasshass.async_add_executor_job(self.
release_notesrelease_notes)
412 """Return full release notes.
414 This is suitable for a long changelog that does not fit in the release_summary
415 property. The returned string can contain markdown.
417 raise NotImplementedError
420 """Return True if latest_version is newer than installed_version."""
427 """Return the entity state."""
435 if latest_version == installed_version:
439 newer = self.
version_is_newerversion_is_newer(latest_version, installed_version)
440 except AwesomeVersionCompareException:
443 return STATE_ON
if newer
else STATE_OFF
448 """Return state attributes."""
449 if (release_summary := self.
release_summaryrelease_summary)
is not None:
450 release_summary = release_summary[:255]
456 update_percentage = self.
update_percentageupdate_percentage
if in_progress
else None
457 if type(in_progress)
is not bool
and isinstance(in_progress, int):
458 update_percentage = in_progress
462 update_percentage =
None
469 if (installed_version
is not None and skipped_version == installed_version)
or (
470 latest_version
is not None and skipped_version != latest_version
472 skipped_version =
None
478 ATTR_INSTALLED_VERSION: installed_version,
479 ATTR_IN_PROGRESS: in_progress,
480 ATTR_LATEST_VERSION: latest_version,
481 ATTR_RELEASE_SUMMARY: release_summary,
483 ATTR_SKIPPED_VERSION: skipped_version,
484 ATTR_TITLE: self.
titletitle,
485 ATTR_UPDATE_PERCENTAGE: update_percentage,
490 self, version: str |
None, backup: bool
492 """Install update and handle progress if needed.
494 Handles setting the in_progress state in case the entity doesn't
510 """Call when the update entity is added to hass.
512 It is used to restore the skipped version, if any.
516 if state
is not None and state.attributes.get(ATTR_SKIPPED_VERSION)
is not None:
520 @websocket_api.require_admin
521 @websocket_api.websocket_command(
{
vol.Required("type"):
"update/release_notes",
522 vol.Required(
"entity_id"): cv.entity_id,
525 @websocket_api.async_response
531 """Get the full release notes for a entity."""
532 entity = hass.data[DATA_COMPONENT].
get_entity(msg[
"entity_id"])
535 connection.send_error(
536 msg[
"id"], websocket_api.ERR_NOT_FOUND,
"Entity not found"
540 if UpdateEntityFeature.RELEASE_NOTES
not in entity.supported_features_compat:
541 connection.send_error(
543 websocket_api.ERR_NOT_SUPPORTED,
544 "Entity does not support release notes",
548 connection.send_result(
550 await entity.async_release_notes(),
552
UpdateEntityFeature supported_features_compat(self)
bool _default_to_device_class_name(self)
None async_install(self, str|None version, bool backup, **Any kwargs)
UpdateEntityFeature supported_features(self)
None async_install_with_progress(self, str|None version, bool backup)
dict[str, Any]|None state_attributes(self)
UpdateDeviceClass|None device_class(self)
int display_precision(self)
str|None installed_version(self)
bool version_is_newer(self, str latest_version, str installed_version)
None async_internal_added_to_hass(self)
int|float|None update_percentage(self)
EntityCategory|None entity_category(self)
str|None release_url(self)
str|None async_release_notes(self)
str|None release_summary(self)
None install(self, str|None version, bool backup, **Any kwargs)
str|None entity_picture(self)
str|None release_notes(self)
bool|int|None in_progress(self)
None async_clear_skipped(self)
str|None latest_version(self)
None _report_deprecated_supported_features_values(self, IntFlag replacement)
str|None device_class(self)
None async_write_ha_state(self)
int|None supported_features(self)
State|None async_get_last_state(self)
CalendarEntity get_entity(HomeAssistant hass, str entity_id)
None async_skip(UpdateEntity entity, ServiceCall service_call)
bool _version_is_newer(str latest_version, str installed_version)
None async_clear_skipped(UpdateEntity entity, ServiceCall service_call)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None websocket_release_notes(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None async_install(UpdateEntity entity, ServiceCall service_call)
bool async_setup(HomeAssistant hass, ConfigType config)