diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 0a78840e66e..6b30f45b023 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from .const import ( + ATTR_AUTO_UPDATE, ATTR_BACKUP, ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, @@ -93,7 +94,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SKIP, {}, - UpdateEntity.async_skip.__name__, + async_skip, ) websocket_api.async_register_command(hass, websocket_release_notes) @@ -144,6 +145,13 @@ async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None await entity.async_install_with_progress(version, backup) +async def async_skip(entity: UpdateEntity, service_call: ServiceCall) -> None: + """Service call wrapper to validate the call.""" + if entity.auto_update: + raise HomeAssistantError(f"Skipping update is not supported for {entity.name}") + await entity.async_skip() + + @dataclass class UpdateEntityDescription(EntityDescription): """A class that describes update entities.""" @@ -156,6 +164,7 @@ class UpdateEntity(RestoreEntity): """Representation of an update entity.""" entity_description: UpdateEntityDescription + _attr_auto_update: bool = False _attr_current_version: str | None = None _attr_device_class: UpdateDeviceClass | str | None _attr_in_progress: bool | int = False @@ -168,6 +177,11 @@ class UpdateEntity(RestoreEntity): __skipped_version: str | None = None __in_progress: bool = False + @property + def auto_update(self) -> bool: + """Indicate if the device or service has auto update enabled.""" + return self._attr_auto_update + @property def current_version(self) -> str | None: """Version currently in use.""" @@ -329,6 +343,7 @@ class UpdateEntity(RestoreEntity): self.__skipped_version = None return { + ATTR_AUTO_UPDATE: self.auto_update, ATTR_CURRENT_VERSION: self.current_version, ATTR_IN_PROGRESS: in_progress, ATTR_LATEST_VERSION: self.latest_version, diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py index 7c70572f458..916d2cbaceb 100644 --- a/homeassistant/components/update/const.py +++ b/homeassistant/components/update/const.py @@ -20,6 +20,7 @@ class UpdateEntityFeature(IntEnum): SERVICE_INSTALL: Final = "install" SERVICE_SKIP: Final = "skip" +ATTR_AUTO_UPDATE: Final = "auto_update" ATTR_BACKUP: Final = "backup" ATTR_CURRENT_VERSION: Final = "current_version" ATTR_IN_PROGRESS: Final = "in_progress" diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index c6845f73d0c..b8db7f24f4a 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -16,6 +16,7 @@ from homeassistant.components.update import ( UpdateEntityDescription, ) from homeassistant.components.update.const import ( + ATTR_AUTO_UPDATE, ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, ATTR_LATEST_VERSION, @@ -65,6 +66,7 @@ async def test_update(hass: HomeAssistant) -> None: assert update.in_progress is False assert update.state == STATE_ON assert update.state_attributes == { + ATTR_AUTO_UPDATE: False, ATTR_CURRENT_VERSION: "1.0.0", ATTR_IN_PROGRESS: False, ATTR_LATEST_VERSION: "1.0.1", @@ -242,6 +244,46 @@ async def test_entity_with_no_updates( ) +async def test_entity_with_auto_update( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity that has auto update feature.""" + 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_with_auto_update") + 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 + + # 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 to skip the update + with pytest.raises( + HomeAssistantError, + match="Skipping update is not supported for Update with auto update", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SKIP, + {ATTR_ENTITY_ID: "update.update_with_auto_update"}, + blocking=True, + ) + + async def test_entity_with_updates_available( hass: HomeAssistant, enable_custom_integrations: None, diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py index ce8cd3869f5..fc5ee31246e 100644 --- a/tests/testing_config/custom_components/test/update.py +++ b/tests/testing_config/custom_components/test/update.py @@ -20,6 +20,11 @@ _LOGGER = logging.getLogger(__name__) class MockUpdateEntity(MockEntity, 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 current_version(self) -> str | None: """Version currently in use.""" @@ -135,6 +140,14 @@ def init(empty=False): latest_version="1.0.1", supported_features=UpdateEntityFeature.RELEASE_NOTES, ), + MockUpdateEntity( + name="Update with auto update", + unique_id="with_auto_update", + current_version="1.0.0", + latest_version="1.0.1", + auto_update=True, + supported_features=UpdateEntityFeature.INSTALL, + ), ] )