Skip to content

Commit

Permalink
Allow update entities to report progress as a float (home-assistant#1…
Browse files Browse the repository at this point in the history
…28930)

* Allow update entities to report progress as a float

* Add test

* Update snapshots

* Update recorder test

* Use _attr_* in MockUpdateEntity
  • Loading branch information
emontnemery authored and zxdavb committed Oct 25, 2024
1 parent ec20b63 commit 7a3b927
Show file tree
Hide file tree
Showing 16 changed files with 76 additions and 51 deletions.
21 changes: 18 additions & 3 deletions homeassistant/components/update/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .const import (
ATTR_AUTO_UPDATE,
ATTR_BACKUP,
ATTR_DISPLAY_PRECISION,
ATTR_IN_PROGRESS,
ATTR_INSTALLED_VERSION,
ATTR_LATEST_VERSION,
Expand Down Expand Up @@ -178,6 +179,7 @@ class UpdateEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes update entities."""

device_class: UpdateDeviceClass | None = None
display_precision: int = 0
entity_category: EntityCategory | None = EntityCategory.CONFIG


Expand All @@ -191,6 +193,7 @@ def _version_is_newer(latest_version: str, installed_version: str) -> bool:
"auto_update",
"installed_version",
"device_class",
"display_precision",
"in_progress",
"latest_version",
"release_summary",
Expand All @@ -210,6 +213,7 @@ class UpdateEntity(

_entity_component_unrecorded_attributes = frozenset(
{
ATTR_DISPLAY_PRECISION,
ATTR_ENTITY_PICTURE,
ATTR_IN_PROGRESS,
ATTR_RELEASE_SUMMARY,
Expand All @@ -221,14 +225,15 @@ class UpdateEntity(
_attr_auto_update: bool = False
_attr_installed_version: str | None = None
_attr_device_class: UpdateDeviceClass | None
_attr_display_precision: int
_attr_in_progress: bool | int = False
_attr_latest_version: str | None = None
_attr_release_summary: str | None = None
_attr_release_url: str | None = None
_attr_state: None = None
_attr_supported_features: UpdateEntityFeature = UpdateEntityFeature(0)
_attr_title: str | None = None
_attr_update_percentage: int | None = None
_attr_update_percentage: int | float | None = None
__skipped_version: str | None = None
__in_progress: bool = False

Expand Down Expand Up @@ -258,6 +263,15 @@ def device_class(self) -> UpdateDeviceClass | None:
return self.entity_description.device_class
return None

@cached_property
def display_precision(self) -> int:
"""Return number of decimal digits for display of update progress."""
if hasattr(self, "_attr_display_precision"):
return self._attr_display_precision
if hasattr(self, "entity_description"):
return self.entity_description.display_precision
return 0

@property
def entity_category(self) -> EntityCategory | None:
"""Return the category of the entity, if any."""
Expand Down Expand Up @@ -337,12 +351,12 @@ def supported_features_compat(self) -> UpdateEntityFeature:
return features

@cached_property
def update_percentage(self) -> int | None:
def update_percentage(self) -> int | float | None:
"""Update installation progress.
Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used.
Can either return an integer to indicate the progress from 0 to 100% or None.
Can either return a number to indicate the progress from 0 to 100% or None.
"""
return self._attr_update_percentage

Expand Down Expand Up @@ -460,6 +474,7 @@ def state_attributes(self) -> dict[str, Any] | None:

return {
ATTR_AUTO_UPDATE: self.auto_update,
ATTR_DISPLAY_PRECISION: self.display_precision,
ATTR_INSTALLED_VERSION: installed_version,
ATTR_IN_PROGRESS: in_progress,
ATTR_LATEST_VERSION: latest_version,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/update/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class UpdateEntityFeature(IntFlag):

ATTR_AUTO_UPDATE: Final = "auto_update"
ATTR_BACKUP: Final = "backup"
ATTR_DISPLAY_PRECISION: Final = "display_precision"
ATTR_INSTALLED_VERSION: Final = "installed_version"
ATTR_IN_PROGRESS: Final = "in_progress"
ATTR_LATEST_VERSION: Final = "latest_version"
Expand Down
1 change: 1 addition & 0 deletions tests/components/airgradient/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/airgradient/icon.png',
'friendly_name': 'Airgradient Firmware',
'in_progress': False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/devolo_home_network/icon.png',
'friendly_name': 'Mock Title Firmware',
'in_progress': False,
Expand Down
3 changes: 3 additions & 0 deletions tests/components/fritz/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/fritz/icon.png',
'friendly_name': 'Mock Title FRITZ!OS',
'in_progress': False,
Expand Down Expand Up @@ -93,6 +94,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/fritz/icon.png',
'friendly_name': 'Mock Title FRITZ!OS',
'in_progress': False,
Expand Down Expand Up @@ -150,6 +152,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/fritz/icon.png',
'friendly_name': 'Mock Title FRITZ!OS',
'in_progress': False,
Expand Down
1 change: 1 addition & 0 deletions tests/components/iron_os/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/iron_os/icon.png',
'friendly_name': 'Pinecil Firmware',
'in_progress': False,
Expand Down
2 changes: 2 additions & 0 deletions tests/components/lamarzocco/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png',
'friendly_name': 'GS01234 Gateway firmware',
'in_progress': False,
Expand Down Expand Up @@ -62,6 +63,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png',
'friendly_name': 'GS01234 Machine firmware',
'in_progress': False,
Expand Down
1 change: 1 addition & 0 deletions tests/components/nextcloud/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/nextcloud/icon.png',
'friendly_name': 'my.nc_url.local None',
'in_progress': False,
Expand Down
2 changes: 2 additions & 0 deletions tests/components/smlight/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/smlight/icon.png',
'friendly_name': 'Mock Title Core firmware',
'in_progress': False,
Expand Down Expand Up @@ -95,6 +96,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/smlight/icon.png',
'friendly_name': 'Mock Title Zigbee firmware',
'in_progress': False,
Expand Down
2 changes: 2 additions & 0 deletions tests/components/teslemetry/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png',
'friendly_name': 'Test Update',
'in_progress': False,
Expand Down Expand Up @@ -93,6 +94,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png',
'friendly_name': 'Test Update',
'in_progress': False,
Expand Down
1 change: 1 addition & 0 deletions tests/components/tessie/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/tessie/icon.png',
'friendly_name': 'Test Update',
'in_progress': False,
Expand Down
4 changes: 4 additions & 0 deletions tests/components/unifi/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png',
'friendly_name': 'Device 1',
'in_progress': False,
Expand Down Expand Up @@ -95,6 +96,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png',
'friendly_name': 'Device 2',
'in_progress': False,
Expand Down Expand Up @@ -153,6 +155,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png',
'friendly_name': 'Device 1',
'in_progress': False,
Expand Down Expand Up @@ -211,6 +214,7 @@
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'display_precision': 0,
'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png',
'friendly_name': 'Device 2',
'in_progress': False,
Expand Down
51 changes: 7 additions & 44 deletions tests/components/update/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,27 @@

from homeassistant.components.update import UpdateEntity

from tests.common import MockEntity

_LOGGER = logging.getLogger(__name__)


class MockUpdateEntity(MockEntity, UpdateEntity):
class MockUpdateEntity(UpdateEntity):
"""Mock UpdateEntity class."""

@property
def auto_update(self) -> bool:
"""Indicate if the device or service has auto update enabled."""
return self._handle("auto_update")

@property
def installed_version(self) -> str | None:
"""Version currently installed and in use."""
return self._handle("installed_version")

@property
def in_progress(self) -> bool | int | None:
"""Update installation progress."""
return self._handle("in_progress")

@property
def latest_version(self) -> str | None:
"""Latest version available for install."""
return self._handle("latest_version")

@property
def release_summary(self) -> str | None:
"""Summary of the release notes or changelog."""
return self._handle("release_summary")

@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
return self._handle("release_url")

@property
def title(self) -> str | None:
"""Title of the software."""
return self._handle("title")

@property
def update_percentage(self) -> int | None:
"""Update installation progress."""
return self._handle("update_percentage")
def __init__(self, **values: Any) -> None:
"""Initialize an entity."""
for key, val in values.items():
setattr(self, f"_attr_{key}", val)

def install(self, version: str | None, backup: bool, **kwargs: Any) -> None:
"""Install an update."""
if backup:
_LOGGER.info("Creating backup before installing update")

if version is not None:
self._values["installed_version"] = version
self._attr_installed_version = version
_LOGGER.info("Installed update with version: %s", version)
else:
self._values["installed_version"] = self.latest_version
self._attr_installed_version = self.latest_version
_LOGGER.info("Installed latest update")

def release_notes(self) -> str | None:
Expand Down
13 changes: 12 additions & 1 deletion tests/components/update/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,25 @@ def mock_update_entities() -> list[MockUpdateEntity]:
),
MockUpdateEntity(
name="Update Already in Progress",
unique_id="update_already_in_progres",
unique_id="update_already_in_progress",
installed_version="1.0.0",
latest_version="1.0.1",
in_progress=True,
supported_features=UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS,
update_percentage=50,
),
MockUpdateEntity(
name="Update Already in Progress Float",
unique_id="update_already_in_progress_float",
installed_version="1.0.0",
latest_version="1.0.1",
in_progress=True,
supported_features=UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS,
update_percentage=0.25,
display_precision=2,
),
MockUpdateEntity(
name="Update No Install",
unique_id="no_install",
Expand Down
Loading

0 comments on commit 7a3b927

Please sign in to comment.