Add update entity platform (#68248)

Co-authored-by: Glenn Waters <glenn@watrs.ca>
This commit is contained in:
Franck Nijhof 2022-03-21 11:02:48 +01:00 committed by GitHub
parent 830cc278d3
commit 073fb40b79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1589 additions and 0 deletions

View File

@ -39,6 +39,7 @@ base_platforms: &base_platforms
- homeassistant/components/stt/**
- homeassistant/components/switch/**
- homeassistant/components/tts/**
- homeassistant/components/update/**
- homeassistant/components/vacuum/**
- homeassistant/components/water_heater/**
- homeassistant/components/weather/**

View File

@ -207,6 +207,7 @@ homeassistant.components.tts.*
homeassistant.components.twentemilieu.*
homeassistant.components.unifiprotect.*
homeassistant.components.upcloud.*
homeassistant.components.update.*
homeassistant.components.uptime.*
homeassistant.components.uptimerobot.*
homeassistant.components.usb.*

View File

@ -1059,6 +1059,8 @@ tests/components/upb/* @gwww
homeassistant/components/upc_connect/* @pvizeli @fabaff
homeassistant/components/upcloud/* @scop
tests/components/upcloud/* @scop
homeassistant/components/update/* @home-assistant/core
tests/components/update/* @home-assistant/core
homeassistant/components/updater/* @home-assistant/core
tests/components/updater/* @home-assistant/core
homeassistant/components/upnp/* @StevenLooman @ehendrix23

View File

@ -40,6 +40,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
"sensor",
"siren",
"switch",
"update",
"vacuum",
"water_heater",
]

View File

@ -0,0 +1,153 @@
"""Demo platform that offers fake update entities."""
from __future__ import annotations
import asyncio
from typing import Any
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
from homeassistant.components.update.const import UpdateEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
FAKE_INSTALL_SLEEP_TIME = 0.5
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up demo update entities."""
async_add_entities(
[
DemoUpdate(
unique_id="update_no_install",
name="Demo Update No Install",
title="Awesomesoft Inc.",
current_version="1.0.0",
latest_version="1.0.1",
release_summary="Awesome update, fixing everything!",
release_url="https://www.example.com/release/1.0.1",
support_install=False,
),
DemoUpdate(
unique_id="update_2_date",
name="Demo No Update",
title="AdGuard Home",
current_version="1.0.0",
latest_version="1.0.0",
),
DemoUpdate(
unique_id="update_addon",
name="Demo add-on",
title="AdGuard Home",
current_version="1.0.0",
latest_version="1.0.1",
release_summary="Awesome update, fixing everything!",
release_url="https://www.example.com/release/1.0.1",
),
DemoUpdate(
unique_id="update_light_bulb",
name="Demo Living Room Bulb Update",
title="Philips Lamps Firmware",
current_version="1.93.3",
latest_version="1.94.2",
release_summary="Added support for effects",
release_url="https://www.example.com/release/1.93.3",
device_class=UpdateDeviceClass.FIRMWARE,
),
DemoUpdate(
unique_id="update_support_progress",
name="Demo Update with Progress",
title="Philips Lamps Firmware",
current_version="1.93.3",
latest_version="1.94.2",
support_progress=True,
release_summary="Added support for effects",
release_url="https://www.example.com/release/1.93.3",
device_class=UpdateDeviceClass.FIRMWARE,
),
]
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Demo config entry."""
await async_setup_platform(hass, {}, async_add_entities)
async def _fake_install() -> None:
"""Fake install an update."""
await asyncio.sleep(FAKE_INSTALL_SLEEP_TIME)
class DemoUpdate(UpdateEntity):
"""Representation of a demo update entity."""
_attr_should_poll = False
def __init__(
self,
*,
unique_id: str,
name: str,
title: str | None,
current_version: str | None,
latest_version: str | None,
release_summary: str | None = None,
release_url: str | None = None,
support_progress: bool = False,
support_install: bool = True,
device_class: UpdateDeviceClass | None = None,
) -> None:
"""Initialize the Demo select entity."""
self._attr_current_version = current_version
self._attr_device_class = device_class
self._attr_latest_version = latest_version
self._attr_name = name or DEVICE_DEFAULT_NAME
self._attr_release_summary = release_summary
self._attr_release_url = release_url
self._attr_title = title
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=name,
)
if support_install:
self._attr_supported_features |= (
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.BACKUP
| UpdateEntityFeature.SPECIFIC_VERSION
)
if support_progress:
self._attr_supported_features |= UpdateEntityFeature.PROGRESS
async def async_install(
self,
version: str | None = None,
backup: bool | None = None,
**kwargs: Any,
) -> None:
"""Install an update."""
if self.supported_features & UpdateEntityFeature.PROGRESS:
for progress in range(0, 100, 10):
self._attr_in_progress = progress
self.async_write_ha_state()
await _fake_install()
self._attr_in_progress = False
self._attr_current_version = (
version if version is not None else self.latest_version
)
self.async_write_ha_state()

View File

@ -0,0 +1,353 @@
"""Component to allow for providing device or service updates."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any, Final, final
import voluptuous as vol
from homeassistant.backports.enum import StrEnum
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_validation import (
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import EntityCategory, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from .const import (
ATTR_BACKUP,
ATTR_CURRENT_VERSION,
ATTR_IN_PROGRESS,
ATTR_LATEST_VERSION,
ATTR_RELEASE_SUMMARY,
ATTR_RELEASE_URL,
ATTR_SKIPPED_VERSION,
ATTR_TITLE,
ATTR_VERSION,
DOMAIN,
SERVICE_INSTALL,
SERVICE_SKIP,
UpdateEntityFeature,
)
SCAN_INTERVAL = timedelta(minutes=15)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
_LOGGER = logging.getLogger(__name__)
class UpdateDeviceClass(StrEnum):
"""Device class for update."""
FIRMWARE = "firmware"
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(UpdateDeviceClass))
__all__ = [
"ATTR_BACKUP",
"ATTR_VERSION",
"DEVICE_CLASSES_SCHEMA",
"DOMAIN",
"PLATFORM_SCHEMA_BASE",
"PLATFORM_SCHEMA",
"SERVICE_INSTALL",
"SERVICE_SKIP",
"UpdateDeviceClass",
"UpdateEntity",
"UpdateEntityDescription",
"UpdateEntityFeature",
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Select entities."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
component.async_register_entity_service(
SERVICE_INSTALL,
{
vol.Optional(ATTR_VERSION): cv.string,
vol.Optional(ATTR_BACKUP): cv.boolean,
},
async_install,
[UpdateEntityFeature.INSTALL],
)
component.async_register_entity_service(
SERVICE_SKIP,
{},
UpdateEntity.async_skip.__name__,
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
component: EntityComponent = hass.data[DOMAIN]
return await component.async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
component: EntityComponent = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None:
"""Service call wrapper to validate the call."""
# If version is not specified, but no update is available.
if (version := service_call.data.get(ATTR_VERSION)) is None and (
entity.current_version == entity.latest_version or entity.latest_version is None
):
raise HomeAssistantError(f"No update available for {entity.name}")
# If version is specified, but not supported by the entity.
if (
version is not None
and not entity.supported_features & UpdateEntityFeature.SPECIFIC_VERSION
):
raise HomeAssistantError(
f"Installing a specific version is not supported for {entity.name}"
)
# If backup is requested, but not supported by the entity.
if (
backup := service_call.data.get(ATTR_BACKUP)
) and not entity.supported_features & UpdateEntityFeature.BACKUP:
raise HomeAssistantError(f"Backup is not supported for {entity.name}")
# Update is already in progress.
if entity.in_progress is not False:
raise HomeAssistantError(
f"Update installation already in progress for {entity.name}"
)
await entity.async_install_with_progress(version, backup)
@dataclass
class UpdateEntityDescription(EntityDescription):
"""A class that describes update entities."""
device_class: UpdateDeviceClass | str | None = None
entity_category: EntityCategory | None = EntityCategory.CONFIG
class UpdateEntity(RestoreEntity):
"""Representation of an update entity."""
entity_description: UpdateEntityDescription
_attr_current_version: str | None = None
_attr_device_class: UpdateDeviceClass | str | None
_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: int = 0
_attr_title: str | None = None
__skipped_version: str | None = None
__in_progress: bool = False
@property
def current_version(self) -> str | None:
"""Version currently in use."""
return self._attr_current_version
@property
def device_class(self) -> UpdateDeviceClass | str | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
return self._attr_device_class
if hasattr(self, "entity_description"):
return self.entity_description.device_class
return None
@property
def entity_category(self) -> EntityCategory | str | None:
"""Return the category of the entity, if any."""
if hasattr(self, "_attr_entity_category"):
return self._attr_entity_category
if hasattr(self, "entity_description"):
return self.entity_description.entity_category
return EntityCategory.CONFIG
@property
def in_progress(self) -> bool | int | None:
"""Update installation progress.
Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used.
Can either return a boolean (True if in progress, False if not)
or an integer to indicate the progress in from 0 to 100%.
"""
return self._attr_in_progress
@property
def latest_version(self) -> str | None:
"""Latest version available for install."""
return self._attr_latest_version
@property
def release_summary(self) -> str | None:
"""Summary of the release notes or changelog.
This is not suitable for long changelogs, but merely suitable
for a short excerpt update description of max 255 characters.
"""
return self._attr_release_summary
@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
return self._attr_release_url
@property
def supported_features(self) -> int:
"""Flag supported features."""
return self._attr_supported_features
@property
def title(self) -> str | None:
"""Title of the software.
This helps to differentiate between the device or entity name
versus the title of the software installed.
"""
return self._attr_title
@final
async def async_skip(self) -> None:
"""Skip the current offered version to update."""
if (latest_version := self.latest_version) is None:
raise HomeAssistantError(f"Cannot skip an unknown version for {self.name}")
if self.current_version == latest_version:
raise HomeAssistantError(f"No update available to skip for {self.name}")
self.__skipped_version = latest_version
self.async_write_ha_state()
async def async_install(
self,
version: str | None = None,
backup: bool | None = None,
**kwargs: Any,
) -> None:
"""Install an update.
Version can be specified to install a specific version. When `None`, the
latest version needs to be installed.
The backup parameter indicates a backup should be taken before
installing the update.
"""
await self.hass.async_add_executor_job(self.install, version, backup)
def install(
self,
version: str | None = None,
backup: bool | None = None,
**kwargs: Any,
) -> None:
"""Install an update.
Version can be specified to install a specific version. When `None`, the
latest version needs to be installed.
The backup parameter indicates a backup should be taken before
installing the update.
"""
raise NotImplementedError()
@property
@final
def state(self) -> str | None:
"""Return the entity state."""
if (current_version := self.current_version) is None or (
latest_version := self.latest_version
) is None:
return None
if latest_version not in (current_version, self.__skipped_version):
return STATE_ON
return STATE_OFF
@final
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return state attributes."""
if (release_summary := self.release_summary) is not None:
release_summary = release_summary[:255]
# If entity supports progress, return the in_progress value.
# Otherwise, we use the internal progress value.
if self.supported_features & UpdateEntityFeature.PROGRESS:
in_progress = self.in_progress
else:
in_progress = self.__in_progress
# Clear skipped version in case it matches the current version or
# the latest version diverged.
if (
self.__skipped_version == self.current_version
or self.__skipped_version != self.latest_version
):
self.__skipped_version = None
return {
ATTR_CURRENT_VERSION: self.current_version,
ATTR_IN_PROGRESS: in_progress,
ATTR_LATEST_VERSION: self.latest_version,
ATTR_RELEASE_SUMMARY: release_summary,
ATTR_RELEASE_URL: self.release_url,
ATTR_SKIPPED_VERSION: self.__skipped_version,
ATTR_TITLE: self.title,
}
@final
async def async_install_with_progress(
self,
version: str | None = None,
backup: bool | None = None,
) -> None:
"""Install update and handle progress if needed.
Handles setting the in_progress state in case the entity doesn't
support it natively.
"""
if not self.supported_features & UpdateEntityFeature.PROGRESS:
self.__in_progress = True
self.async_write_ha_state()
try:
await self.async_install(version, backup)
finally:
# No matter what happens, we always stop progress in the end
self._attr_in_progress = False
self.__in_progress = False
self.async_write_ha_state()
async def async_internal_added_to_hass(self) -> None:
"""Call when the update entity is added to hass.
It is used to restore the skipped version, if any.
"""
await super().async_internal_added_to_hass()
state = await self.async_get_last_state()
if state is not None and state.attributes.get(ATTR_SKIPPED_VERSION) is not None:
self.__skipped_version = state.attributes[ATTR_SKIPPED_VERSION]

View File

@ -0,0 +1,30 @@
"""Constants for the update component."""
from __future__ import annotations
from enum import IntEnum
from typing import Final
DOMAIN: Final = "update"
class UpdateEntityFeature(IntEnum):
"""Supported features of the update entity."""
INSTALL = 1
SPECIFIC_VERSION = 2
PROGRESS = 4
BACKUP = 8
SERVICE_INSTALL: Final = "install"
SERVICE_SKIP: Final = "skip"
ATTR_BACKUP: Final = "backup"
ATTR_CURRENT_VERSION: Final = "current_version"
ATTR_IN_PROGRESS: Final = "in_progress"
ATTR_LATEST_VERSION: Final = "latest_version"
ATTR_RELEASE_SUMMARY: Final = "release_summary"
ATTR_RELEASE_URL: Final = "release_url"
ATTR_SKIPPED_VERSION: Final = "skipped_version"
ATTR_TITLE: Final = "title"
ATTR_VERSION: Final = "version"

View File

@ -0,0 +1,7 @@
{
"domain": "update",
"name": "Update",
"documentation": "https://www.home-assistant.io/integrations/update",
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
}

View File

@ -0,0 +1,27 @@
install:
name: Install update
description: Install an update for this device or service
target:
entity:
domain: update
fields:
version:
name: Version
description: Version to install, if omitted, the latest version will be installed.
required: false
example: "1.0.0"
selector:
text:
backup:
name: Backup
description: Backup before installing the update, if supported by the integration.
required: false
selector:
boolean:
skip:
name: Skip update
description: Mark currently available update as skipped.
target:
entity:
domain: update

View File

@ -0,0 +1,30 @@
"""Helper to test significant update state changes."""
from __future__ import annotations
from typing import Any
from homeassistant.core import HomeAssistant, callback
from .const import ATTR_CURRENT_VERSION, ATTR_LATEST_VERSION
@callback
def async_check_significant_change(
hass: HomeAssistant,
old_state: str,
old_attrs: dict,
new_state: str,
new_attrs: dict,
**kwargs: Any,
) -> bool | None:
"""Test if state significantly changed."""
if old_state != new_state:
return True
if old_attrs.get(ATTR_CURRENT_VERSION) != new_attrs.get(ATTR_CURRENT_VERSION):
return True
if old_attrs.get(ATTR_LATEST_VERSION) != new_attrs.get(ATTR_LATEST_VERSION):
return True
return False

View File

@ -0,0 +1,3 @@
{
"title": "Update"
}

View File

@ -50,6 +50,7 @@ class Platform(StrEnum):
SWITCH = "switch"
TTS = "tts"
VACUUM = "vacuum"
UPDATE = "update"
WATER_HEATER = "water_heater"
WEATHER = "weather"

View File

@ -2079,6 +2079,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.update.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.uptime.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -0,0 +1,157 @@
"""The tests for the demo update platform."""
from unittest.mock import patch
import pytest
from homeassistant.components.update import DOMAIN, SERVICE_INSTALL, UpdateDeviceClass
from homeassistant.components.update.const import (
ATTR_CURRENT_VERSION,
ATTR_IN_PROGRESS,
ATTR_LATEST_VERSION,
ATTR_RELEASE_SUMMARY,
ATTR_RELEASE_URL,
ATTR_TITLE,
)
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True)
async def setup_demo_update(hass: HomeAssistant) -> None:
"""Initialize setup demo update entity."""
assert await async_setup_component(hass, DOMAIN, {"update": {"platform": "demo"}})
await hass.async_block_till_done()
def test_setup_params(hass: HomeAssistant) -> None:
"""Test the initial parameters."""
state = hass.states.get("update.demo_update_no_install")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_TITLE] == "Awesomesoft Inc."
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert (
state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!"
)
assert state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.0.1"
state = hass.states.get("update.demo_no_update")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_TITLE] == "AdGuard Home"
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0"
assert state.attributes[ATTR_RELEASE_SUMMARY] is None
assert state.attributes[ATTR_RELEASE_URL] is None
state = hass.states.get("update.demo_add_on")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_TITLE] == "AdGuard Home"
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert (
state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!"
)
assert state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.0.1"
state = hass.states.get("update.demo_living_room_bulb_update")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware"
assert state.attributes[ATTR_CURRENT_VERSION] == "1.93.3"
assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2"
assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects"
assert (
state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.93.3"
)
assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE
state = hass.states.get("update.demo_update_with_progress")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware"
assert state.attributes[ATTR_CURRENT_VERSION] == "1.93.3"
assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2"
assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects"
assert (
state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.93.3"
)
assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE
async def test_update_with_progress(hass: HomeAssistant) -> None:
"""Test update with progress."""
state = hass.states.get("update.demo_update_with_progress")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_IN_PROGRESS] is False
events = []
async_track_state_change_event(
hass,
"update.demo_update_with_progress",
callback(lambda event: events.append(event)),
)
with patch("homeassistant.components.demo.update.FAKE_INSTALL_SLEEP_TIME", new=0):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.demo_update_with_progress"},
blocking=True,
)
assert len(events) == 10
assert events[0].data["new_state"].state == STATE_ON
assert events[0].data["new_state"].attributes[ATTR_IN_PROGRESS] == 10
assert events[1].data["new_state"].attributes[ATTR_IN_PROGRESS] == 20
assert events[2].data["new_state"].attributes[ATTR_IN_PROGRESS] == 30
assert events[3].data["new_state"].attributes[ATTR_IN_PROGRESS] == 40
assert events[4].data["new_state"].attributes[ATTR_IN_PROGRESS] == 50
assert events[5].data["new_state"].attributes[ATTR_IN_PROGRESS] == 60
assert events[6].data["new_state"].attributes[ATTR_IN_PROGRESS] == 70
assert events[7].data["new_state"].attributes[ATTR_IN_PROGRESS] == 80
assert events[8].data["new_state"].attributes[ATTR_IN_PROGRESS] == 90
assert events[9].data["new_state"].attributes[ATTR_IN_PROGRESS] is False
assert events[9].data["new_state"].state == STATE_OFF
async def test_update_with_progress_raising(hass: HomeAssistant) -> None:
"""Test update with progress failing to install."""
state = hass.states.get("update.demo_update_with_progress")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_IN_PROGRESS] is False
events = []
async_track_state_change_event(
hass,
"update.demo_update_with_progress",
callback(lambda event: events.append(event)),
)
with patch(
"homeassistant.components.demo.update._fake_install",
side_effect=[None, None, None, None, RuntimeError],
) as fake_sleep, pytest.raises(RuntimeError):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.demo_update_with_progress"},
blocking=True,
)
await hass.async_block_till_done()
assert fake_sleep.call_count == 5
assert len(events) == 5
assert events[0].data["new_state"].state == STATE_ON
assert events[0].data["new_state"].attributes[ATTR_IN_PROGRESS] == 10
assert events[1].data["new_state"].attributes[ATTR_IN_PROGRESS] == 20
assert events[2].data["new_state"].attributes[ATTR_IN_PROGRESS] == 30
assert events[3].data["new_state"].attributes[ATTR_IN_PROGRESS] == 40
assert events[4].data["new_state"].attributes[ATTR_IN_PROGRESS] is False
assert events[4].data["new_state"].state == STATE_ON

View File

@ -0,0 +1 @@
"""The tests for the Update integration."""

View File

@ -0,0 +1,583 @@
"""The tests for the Update component."""
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.update import (
ATTR_BACKUP,
ATTR_VERSION,
DOMAIN,
SERVICE_INSTALL,
SERVICE_SKIP,
UpdateDeviceClass,
UpdateEntity,
UpdateEntityDescription,
)
from homeassistant.components.update.const import (
ATTR_CURRENT_VERSION,
ATTR_IN_PROGRESS,
ATTR_LATEST_VERSION,
ATTR_RELEASE_SUMMARY,
ATTR_RELEASE_URL,
ATTR_SKIPPED_VERSION,
ATTR_TITLE,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_PLATFORM,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.setup import async_setup_component
from tests.common import mock_restore_cache
class MockUpdateEntity(UpdateEntity):
"""Mock UpdateEntity to use in tests."""
async def test_update(hass: HomeAssistant) -> None:
"""Test getting data from the mocked update entity."""
update = MockUpdateEntity()
update.hass = hass
update._attr_current_version = "1.0.0"
update._attr_latest_version = "1.0.1"
update._attr_release_summary = "Summary"
update._attr_release_url = "https://example.com"
update._attr_title = "Title"
assert update.entity_category is EntityCategory.CONFIG
assert update.current_version == "1.0.0"
assert update.latest_version == "1.0.1"
assert update.release_summary == "Summary"
assert update.release_url == "https://example.com"
assert update.title == "Title"
assert update.in_progress is False
assert update.state == STATE_ON
assert update.state_attributes == {
ATTR_CURRENT_VERSION: "1.0.0",
ATTR_IN_PROGRESS: False,
ATTR_LATEST_VERSION: "1.0.1",
ATTR_RELEASE_SUMMARY: "Summary",
ATTR_RELEASE_URL: "https://example.com",
ATTR_SKIPPED_VERSION: None,
ATTR_TITLE: "Title",
}
# Test no update available
update._attr_current_version = "1.0.0"
update._attr_latest_version = "1.0.0"
assert update.state is STATE_OFF
# Test state becomes unknown if current version is unknown
update._attr_current_version = None
update._attr_latest_version = "1.0.0"
assert update.state is None
# Test state becomes unknown if latest version is unknown
update._attr_current_version = "1.0.0"
update._attr_latest_version = None
assert update.state is None
# UpdateEntityDescription was set
update.entity_description = UpdateEntityDescription(key="F5 - Its very refreshing")
assert update.device_class is None
assert update.entity_category is EntityCategory.CONFIG
update.entity_description = UpdateEntityDescription(
key="F5 - Its very refreshing",
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=None,
)
assert update.device_class is UpdateDeviceClass.FIRMWARE
assert update.entity_category is None
# Device class via attribute (override entity description)
update._attr_device_class = None
assert update.device_class is None
update._attr_device_class = UpdateDeviceClass.FIRMWARE
assert update.device_class is UpdateDeviceClass.FIRMWARE
# Entity Attribute via attribute (override entity description)
update._attr_entity_category = None
assert update.entity_category is None
update._attr_entity_category = EntityCategory.DIAGNOSTIC
assert update.entity_category is EntityCategory.DIAGNOSTIC
with pytest.raises(NotImplementedError):
await update.async_install()
with pytest.raises(NotImplementedError):
update.install()
update.install = MagicMock()
await update.async_install(version="1.0.1", backup=True)
assert update.install.called
assert update.install.call_args[0][0] == "1.0.1"
assert update.install.call_args[0][1] is True
async def test_entity_with_no_install(
hass: HomeAssistant,
enable_custom_integrations: None,
) -> None:
"""Test entity with no updates."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
# Update is available
state = hass.states.get("update.update_no_install")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
# Should not be able to install as the entity doesn't support that
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.update_no_install"},
blocking=True,
)
# Nothing changed
state = hass.states.get("update.update_no_install")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_SKIPPED_VERSION] is None
# We can mark the update as skipped
await hass.services.async_call(
DOMAIN,
SERVICE_SKIP,
{ATTR_ENTITY_ID: "update.update_no_install"},
blocking=True,
)
state = hass.states.get("update.update_no_install")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1"
async def test_entity_with_no_updates(
hass: HomeAssistant,
enable_custom_integrations: None,
) -> None:
"""Test entity with no updates."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
# No update available
state = hass.states.get("update.no_update")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0"
# Should not be able to skip when there is no update available
with pytest.raises(HomeAssistantError, match="No update available to skip for"):
await hass.services.async_call(
DOMAIN,
SERVICE_SKIP,
{ATTR_ENTITY_ID: "update.no_update"},
blocking=True,
)
# Should not be able to install an update when there is no update available
with pytest.raises(HomeAssistantError, match="No update available for"):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.no_update"},
blocking=True,
)
# Updating to a specific version is not supported by this entity
with pytest.raises(
HomeAssistantError,
match="Installing a specific version is not supported for",
):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_VERSION: "0.9.0", ATTR_ENTITY_ID: "update.no_update"},
blocking=True,
)
async def test_entity_with_updates_available(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test basic update entity with updates available."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
# Entity has an update available
state = hass.states.get("update.update_available")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_SKIPPED_VERSION] is None
# Skip skip the update
await hass.services.async_call(
DOMAIN,
SERVICE_SKIP,
{ATTR_ENTITY_ID: "update.update_available"},
blocking=True,
)
# The state should have changed to off, skipped version should be set
state = hass.states.get("update.update_available")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1"
# Even though skipped, we can still update if we want to
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.update_available"},
blocking=True,
)
# The state should have changed to off, skipped version should be set
state = hass.states.get("update.update_available")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.1"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_SKIPPED_VERSION] is None
assert "Installed latest update" in caplog.text
async def test_entity_with_unknown_version(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update entity that has an unknown version."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
state = hass.states.get("update.update_unknown")
assert state
assert state.state == STATE_UNKNOWN
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] is None
assert state.attributes[ATTR_SKIPPED_VERSION] is None
# Should not be able to install an update when there is no update available
with pytest.raises(HomeAssistantError, match="No update available for"):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.update_unknown"},
blocking=True,
)
# Should not be to skip the update
with pytest.raises(HomeAssistantError, match="Cannot skip an unknown version for"):
await hass.services.async_call(
DOMAIN,
SERVICE_SKIP,
{ATTR_ENTITY_ID: "update.update_unknown"},
blocking=True,
)
async def test_entity_with_specific_version(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update entity that support specific version."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
state = hass.states.get("update.update_specific_version")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0"
# Update to a specific version
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_VERSION: "0.9.9", ATTR_ENTITY_ID: "update.update_specific_version"},
blocking=True,
)
# Version has changed, state should be on as there is an update available
state = hass.states.get("update.update_specific_version")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_VERSION] == "0.9.9"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0"
assert "Installed update with version: 0.9.9" in caplog.text
# Update back to the latest version
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.update_specific_version"},
blocking=True,
)
state = hass.states.get("update.update_specific_version")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0"
assert "Installed latest update" in caplog.text
# This entity does not support doing a backup before upgrade
with pytest.raises(HomeAssistantError, match="Backup is not supported for"):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{
ATTR_VERSION: "0.9.9",
ATTR_BACKUP: True,
ATTR_ENTITY_ID: "update.update_specific_version",
},
blocking=True,
)
async def test_entity_with_backup_support(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update entity with backup support."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
# This entity support backing up before install the update
state = hass.states.get("update.update_backup")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
# Without a backup
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{
ATTR_BACKUP: False,
ATTR_ENTITY_ID: "update.update_backup",
},
blocking=True,
)
state = hass.states.get("update.update_backup")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.1"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert "Creating backup before installing update" not in caplog.text
assert "Installed latest update" in caplog.text
# Specific version, do create a backup this time
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{
ATTR_BACKUP: True,
ATTR_VERSION: "0.9.8",
ATTR_ENTITY_ID: "update.update_backup",
},
blocking=True,
)
# This entity support backing up before install the update
state = hass.states.get("update.update_backup")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_VERSION] == "0.9.8"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert "Creating backup before installing update" in caplog.text
assert "Installed update with version: 0.9.8" in caplog.text
async def test_entity_already_in_progress(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update install already in progress."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
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
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_IN_PROGRESS] == 50
with pytest.raises(
HomeAssistantError,
match="Update installation already in progress for",
):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.update_already_in_progress"},
blocking=True,
)
async def test_entity_without_progress_support(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update entity without progress support.
In that case, progress is still handled by Home Assistant.
"""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
events = []
async_track_state_change_event(
hass, "update.update_available", callback(lambda event: events.append(event))
)
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.update_available"},
blocking=True,
)
assert len(events) == 2
assert events[0].data.get("old_state").attributes[ATTR_IN_PROGRESS] is False
assert events[0].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert events[0].data.get("new_state").attributes[ATTR_IN_PROGRESS] is True
assert events[0].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert events[1].data.get("old_state").attributes[ATTR_IN_PROGRESS] is True
assert events[1].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert events[1].data.get("new_state").attributes[ATTR_IN_PROGRESS] is False
assert events[1].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.1"
async def test_entity_without_progress_support_raising(
hass: HomeAssistant,
enable_custom_integrations: None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update entity without progress support that raises during install.
In that case, progress is still handled by Home Assistant.
"""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
events = []
async_track_state_change_event(
hass, "update.update_available", callback(lambda event: events.append(event))
)
with patch(
"homeassistant.components.update.UpdateEntity.async_install",
side_effect=RuntimeError,
), pytest.raises(RuntimeError):
await hass.services.async_call(
DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.update_available"},
blocking=True,
)
assert len(events) == 2
assert events[0].data.get("old_state").attributes[ATTR_IN_PROGRESS] is False
assert events[0].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert events[0].data.get("new_state").attributes[ATTR_IN_PROGRESS] is True
assert events[0].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert events[1].data.get("old_state").attributes[ATTR_IN_PROGRESS] is True
assert events[1].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert events[1].data.get("new_state").attributes[ATTR_IN_PROGRESS] is False
assert events[1].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0"
async def test_restore_state(
hass: HomeAssistant, enable_custom_integrations: None
) -> None:
"""Test we restore skipped version state."""
mock_restore_cache(
hass,
(
State(
"update.update_available",
STATE_ON, # Incorrect, but helps checking if it is ignored
{
ATTR_SKIPPED_VERSION: "1.0.1",
},
),
),
)
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
state = hass.states.get("update.update_available")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1"

View File

@ -0,0 +1,90 @@
"""Test the update significant change platform."""
from homeassistant.components.update.const import (
ATTR_CURRENT_VERSION,
ATTR_IN_PROGRESS,
ATTR_LATEST_VERSION,
ATTR_RELEASE_SUMMARY,
ATTR_RELEASE_URL,
ATTR_SKIPPED_VERSION,
ATTR_TITLE,
)
from homeassistant.components.update.significant_change import (
async_check_significant_change,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
async def test_significant_change(hass: HomeAssistant) -> None:
"""Detect update significant changes."""
assert async_check_significant_change(hass, STATE_ON, {}, STATE_OFF, {})
assert async_check_significant_change(hass, STATE_OFF, {}, STATE_ON, {})
assert not async_check_significant_change(hass, STATE_OFF, {}, STATE_OFF, {})
assert not async_check_significant_change(hass, STATE_ON, {}, STATE_ON, {})
attrs = {
ATTR_CURRENT_VERSION: "1.0.0",
ATTR_IN_PROGRESS: False,
ATTR_LATEST_VERSION: "1.0.1",
ATTR_RELEASE_SUMMARY: "Fixes!",
ATTR_RELEASE_URL: "https://www.example.com",
ATTR_SKIPPED_VERSION: None,
ATTR_TITLE: "Piece of Software",
}
assert not async_check_significant_change(hass, STATE_ON, attrs, STATE_ON, attrs)
assert async_check_significant_change(
hass,
STATE_ON,
attrs,
STATE_ON,
attrs.copy() | {ATTR_CURRENT_VERSION: "1.0.1"},
)
assert async_check_significant_change(
hass,
STATE_ON,
attrs,
STATE_ON,
attrs.copy() | {ATTR_LATEST_VERSION: "1.0.2"},
)
assert not async_check_significant_change(
hass,
STATE_ON,
attrs,
STATE_ON,
attrs.copy() | {ATTR_IN_PROGRESS: True},
)
assert not async_check_significant_change(
hass,
STATE_ON,
attrs,
STATE_ON,
attrs.copy() | {ATTR_RELEASE_SUMMARY: "More fixes!"},
)
assert not async_check_significant_change(
hass,
STATE_ON,
attrs,
STATE_ON,
attrs.copy() | {ATTR_RELEASE_URL: "https://www.example.com/changed_url"},
)
assert not async_check_significant_change(
hass,
STATE_ON,
attrs,
STATE_ON,
attrs.copy() | {ATTR_SKIPPED_VERSION: "1.0.0"},
)
assert not async_check_significant_change(
hass,
STATE_ON,
attrs,
STATE_ON,
attrs.copy() | {ATTR_TITLE: "Renamed the software..."},
)

View File

@ -0,0 +1,138 @@
"""
Provide a mock update platform.
Call init before using it in your tests to ensure clean test data.
"""
from __future__ import annotations
import logging
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from tests.common import MockEntity
ENTITIES = []
_LOGGER = logging.getLogger(__name__)
class MockUpdateEntity(MockEntity, UpdateEntity):
"""Mock UpdateEntity class."""
@property
def current_version(self) -> str | None:
"""Version currently in use."""
return self._handle("current_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")
def install(
self,
version: str | None = None,
backup: bool | None = None,
) -> None:
"""Install an update."""
if backup:
_LOGGER.info("Creating backup before installing update")
if version is not None:
self._values["current_version"] = version
_LOGGER.info(f"Installed update with version: {version}")
else:
self._values["current_version"] = self.latest_version
_LOGGER.info("Installed latest update")
def init(empty=False):
"""Initialize the platform with entities."""
global ENTITIES
ENTITIES = (
[]
if empty
else [
MockUpdateEntity(
name="No Update",
unique_id="no_update",
current_version="1.0.0",
latest_version="1.0.0",
supported_features=UpdateEntityFeature.INSTALL,
),
MockUpdateEntity(
name="Update Available",
unique_id="update_available",
current_version="1.0.0",
latest_version="1.0.1",
supported_features=UpdateEntityFeature.INSTALL,
),
MockUpdateEntity(
name="Update Unknown",
unique_id="update_unknown",
current_version="1.0.0",
latest_version=None,
supported_features=UpdateEntityFeature.INSTALL,
),
MockUpdateEntity(
name="Update Specific Version",
unique_id="update_specific_version",
current_version="1.0.0",
latest_version="1.0.0",
supported_features=UpdateEntityFeature.INSTALL
| UpdateEntityFeature.SPECIFIC_VERSION,
),
MockUpdateEntity(
name="Update Backup",
unique_id="update_backup",
current_version="1.0.0",
latest_version="1.0.1",
supported_features=UpdateEntityFeature.INSTALL
| UpdateEntityFeature.SPECIFIC_VERSION
| UpdateEntityFeature.BACKUP,
),
MockUpdateEntity(
name="Update Already in Progress",
unique_id="update_already_in_progres",
current_version="1.0.0",
latest_version="1.0.1",
in_progress=50,
supported_features=UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS,
),
MockUpdateEntity(
name="Update No Install",
unique_id="no_install",
current_version="1.0.0",
latest_version="1.0.1",
),
]
)
async def async_setup_platform(
hass, config, async_add_entities_callback, discovery_info=None
):
"""Return mock entities."""
async_add_entities_callback(ENTITIES)