Allow update entities to report progress as a float (#128930)
* Allow update entities to report progress as a float * Add test * Update snapshots * Update recorder test * Use _attr_* in MockUpdateEntitypull/129110/head
parent
87a2465a25
commit
bd55fe868d
homeassistant/components/update
tests/components
airgradient/snapshots
devolo_home_network/snapshots
fritz/snapshots
iron_os/snapshots
lamarzocco/snapshots
nextcloud/snapshots
smlight/snapshots
teslemetry/snapshots
tessie/snapshots
unifi/snapshots
|
@ -27,6 +27,7 @@ from homeassistant.util.hass_dict import HassKey
|
|||
from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_BACKUP,
|
||||
ATTR_DISPLAY_PRECISION,
|
||||
ATTR_IN_PROGRESS,
|
||||
ATTR_INSTALLED_VERSION,
|
||||
ATTR_LATEST_VERSION,
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -191,6 +193,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
|||
"auto_update",
|
||||
"installed_version",
|
||||
"device_class",
|
||||
"display_precision",
|
||||
"in_progress",
|
||||
"latest_version",
|
||||
"release_summary",
|
||||
|
@ -210,6 +213,7 @@ class UpdateEntity(
|
|||
|
||||
_entity_component_unrecorded_attributes = frozenset(
|
||||
{
|
||||
ATTR_DISPLAY_PRECISION,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_IN_PROGRESS,
|
||||
ATTR_RELEASE_SUMMARY,
|
||||
|
@ -221,6 +225,7 @@ 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
|
||||
|
@ -228,7 +233,7 @@ class UpdateEntity(
|
|||
_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
|
||||
|
||||
|
@ -258,6 +263,15 @@ class UpdateEntity(
|
|||
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."""
|
||||
|
@ -337,12 +351,12 @@ class UpdateEntity(
|
|||
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
|
||||
|
||||
|
@ -460,6 +474,7 @@ class UpdateEntity(
|
|||
|
||||
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,
|
||||
|
|
|
@ -23,6 +23,7 @@ SERVICE_SKIP: Final = "skip"
|
|||
|
||||
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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
|
|
@ -5,53 +5,16 @@ from typing import Any
|
|||
|
||||
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."""
|
||||
|
@ -59,10 +22,10 @@ class MockUpdateEntity(MockEntity, UpdateEntity):
|
|||
_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:
|
||||
|
|
|
@ -51,7 +51,7 @@ 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,
|
||||
|
@ -59,6 +59,17 @@ def mock_update_entities() -> list[MockUpdateEntity]:
|
|||
| 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",
|
||||
|
|
|
@ -18,6 +18,7 @@ from homeassistant.components.update import (
|
|||
)
|
||||
from homeassistant.components.update.const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_DISPLAY_PRECISION,
|
||||
ATTR_IN_PROGRESS,
|
||||
ATTR_INSTALLED_VERSION,
|
||||
ATTR_LATEST_VERSION,
|
||||
|
@ -92,6 +93,7 @@ async def test_update(hass: HomeAssistant) -> None:
|
|||
assert update.state == STATE_ON
|
||||
assert update.state_attributes == {
|
||||
ATTR_AUTO_UPDATE: False,
|
||||
ATTR_DISPLAY_PRECISION: 0,
|
||||
ATTR_INSTALLED_VERSION: "1.0.0",
|
||||
ATTR_IN_PROGRESS: False,
|
||||
ATTR_LATEST_VERSION: "1.0.1",
|
||||
|
@ -546,10 +548,20 @@ async def test_entity_with_backup_support(
|
|||
assert "Installed update with version: 0.9.8" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expected_display_precision", "expected_update_percentage"),
|
||||
[
|
||||
("update.update_already_in_progress", 0, 50),
|
||||
("update.update_already_in_progress_float", 2, 0.25),
|
||||
],
|
||||
)
|
||||
async def test_entity_already_in_progress(
|
||||
hass: HomeAssistant,
|
||||
mock_update_entities: list[MockUpdateEntity],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
entity_id: str,
|
||||
expected_display_precision: int,
|
||||
expected_update_percentage: float,
|
||||
) -> None:
|
||||
"""Test update install already in progress."""
|
||||
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
||||
|
@ -557,13 +569,14 @@ async def test_entity_already_in_progress(
|
|||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.update_already_in_progress")
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_DISPLAY_PRECISION] == expected_display_precision
|
||||
assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0"
|
||||
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
|
||||
assert state.attributes[ATTR_IN_PROGRESS] is True
|
||||
assert state.attributes[ATTR_UPDATE_PERCENTAGE] == 50
|
||||
assert state.attributes[ATTR_UPDATE_PERCENTAGE] == expected_update_percentage
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
|
@ -572,7 +585,7 @@ async def test_entity_already_in_progress(
|
|||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{ATTR_ENTITY_ID: "update.update_already_in_progress"},
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
@ -1056,6 +1069,7 @@ async def test_update_percentage_backwards_compatibility(
|
|||
|
||||
expected_attributes = {
|
||||
ATTR_AUTO_UPDATE: False,
|
||||
ATTR_DISPLAY_PRECISION: 0,
|
||||
ATTR_ENTITY_PICTURE: "https://brands.home-assistant.io/_/test/icon.png",
|
||||
ATTR_FRIENDLY_NAME: "legacy",
|
||||
ATTR_INSTALLED_VERSION: "1.0.0",
|
||||
|
|
|
@ -7,6 +7,7 @@ from datetime import timedelta
|
|||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.recorder.history import get_significant_states
|
||||
from homeassistant.components.update.const import (
|
||||
ATTR_DISPLAY_PRECISION,
|
||||
ATTR_IN_PROGRESS,
|
||||
ATTR_INSTALLED_VERSION,
|
||||
ATTR_RELEASE_SUMMARY,
|
||||
|
@ -35,6 +36,7 @@ async def test_exclude_attributes(
|
|||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("update.update_already_in_progress")
|
||||
assert state.attributes[ATTR_DISPLAY_PRECISION] == 0
|
||||
assert state.attributes[ATTR_IN_PROGRESS] is True
|
||||
assert state.attributes[ATTR_UPDATE_PERCENTAGE] == 50
|
||||
assert (
|
||||
|
@ -54,6 +56,7 @@ async def test_exclude_attributes(
|
|||
assert len(states) >= 1
|
||||
for entity_states in states.values():
|
||||
for state in entity_states:
|
||||
assert ATTR_DISPLAY_PRECISION not in state.attributes
|
||||
assert ATTR_ENTITY_PICTURE not in state.attributes
|
||||
assert ATTR_IN_PROGRESS not in state.attributes
|
||||
assert ATTR_RELEASE_SUMMARY not in state.attributes
|
||||
|
|
Loading…
Reference in New Issue