From 857df05308775559579cf29cb77a546195344608 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 30 Jan 2023 19:05:13 +0200 Subject: [PATCH] Add Shelly Gen2 update entity for sleeping devices (#86837) --- homeassistant/components/shelly/__init__.py | 1 + homeassistant/components/shelly/update.py | 52 ++++++- homeassistant/components/update/__init__.py | 2 + tests/components/shelly/test_update.py | 158 ++++++++++++++++++-- 4 files changed, 197 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b57ec6fa96d..7cb1f697765 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -74,6 +74,7 @@ RPC_PLATFORMS: Final = [ RPC_SLEEPING_PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.SENSOR, + Platform.UPDATE, ] COAP_SCHEMA: Final = vol.Schema( diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 065b0469e39..8f2b7438945 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -9,6 +9,8 @@ from typing import Any, Final, cast from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.components.update import ( + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, UpdateDeviceClass, UpdateEntity, UpdateEntityDescription, @@ -27,6 +29,7 @@ from .entity import ( RpcEntityDescription, ShellyRestAttributeEntity, ShellyRpcAttributeEntity, + ShellySleepingRpcAttributeEntity, async_setup_entry_rest, async_setup_entry_rpc, ) @@ -95,7 +98,6 @@ RPC_UPDATES: Final = { beta=False, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, ), "fwupdate_beta": RpcUpdateDescription( name="Beta firmware update", @@ -117,9 +119,19 @@ async def async_setup_entry( ) -> None: """Set up update entities for Shelly component.""" if get_device_entry_gen(config_entry) == 2: - return async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_UPDATES, RpcUpdateEntity - ) + if config_entry.data[CONF_SLEEP_PERIOD]: + async_setup_entry_rpc( + hass, + config_entry, + async_add_entities, + RPC_UPDATES, + RpcSleepingUpdateEntity, + ) + else: + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_UPDATES, RpcUpdateEntity + ) + return if not config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_rest( @@ -268,3 +280,35 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): self.coordinator.entry.async_start_reauth(self.hass) else: LOGGER.debug("OTA update call successful") + + +class RpcSleepingUpdateEntity(ShellySleepingRpcAttributeEntity, UpdateEntity): + """Represent a RPC sleeping update entity.""" + + entity_description: RpcUpdateDescription + + @property + def installed_version(self) -> str | None: + """Version currently in use.""" + if self.coordinator.device.initialized: + return cast(str, self.coordinator.device.shelly["ver"]) + + if self.last_state is None: + return None + + return self.last_state.attributes.get(ATTR_INSTALLED_VERSION) + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + if self.coordinator.device.initialized: + new_version = self.entity_description.latest_version(self.sub_status) + if new_version: + return cast(str, new_version) + + return self.installed_version + + if self.last_state is None: + return None + + return self.last_state.attributes.get(ATTR_LATEST_VERSION) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 09207665748..c56c2799ecf 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -60,6 +60,8 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(UpdateDeviceClass)) __all__ = [ "ATTR_BACKUP", + "ATTR_INSTALLED_VERSION", + "ATTR_LATEST_VERSION", "ATTR_VERSION", "DEVICE_CLASSES_SCHEMA", "DOMAIN", diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index a6bd08bf272..e057a21dd95 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -11,14 +11,29 @@ from homeassistant.components.update import ( ATTR_LATEST_VERSION, DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, + UpdateEntityFeature, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get -from . import MOCK_MAC, init_integration, mock_rest_update +from . import ( + MOCK_MAC, + init_integration, + mock_rest_update, + register_device, + register_entity, +) + +from tests.common import mock_restore_cache async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch): @@ -40,6 +55,8 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch) assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "2" assert state.attributes[ATTR_IN_PROGRESS] is False + supported_feat = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_feat == UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS await hass.services.async_call( UPDATE_DOMAIN, @@ -196,14 +213,6 @@ async def test_block_update_auth_error( async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): """Test RPC device update entity.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-sys-fwupdate", - suggested_object_id="test_name_firmware_update", - disabled_by=None, - ) monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( mock_rpc_device.status["sys"], @@ -219,6 +228,8 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "2" assert state.attributes[ATTR_IN_PROGRESS] is False + supported_feat = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_feat == UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS await hass.services.async_call( UPDATE_DOMAIN, @@ -235,7 +246,7 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2") - await mock_rest_update(hass) + mock_rpc_device.mock_update() state = hass.states.get("update.test_name_firmware_update") assert state.state == STATE_OFF @@ -244,6 +255,129 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): assert state.attributes[ATTR_IN_PROGRESS] is False +async def test_rpc_sleeping_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): + """Test RPC sleeping device update entity.""" + monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") + monkeypatch.setitem( + mock_rpc_device.status["sys"], + "available_updates", + { + "stable": {"version": "2"}, + }, + ) + entity_id = f"{UPDATE_DOMAIN}.test_name_firmware_update" + await init_integration(hass, 2, sleep_period=1000) + + # Entity should be created when device is online + assert hass.states.get(entity_id) is None + + # Make device online + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is False + assert state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature(0) + + monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2") + mock_rpc_device.mock_update() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "2" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is False + assert state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature(0) + + +async def test_rpc_restored_sleeping_update( + hass, mock_rpc_device, device_reg, monkeypatch +): + """Test RPC restored update entity.""" + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + UPDATE_DOMAIN, + "test_name_firmware_update", + "sys-fwupdate", + entry, + ) + + attr = {ATTR_INSTALLED_VERSION: "1", ATTR_LATEST_VERSION: "2"} + mock_restore_cache(hass, [State(entity_id, STATE_ON, attributes=attr)]) + monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2") + monkeypatch.setitem(mock_rpc_device.status["sys"], "available_updates", {}) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is False + assert state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature(0) + + # Make device online + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "2" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is False + assert state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature(0) + + +async def test_rpc_restored_sleeping_update_no_last_state( + hass, mock_rpc_device, device_reg, monkeypatch +): + """Test RPC restored update entity missing last state.""" + monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") + monkeypatch.setitem( + mock_rpc_device.status["sys"], + "available_updates", + { + "stable": {"version": "2"}, + }, + ) + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + UPDATE_DOMAIN, + "test_name_firmware_update", + "sys-fwupdate", + entry, + ) + + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + + # Make device online + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is False + assert state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature(0) + + async def test_rpc_beta_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): """Test RPC device beta update entity.""" entity_registry = async_get(hass)