mirror of https://github.com/home-assistant/core
1111 lines
37 KiB
Python
1111 lines
37 KiB
Python
"""The tests for the Update component."""
|
|
|
|
from collections.abc import Generator
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
|
|
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_AUTO_UPDATE,
|
|
ATTR_DISPLAY_PRECISION,
|
|
ATTR_IN_PROGRESS,
|
|
ATTR_INSTALLED_VERSION,
|
|
ATTR_LATEST_VERSION,
|
|
ATTR_RELEASE_SUMMARY,
|
|
ATTR_RELEASE_URL,
|
|
ATTR_SKIPPED_VERSION,
|
|
ATTR_TITLE,
|
|
ATTR_UPDATE_PERCENTAGE,
|
|
UpdateEntityFeature,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
ATTR_ENTITY_PICTURE,
|
|
ATTR_FRIENDLY_NAME,
|
|
ATTR_SUPPORTED_FEATURES,
|
|
CONF_PLATFORM,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
STATE_UNKNOWN,
|
|
EntityCategory,
|
|
)
|
|
from homeassistant.core import HomeAssistant, State, callback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.event import async_track_state_change_event
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from tests.common import (
|
|
MockConfigEntry,
|
|
MockEntityPlatform,
|
|
MockModule,
|
|
MockPlatform,
|
|
mock_config_flow,
|
|
mock_integration,
|
|
mock_platform,
|
|
mock_restore_cache,
|
|
setup_test_component_platform,
|
|
)
|
|
from tests.typing import WebSocketGenerator
|
|
|
|
TEST_DOMAIN = "test"
|
|
|
|
|
|
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.platform = MockEntityPlatform(hass)
|
|
|
|
update._attr_installed_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.DIAGNOSTIC
|
|
assert (
|
|
update.entity_picture
|
|
== "https://brands.home-assistant.io/_/test_platform/icon.png"
|
|
)
|
|
assert update.installed_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_AUTO_UPDATE: False,
|
|
ATTR_DISPLAY_PRECISION: 0,
|
|
ATTR_INSTALLED_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",
|
|
ATTR_UPDATE_PERCENTAGE: None,
|
|
}
|
|
|
|
# Test no update available
|
|
update._attr_installed_version = "1.0.0"
|
|
update._attr_latest_version = "1.0.0"
|
|
assert update.state is STATE_OFF
|
|
|
|
# Test state becomes unknown if installed version is unknown
|
|
update._attr_installed_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_installed_version = "1.0.0"
|
|
update._attr_latest_version = None
|
|
assert update.state is None
|
|
|
|
# Test no update if new version is not an update
|
|
update._attr_installed_version = "1.0.0"
|
|
update._attr_latest_version = "0.9.0"
|
|
assert update.state is STATE_OFF
|
|
|
|
# Test update if new version is not considered a valid version
|
|
update._attr_installed_version = "1.0.0"
|
|
update._attr_latest_version = "awesome_update"
|
|
assert update.state is STATE_ON
|
|
|
|
# Test entity category becomes config when its possible to install
|
|
update._attr_supported_features = UpdateEntityFeature.INSTALL
|
|
assert update.entity_category is EntityCategory.CONFIG
|
|
|
|
# UpdateEntityDescription was set
|
|
update._attr_supported_features = 0
|
|
update.entity_description = UpdateEntityDescription(key="F5 - Its very refreshing")
|
|
assert update.device_class is None
|
|
assert update.entity_category is EntityCategory.CONFIG
|
|
del update.device_class
|
|
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(version=None, backup=True)
|
|
|
|
with pytest.raises(NotImplementedError):
|
|
update.install(version=None, backup=False)
|
|
|
|
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,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
) -> None:
|
|
"""Test entity with no updates."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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_INSTALLED_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_INSTALLED_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_INSTALLED_VERSION] == "1.0.0"
|
|
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
|
|
assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1"
|
|
|
|
# We can clear the skipped marker again
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
"clear_skipped",
|
|
{ATTR_ENTITY_ID: "update.update_no_install"},
|
|
blocking=True,
|
|
)
|
|
|
|
state = hass.states.get("update.update_no_install")
|
|
assert state
|
|
assert state.state == STATE_ON
|
|
assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0"
|
|
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
|
|
assert state.attributes[ATTR_SKIPPED_VERSION] is None
|
|
|
|
|
|
async def test_entity_with_no_updates(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
) -> None:
|
|
"""Test entity with no updates."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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_INSTALLED_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_auto_update(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test update entity that has auto update feature."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("update.update_with_auto_update")
|
|
assert state
|
|
assert state.state == STATE_ON
|
|
assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0"
|
|
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
|
|
assert state.attributes[ATTR_SKIPPED_VERSION] is None
|
|
|
|
# Should be able to manually install an update even if it can auto update
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_INSTALL,
|
|
{ATTR_ENTITY_ID: "update.update_with_auto_update"},
|
|
blocking=True,
|
|
)
|
|
|
|
# Should not be able to skip the update
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match="Skipping update is not supported for update.update_with_auto_update",
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_SKIP,
|
|
{ATTR_ENTITY_ID: "update.update_with_auto_update"},
|
|
blocking=True,
|
|
)
|
|
|
|
# Should not be able to clear a skipped the update
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match="Clearing skipped update is not supported for update.update_with_auto_update",
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
"clear_skipped",
|
|
{ATTR_ENTITY_ID: "update.update_with_auto_update"},
|
|
blocking=True,
|
|
)
|
|
|
|
|
|
async def test_entity_with_updates_available(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test basic update entity with updates available."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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_INSTALLED_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_INSTALLED_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_INSTALLED_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,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test update entity that has an unknown version."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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_INSTALLED_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,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test update entity that support specific version."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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_INSTALLED_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_INSTALLED_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_INSTALLED_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,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test update entity with backup support."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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_INSTALLED_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_INSTALLED_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_INSTALLED_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
|
|
|
|
|
|
@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)
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
|
await hass.async_block_till_done()
|
|
|
|
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] == expected_update_percentage
|
|
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match="Update installation already in progress for",
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_INSTALL,
|
|
{ATTR_ENTITY_ID: entity_id},
|
|
blocking=True,
|
|
)
|
|
|
|
# Check update percentage is suppressed when in_progress is False
|
|
entity = next(
|
|
entity for entity in mock_update_entities if entity.entity_id == entity_id
|
|
)
|
|
entity._attr_in_progress = False
|
|
entity.async_write_ha_state()
|
|
state = hass.states.get(entity_id)
|
|
assert state.attributes[ATTR_IN_PROGRESS] is False
|
|
assert state.attributes[ATTR_UPDATE_PERCENTAGE] is None
|
|
|
|
|
|
async def test_entity_without_progress_support(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test update entity without progress support.
|
|
|
|
In that case, progress is still handled by Home Assistant.
|
|
"""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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",
|
|
# pylint: disable-next=unnecessary-lambda
|
|
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_INSTALLED_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_INSTALLED_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_INSTALLED_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_INSTALLED_VERSION] == "1.0.1"
|
|
|
|
|
|
async def test_entity_without_progress_support_raising(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test update entity without progress support that raises during install.
|
|
|
|
In that case, progress is still handled by Home Assistant.
|
|
"""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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",
|
|
# pylint: disable-next=unnecessary-lambda
|
|
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,
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
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_INSTALLED_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_INSTALLED_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_INSTALLED_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_INSTALLED_VERSION] == "1.0.0"
|
|
|
|
|
|
async def test_restore_state(
|
|
hass: HomeAssistant, mock_update_entities: list[MockUpdateEntity]
|
|
) -> 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",
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
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_INSTALLED_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_release_notes(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
hass_ws_client: WebSocketGenerator,
|
|
) -> None:
|
|
"""Test getting the release notes over the websocket connection."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
|
await hass.async_block_till_done()
|
|
|
|
client = await hass_ws_client(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
await client.send_json(
|
|
{
|
|
"id": 1,
|
|
"type": "update/release_notes",
|
|
"entity_id": "update.update_with_release_notes",
|
|
}
|
|
)
|
|
result = await client.receive_json()
|
|
assert result["result"] == "Release notes"
|
|
|
|
|
|
async def test_release_notes_entity_not_found(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
hass_ws_client: WebSocketGenerator,
|
|
) -> None:
|
|
"""Test getting the release notes for not found entity."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
|
await hass.async_block_till_done()
|
|
|
|
client = await hass_ws_client(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
await client.send_json(
|
|
{
|
|
"id": 1,
|
|
"type": "update/release_notes",
|
|
"entity_id": "update.entity_not_found",
|
|
}
|
|
)
|
|
result = await client.receive_json()
|
|
assert result["error"]["code"] == "not_found"
|
|
assert result["error"]["message"] == "Entity not found"
|
|
|
|
|
|
async def test_release_notes_entity_does_not_support_release_notes(
|
|
hass: HomeAssistant,
|
|
mock_update_entities: list[MockUpdateEntity],
|
|
hass_ws_client: WebSocketGenerator,
|
|
) -> None:
|
|
"""Test getting the release notes for entity that does not support release notes."""
|
|
setup_test_component_platform(hass, DOMAIN, mock_update_entities)
|
|
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
|
await hass.async_block_till_done()
|
|
|
|
client = await hass_ws_client(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
await client.send_json(
|
|
{
|
|
"id": 1,
|
|
"type": "update/release_notes",
|
|
"entity_id": "update.update_available",
|
|
}
|
|
)
|
|
result = await client.receive_json()
|
|
assert result["error"]["code"] == "not_supported"
|
|
assert result["error"]["message"] == "Entity does not support release notes"
|
|
|
|
|
|
class MockFlow(ConfigFlow):
|
|
"""Test flow."""
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def config_flow_fixture(hass: HomeAssistant) -> Generator[None]:
|
|
"""Mock config flow."""
|
|
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
|
|
|
|
with mock_config_flow(TEST_DOMAIN, MockFlow):
|
|
yield
|
|
|
|
|
|
async def test_name(hass: HomeAssistant) -> None:
|
|
"""Test update name."""
|
|
|
|
async def async_setup_entry_init(
|
|
hass: HomeAssistant, config_entry: ConfigEntry
|
|
) -> bool:
|
|
"""Set up test config entry."""
|
|
await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
|
|
return True
|
|
|
|
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
|
|
mock_integration(
|
|
hass,
|
|
MockModule(
|
|
TEST_DOMAIN,
|
|
async_setup_entry=async_setup_entry_init,
|
|
),
|
|
)
|
|
|
|
# Unnamed update entity without device class -> no name
|
|
entity1 = UpdateEntity()
|
|
entity1.entity_id = "update.test1"
|
|
|
|
# Unnamed update entity with device class but has_entity_name False -> no name
|
|
entity2 = UpdateEntity()
|
|
entity2.entity_id = "update.test2"
|
|
entity2._attr_device_class = UpdateDeviceClass.FIRMWARE
|
|
|
|
# Unnamed update entity with device class and has_entity_name True -> named
|
|
entity3 = UpdateEntity()
|
|
entity3.entity_id = "update.test3"
|
|
entity3._attr_device_class = UpdateDeviceClass.FIRMWARE
|
|
entity3._attr_has_entity_name = True
|
|
|
|
# Unnamed update entity with device class and has_entity_name True -> named
|
|
entity4 = UpdateEntity()
|
|
entity4.entity_id = "update.test4"
|
|
entity4.entity_description = UpdateEntityDescription(
|
|
"test",
|
|
UpdateDeviceClass.FIRMWARE,
|
|
has_entity_name=True,
|
|
)
|
|
|
|
async def async_setup_entry_platform(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up test update platform via config entry."""
|
|
async_add_entities([entity1, entity2, entity3, entity4])
|
|
|
|
mock_platform(
|
|
hass,
|
|
f"{TEST_DOMAIN}.{DOMAIN}",
|
|
MockPlatform(async_setup_entry=async_setup_entry_platform),
|
|
)
|
|
|
|
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(entity1.entity_id)
|
|
assert state
|
|
assert "device_class" not in state.attributes
|
|
assert "friendly_name" not in state.attributes
|
|
|
|
state = hass.states.get(entity2.entity_id)
|
|
assert state
|
|
assert state.attributes.get("device_class") == "firmware"
|
|
assert "friendly_name" not in state.attributes
|
|
|
|
expected = {
|
|
"device_class": "firmware",
|
|
"friendly_name": "Firmware",
|
|
}
|
|
state = hass.states.get(entity3.entity_id)
|
|
assert state
|
|
assert expected.items() <= state.attributes.items()
|
|
|
|
state = hass.states.get(entity4.entity_id)
|
|
assert state
|
|
assert expected.items() <= state.attributes.items()
|
|
|
|
|
|
def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None:
|
|
"""Test deprecated supported features ints."""
|
|
|
|
class MockUpdateEntity(UpdateEntity):
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Return supported features."""
|
|
return 1
|
|
|
|
entity = MockUpdateEntity()
|
|
assert entity.supported_features_compat is UpdateEntityFeature(1)
|
|
assert "MockUpdateEntity" in caplog.text
|
|
assert "is using deprecated supported features values" in caplog.text
|
|
assert "Instead it should use" in caplog.text
|
|
assert "UpdateEntityFeature.INSTALL" in caplog.text
|
|
caplog.clear()
|
|
assert entity.supported_features_compat is UpdateEntityFeature(1)
|
|
assert "is using deprecated supported features values" not in caplog.text
|
|
|
|
|
|
async def test_deprecated_supported_features_ints_with_service_call(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test deprecated supported features ints with install service."""
|
|
|
|
async def async_setup_entry_init(
|
|
hass: HomeAssistant, config_entry: ConfigEntry
|
|
) -> bool:
|
|
"""Set up test config entry."""
|
|
await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
|
|
return True
|
|
|
|
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
|
|
mock_integration(
|
|
hass,
|
|
MockModule(
|
|
TEST_DOMAIN,
|
|
async_setup_entry=async_setup_entry_init,
|
|
),
|
|
)
|
|
|
|
class MockUpdateEntity(UpdateEntity):
|
|
_attr_supported_features = 1 | 2
|
|
|
|
def install(self, version: str | None = None, backup: bool = False) -> None:
|
|
"""Install an update."""
|
|
|
|
entity = MockUpdateEntity()
|
|
entity.entity_id = (
|
|
"update.test_deprecated_supported_features_ints_with_service_call"
|
|
)
|
|
|
|
async def async_setup_entry_platform(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up test update platform via config entry."""
|
|
async_add_entities([entity])
|
|
|
|
mock_platform(
|
|
hass,
|
|
f"{TEST_DOMAIN}.{DOMAIN}",
|
|
MockPlatform(async_setup_entry=async_setup_entry_platform),
|
|
)
|
|
|
|
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert "is using deprecated supported features values" in caplog.text
|
|
|
|
assert isinstance(entity.supported_features, int)
|
|
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match="Backup is not supported for update.test_deprecated_supported_features_ints_with_service_call",
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_INSTALL,
|
|
{
|
|
ATTR_VERSION: "0.9.9",
|
|
ATTR_BACKUP: True,
|
|
ATTR_ENTITY_ID: "update.test_deprecated_supported_features_ints_with_service_call",
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
|
|
async def test_custom_version_is_newer(hass: HomeAssistant) -> None:
|
|
"""Test UpdateEntity with overridden version_is_newer method."""
|
|
|
|
class MockUpdateEntity(UpdateEntity):
|
|
def version_is_newer(self, latest_version: str, installed_version: str) -> bool:
|
|
"""Return True if latest_version is newer than installed_version."""
|
|
return AwesomeVersion(
|
|
latest_version,
|
|
find_first_match=True,
|
|
ensure_strategy=[AwesomeVersionStrategy.SEMVER],
|
|
) > AwesomeVersion(
|
|
installed_version,
|
|
find_first_match=True,
|
|
ensure_strategy=[AwesomeVersionStrategy.SEMVER],
|
|
)
|
|
|
|
update = MockUpdateEntity()
|
|
update.hass = hass
|
|
update.platform = MockEntityPlatform(hass)
|
|
|
|
STABLE = "20230913-111730/v1.14.0-gcb84623"
|
|
BETA = "20231107-162609/v1.14.1-rc1-g0617c15"
|
|
|
|
# Set current installed version to STABLE
|
|
update._attr_installed_version = STABLE
|
|
update._attr_latest_version = BETA
|
|
|
|
assert update.installed_version == STABLE
|
|
assert update.latest_version == BETA
|
|
assert update.state == STATE_ON
|
|
|
|
# Set current installed version to BETA
|
|
update._attr_installed_version = BETA
|
|
update._attr_latest_version = STABLE
|
|
|
|
assert update.installed_version == BETA
|
|
assert update.latest_version == STABLE
|
|
assert update.state == STATE_OFF
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("supported_features", "extra_expected_attributes"),
|
|
[
|
|
(
|
|
0,
|
|
[
|
|
{},
|
|
{},
|
|
{},
|
|
{},
|
|
{},
|
|
{},
|
|
{},
|
|
],
|
|
),
|
|
(
|
|
UpdateEntityFeature.PROGRESS,
|
|
[
|
|
{ATTR_IN_PROGRESS: False},
|
|
{ATTR_IN_PROGRESS: False},
|
|
{ATTR_IN_PROGRESS: True, ATTR_UPDATE_PERCENTAGE: 0},
|
|
{ATTR_IN_PROGRESS: True},
|
|
{ATTR_IN_PROGRESS: True, ATTR_UPDATE_PERCENTAGE: 1},
|
|
{ATTR_IN_PROGRESS: True, ATTR_UPDATE_PERCENTAGE: 10},
|
|
{ATTR_IN_PROGRESS: True, ATTR_UPDATE_PERCENTAGE: 100},
|
|
],
|
|
),
|
|
],
|
|
)
|
|
async def test_update_percentage_backwards_compatibility(
|
|
hass: HomeAssistant,
|
|
supported_features: UpdateEntityFeature,
|
|
extra_expected_attributes: list[dict],
|
|
) -> None:
|
|
"""Test deriving update percentage from deprecated in_progress."""
|
|
update = MockUpdateEntity()
|
|
|
|
update._attr_installed_version = "1.0.0"
|
|
update._attr_latest_version = "1.0.1"
|
|
update._attr_name = "legacy"
|
|
update._attr_release_summary = "Summary"
|
|
update._attr_release_url = "https://example.com"
|
|
update._attr_supported_features = supported_features
|
|
update._attr_title = "Title"
|
|
|
|
setup_test_component_platform(hass, DOMAIN, [update])
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
|
await hass.async_block_till_done()
|
|
|
|
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",
|
|
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_SUPPORTED_FEATURES: supported_features,
|
|
ATTR_TITLE: "Title",
|
|
ATTR_UPDATE_PERCENTAGE: None,
|
|
}
|
|
|
|
state = hass.states.get("update.legacy")
|
|
assert state is not None
|
|
assert state.state == STATE_ON
|
|
assert state.attributes == expected_attributes | extra_expected_attributes[0]
|
|
|
|
in_progress_list = [False, 0, True, 1, 10, 100]
|
|
|
|
for i, in_progress in enumerate(in_progress_list):
|
|
update._attr_in_progress = in_progress
|
|
update.async_write_ha_state()
|
|
state = hass.states.get("update.legacy")
|
|
assert state.state == STATE_ON
|
|
assert (
|
|
state.attributes == expected_attributes | extra_expected_attributes[i + 1]
|
|
)
|