From 4bdd8cb459b2c6ddb2d427826e32bfc04b0c0dea Mon Sep 17 00:00:00 2001
From: starkillerOG <starkiller.og@gmail.com>
Date: Wed, 28 Sep 2022 19:21:30 +0200
Subject: [PATCH] Shelly migrate to update entity (#78305)

* Add update entity

* fixes

* fixes

* change to CONFIG catogory

* return latest version if no update available

* fixes

* Remove firmware binary_sensors and buttons

* import Callable from collections

* remove ota_update tests

* Update homeassistant/components/shelly/update.py

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* simplify

* fix mypy

* Create test_update.py

* fix isort

* add progress support

* fix styling

* fix update_tests

* fix styling

* do not exclude shelly update test

* bring coverage to 100%

* snake case

* snake case

* change str(x) to cast(str, x)

* simplify tests

* further simplify tests

* Split MOCK_SHELLY_COAP and MOCK_SHELLY_RPC

* fix issort

* fix status test

* fix isort

* run python3 -m script.hassfest

Co-authored-by: Shay Levy <levyshay1@gmail.com>
---
 homeassistant/components/shelly/__init__.py   |   2 +
 .../components/shelly/binary_sensor.py        |  26 --
 homeassistant/components/shelly/button.py     |  15 --
 homeassistant/components/shelly/update.py     | 236 ++++++++++++++++++
 tests/components/shelly/conftest.py           |  43 +++-
 tests/components/shelly/test_button.py        |  72 +-----
 tests/components/shelly/test_diagnostics.py   |  12 +-
 tests/components/shelly/test_update.py        | 101 ++++++++
 8 files changed, 384 insertions(+), 123 deletions(-)
 create mode 100644 homeassistant/components/shelly/update.py
 create mode 100644 tests/components/shelly/test_update.py

diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index dcb2f518144..ba03cf40f4f 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -80,6 +80,7 @@ BLOCK_PLATFORMS: Final = [
     Platform.LIGHT,
     Platform.SENSOR,
     Platform.SWITCH,
+    Platform.UPDATE,
 ]
 BLOCK_SLEEPING_PLATFORMS: Final = [
     Platform.BINARY_SENSOR,
@@ -94,6 +95,7 @@ RPC_PLATFORMS: Final = [
     Platform.LIGHT,
     Platform.SENSOR,
     Platform.SWITCH,
+    Platform.UPDATE,
 ]
 
 
diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py
index b33947ad6b7..cc6c4494ebb 100644
--- a/homeassistant/components/shelly/binary_sensor.py
+++ b/homeassistant/components/shelly/binary_sensor.py
@@ -143,19 +143,6 @@ REST_SENSORS: Final = {
         entity_registry_enabled_default=False,
         entity_category=EntityCategory.DIAGNOSTIC,
     ),
-    "fwupdate": RestBinarySensorDescription(
-        key="fwupdate",
-        name="Firmware Update",
-        device_class=BinarySensorDeviceClass.UPDATE,
-        value=lambda status, _: status["update"]["has_update"],
-        entity_registry_enabled_default=False,
-        extra_state_attributes=lambda status: {
-            "latest_stable_version": status["update"]["new_version"],
-            "installed_version": status["update"]["old_version"],
-            "beta_version": status["update"].get("beta_version", ""),
-        },
-        entity_category=EntityCategory.DIAGNOSTIC,
-    ),
 }
 
 RPC_SENSORS: Final = {
@@ -175,19 +162,6 @@ RPC_SENSORS: Final = {
         entity_registry_enabled_default=False,
         entity_category=EntityCategory.DIAGNOSTIC,
     ),
-    "fwupdate": RpcBinarySensorDescription(
-        key="sys",
-        sub_key="available_updates",
-        name="Firmware Update",
-        device_class=BinarySensorDeviceClass.UPDATE,
-        entity_registry_enabled_default=False,
-        extra_state_attributes=lambda status, shelly: {
-            "latest_stable_version": status.get("stable", {"version": ""})["version"],
-            "installed_version": shelly["ver"],
-            "beta_version": status.get("beta", {"version": ""})["version"],
-        },
-        entity_category=EntityCategory.DIAGNOSTIC,
-    ),
     "overtemp": RpcBinarySensorDescription(
         key="switch",
         sub_key="errors",
diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py
index f746d824984..144b100e8eb 100644
--- a/homeassistant/components/shelly/button.py
+++ b/homeassistant/components/shelly/button.py
@@ -37,21 +37,6 @@ class ShellyButtonDescription(ButtonEntityDescription, ShellyButtonDescriptionMi
 
 
 BUTTONS: Final = [
-    ShellyButtonDescription(
-        key="ota_update",
-        name="OTA Update",
-        device_class=ButtonDeviceClass.UPDATE,
-        entity_category=EntityCategory.CONFIG,
-        press_action=lambda wrapper: wrapper.async_trigger_ota_update(),
-    ),
-    ShellyButtonDescription(
-        key="ota_update_beta",
-        name="OTA Update Beta",
-        device_class=ButtonDeviceClass.UPDATE,
-        entity_registry_enabled_default=False,
-        entity_category=EntityCategory.CONFIG,
-        press_action=lambda wrapper: wrapper.async_trigger_ota_update(beta=True),
-    ),
     ShellyButtonDescription(
         key="reboot",
         name="Reboot",
diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py
new file mode 100644
index 00000000000..63972e9456d
--- /dev/null
+++ b/homeassistant/components/shelly/update.py
@@ -0,0 +1,236 @@
+"""Update entities for Shelly devices."""
+from __future__ import annotations
+
+from collections.abc import Callable
+from dataclasses import dataclass
+import logging
+from typing import Any, Final, cast
+
+from homeassistant.components.update import (
+    UpdateDeviceClass,
+    UpdateEntity,
+    UpdateEntityDescription,
+    UpdateEntityFeature,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import EntityCategory
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from . import BlockDeviceWrapper, RpcDeviceWrapper
+from .const import BLOCK, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DOMAIN
+from .entity import (
+    RestEntityDescription,
+    RpcEntityDescription,
+    ShellyRestAttributeEntity,
+    ShellyRpcAttributeEntity,
+    async_setup_entry_rest,
+    async_setup_entry_rpc,
+)
+from .utils import get_device_entry_gen
+
+LOGGER = logging.getLogger(__name__)
+
+
+@dataclass
+class RpcUpdateRequiredKeysMixin:
+    """Class for RPC update required keys."""
+
+    latest_version: Callable[[dict], Any]
+    install: Callable
+
+
+@dataclass
+class RestUpdateRequiredKeysMixin:
+    """Class for REST update required keys."""
+
+    latest_version: Callable[[dict], Any]
+    install: Callable
+
+
+@dataclass
+class RpcUpdateDescription(
+    RpcEntityDescription, UpdateEntityDescription, RpcUpdateRequiredKeysMixin
+):
+    """Class to describe a RPC update."""
+
+
+@dataclass
+class RestUpdateDescription(
+    RestEntityDescription, UpdateEntityDescription, RestUpdateRequiredKeysMixin
+):
+    """Class to describe a REST update."""
+
+
+REST_UPDATES: Final = {
+    "fwupdate": RestUpdateDescription(
+        name="Firmware Update",
+        key="fwupdate",
+        latest_version=lambda status: status["update"]["new_version"],
+        install=lambda wrapper: wrapper.async_trigger_ota_update(),
+        device_class=UpdateDeviceClass.FIRMWARE,
+        entity_category=EntityCategory.CONFIG,
+        entity_registry_enabled_default=True,
+    ),
+    "fwupdate_beta": RestUpdateDescription(
+        name="Beta Firmware Update",
+        key="fwupdate",
+        latest_version=lambda status: status["update"].get("beta_version"),
+        install=lambda wrapper: wrapper.async_trigger_ota_update(beta=True),
+        device_class=UpdateDeviceClass.FIRMWARE,
+        entity_category=EntityCategory.CONFIG,
+        entity_registry_enabled_default=False,
+    ),
+}
+
+RPC_UPDATES: Final = {
+    "fwupdate": RpcUpdateDescription(
+        name="Firmware Update",
+        key="sys",
+        sub_key="available_updates",
+        latest_version=lambda status: status.get("stable", {"version": None})[
+            "version"
+        ],
+        install=lambda wrapper: wrapper.async_trigger_ota_update(),
+        device_class=UpdateDeviceClass.FIRMWARE,
+        entity_category=EntityCategory.CONFIG,
+        entity_registry_enabled_default=True,
+    ),
+    "fwupdate_beta": RpcUpdateDescription(
+        name="Beta Firmware Update",
+        key="sys",
+        sub_key="available_updates",
+        latest_version=lambda status: status.get("beta", {"version": None})["version"],
+        install=lambda wrapper: wrapper.async_trigger_ota_update(beta=True),
+        device_class=UpdateDeviceClass.FIRMWARE,
+        entity_category=EntityCategory.CONFIG,
+        entity_registry_enabled_default=False,
+    ),
+}
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> 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 not config_entry.data[CONF_SLEEP_PERIOD]:
+        async_setup_entry_rest(
+            hass,
+            config_entry,
+            async_add_entities,
+            REST_UPDATES,
+            RestUpdateEntity,
+        )
+
+
+class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
+    """Represent a REST update entity."""
+
+    _attr_supported_features = (
+        UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
+    )
+    entity_description: RestUpdateDescription
+
+    def __init__(
+        self,
+        wrapper: BlockDeviceWrapper,
+        attribute: str,
+        description: RestEntityDescription,
+    ) -> None:
+        """Initialize update entity."""
+        super().__init__(wrapper, attribute, description)
+        self._in_progress_old_version: str | None = None
+
+    @property
+    def installed_version(self) -> str | None:
+        """Version currently in use."""
+        version = self.wrapper.device.status["update"]["old_version"]
+        if version is None:
+            return None
+
+        return cast(str, version)
+
+    @property
+    def latest_version(self) -> str | None:
+        """Latest version available for install."""
+        new_version = self.entity_description.latest_version(
+            self.wrapper.device.status,
+        )
+        if new_version is not None:
+            return cast(str, new_version)
+
+        return self.installed_version
+
+    @property
+    def in_progress(self) -> bool:
+        """Update installation in progress."""
+        return self._in_progress_old_version == self.installed_version
+
+    async def async_install(
+        self, version: str | None, backup: bool, **kwargs: Any
+    ) -> None:
+        """Install the latest firmware version."""
+        config_entry = self.wrapper.entry
+        block_wrapper = self.hass.data[DOMAIN][DATA_CONFIG_ENTRY][
+            config_entry.entry_id
+        ].get(BLOCK)
+        self._in_progress_old_version = self.installed_version
+        await self.entity_description.install(block_wrapper)
+
+
+class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
+    """Represent a RPC update entity."""
+
+    _attr_supported_features = (
+        UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
+    )
+    entity_description: RpcUpdateDescription
+
+    def __init__(
+        self,
+        wrapper: RpcDeviceWrapper,
+        key: str,
+        attribute: str,
+        description: RpcEntityDescription,
+    ) -> None:
+        """Initialize update entity."""
+        super().__init__(wrapper, key, attribute, description)
+        self._in_progress_old_version: str | None = None
+
+    @property
+    def installed_version(self) -> str | None:
+        """Version currently in use."""
+        if self.wrapper.device.shelly is None:
+            return None
+
+        return cast(str, self.wrapper.device.shelly["ver"])
+
+    @property
+    def latest_version(self) -> str | None:
+        """Latest version available for install."""
+        new_version = self.entity_description.latest_version(
+            self.wrapper.device.status[self.key][self.entity_description.sub_key],
+        )
+        if new_version is not None:
+            return cast(str, new_version)
+
+        return self.installed_version
+
+    @property
+    def in_progress(self) -> bool:
+        """Update installation in progress."""
+        return self._in_progress_old_version == self.installed_version
+
+    async def async_install(
+        self, version: str | None, backup: bool, **kwargs: Any
+    ) -> None:
+        """Install the latest firmware version."""
+        self._in_progress_old_version = self.installed_version
+        await self.entity_description.install(self.wrapper)
diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py
index c0e0bbadda2..49e86e118e3 100644
--- a/tests/components/shelly/conftest.py
+++ b/tests/components/shelly/conftest.py
@@ -3,13 +3,21 @@ from unittest.mock import AsyncMock, Mock, patch
 
 import pytest
 
-from homeassistant.components.shelly import BlockDeviceWrapper, RpcDeviceWrapper
+from homeassistant.components.shelly import (
+    BlockDeviceWrapper,
+    RpcDeviceWrapper,
+    RpcPollingWrapper,
+    ShellyDeviceRestWrapper,
+)
 from homeassistant.components.shelly.const import (
     BLOCK,
     DATA_CONFIG_ENTRY,
     DOMAIN,
     EVENT_SHELLY_CLICK,
+    REST,
+    REST_SENSORS_UPDATE_INTERVAL,
     RPC,
+    RPC_POLL,
 )
 from homeassistant.setup import async_setup_component
 
@@ -65,13 +73,27 @@ MOCK_CONFIG = {
     },
 }
 
-MOCK_SHELLY = {
+MOCK_SHELLY_COAP = {
     "mac": "test-mac",
     "auth": False,
     "fw": "20201124-092854/v1.9.0@57ac4ad8",
     "num_outputs": 2,
 }
 
+MOCK_SHELLY_RPC = {
+    "name": "Test Gen2",
+    "id": "shellyplus2pm-123456789abc",
+    "mac": "123456789ABC",
+    "model": "SNSW-002P16EU",
+    "gen": 2,
+    "fw_id": "20220830-130540/0.11.0-gfa1bc37",
+    "ver": "0.11.0",
+    "app": "Plus2PM",
+    "auth_en": False,
+    "auth_domain": None,
+    "profile": "cover",
+}
+
 MOCK_STATUS_COAP = {
     "update": {
         "status": "pending",
@@ -80,6 +102,7 @@ MOCK_STATUS_COAP = {
         "new_version": "some_new_version",
         "old_version": "some_old_version",
     },
+    "uptime": 5 * REST_SENSORS_UPDATE_INTERVAL,
 }
 
 
@@ -135,10 +158,11 @@ async def coap_wrapper(hass):
     device = Mock(
         blocks=MOCK_BLOCKS,
         settings=MOCK_SETTINGS,
-        shelly=MOCK_SHELLY,
+        shelly=MOCK_SHELLY_COAP,
         status=MOCK_STATUS_COAP,
         firmware_version="some fw string",
         update=AsyncMock(),
+        update_status=AsyncMock(),
         trigger_ota_update=AsyncMock(),
         trigger_reboot=AsyncMock(),
         initialized=True,
@@ -146,6 +170,10 @@ async def coap_wrapper(hass):
 
     hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
     hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {}
+    hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][
+        REST
+    ] = ShellyDeviceRestWrapper(hass, device, config_entry)
+
     wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][
         BLOCK
     ] = BlockDeviceWrapper(hass, config_entry, device)
@@ -157,7 +185,7 @@ async def coap_wrapper(hass):
 
 @pytest.fixture
 async def rpc_wrapper(hass):
-    """Setups a coap wrapper with mocked device."""
+    """Setups a rpc wrapper with mocked device."""
     await async_setup_component(hass, "shelly", {})
 
     config_entry = MockConfigEntry(
@@ -171,7 +199,7 @@ async def rpc_wrapper(hass):
         call_rpc=AsyncMock(),
         config=MOCK_CONFIG,
         event={},
-        shelly=MOCK_SHELLY,
+        shelly=MOCK_SHELLY_RPC,
         status=MOCK_STATUS_RPC,
         firmware_version="some fw string",
         update=AsyncMock(),
@@ -183,10 +211,13 @@ async def rpc_wrapper(hass):
 
     hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
     hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {}
+    hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][
+        RPC_POLL
+    ] = RpcPollingWrapper(hass, config_entry, device)
+
     wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][
         RPC
     ] = RpcDeviceWrapper(hass, config_entry, device)
-
     wrapper.async_setup()
 
     return wrapper
diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py
index 86a929ad6a4..8bbae677fb6 100644
--- a/tests/components/shelly/test_button.py
+++ b/tests/components/shelly/test_button.py
@@ -7,15 +7,15 @@ from homeassistant.helpers.entity_registry import async_get
 
 
 async def test_block_button(hass: HomeAssistant, coap_wrapper):
-    """Test block device OTA button."""
+    """Test block device reboot button."""
     assert coap_wrapper
 
     entity_registry = async_get(hass)
     entity_registry.async_get_or_create(
         BUTTON_DOMAIN,
         DOMAIN,
-        "test_name_ota_update_beta",
-        suggested_object_id="test_name_ota_update_beta",
+        "test_name_reboot",
+        suggested_object_id="test_name_reboot",
         disabled_by=None,
     )
     hass.async_create_task(
@@ -23,37 +23,6 @@ async def test_block_button(hass: HomeAssistant, coap_wrapper):
     )
     await hass.async_block_till_done()
 
-    # stable channel button
-    state = hass.states.get("button.test_name_ota_update")
-    assert state
-    assert state.state == STATE_UNKNOWN
-
-    await hass.services.async_call(
-        BUTTON_DOMAIN,
-        SERVICE_PRESS,
-        {ATTR_ENTITY_ID: "button.test_name_ota_update"},
-        blocking=True,
-    )
-    await hass.async_block_till_done()
-    assert coap_wrapper.device.trigger_ota_update.call_count == 1
-    coap_wrapper.device.trigger_ota_update.assert_called_with(beta=False)
-
-    # beta channel button
-    state = hass.states.get("button.test_name_ota_update_beta")
-
-    assert state
-    assert state.state == STATE_UNKNOWN
-
-    await hass.services.async_call(
-        BUTTON_DOMAIN,
-        SERVICE_PRESS,
-        {ATTR_ENTITY_ID: "button.test_name_ota_update_beta"},
-        blocking=True,
-    )
-    await hass.async_block_till_done()
-    assert coap_wrapper.device.trigger_ota_update.call_count == 2
-    coap_wrapper.device.trigger_ota_update.assert_called_with(beta=True)
-
     # reboot button
     state = hass.states.get("button.test_name_reboot")
 
@@ -78,8 +47,8 @@ async def test_rpc_button(hass: HomeAssistant, rpc_wrapper):
     entity_registry.async_get_or_create(
         BUTTON_DOMAIN,
         DOMAIN,
-        "test_name_ota_update_beta",
-        suggested_object_id="test_name_ota_update_beta",
+        "test_name_reboot",
+        suggested_object_id="test_name_reboot",
         disabled_by=None,
     )
 
@@ -88,37 +57,6 @@ async def test_rpc_button(hass: HomeAssistant, rpc_wrapper):
     )
     await hass.async_block_till_done()
 
-    # stable channel button
-    state = hass.states.get("button.test_name_ota_update")
-    assert state
-    assert state.state == STATE_UNKNOWN
-
-    await hass.services.async_call(
-        BUTTON_DOMAIN,
-        SERVICE_PRESS,
-        {ATTR_ENTITY_ID: "button.test_name_ota_update"},
-        blocking=True,
-    )
-    await hass.async_block_till_done()
-    assert rpc_wrapper.device.trigger_ota_update.call_count == 1
-    rpc_wrapper.device.trigger_ota_update.assert_called_with(beta=False)
-
-    # beta channel button
-    state = hass.states.get("button.test_name_ota_update_beta")
-
-    assert state
-    assert state.state == STATE_UNKNOWN
-
-    await hass.services.async_call(
-        BUTTON_DOMAIN,
-        SERVICE_PRESS,
-        {ATTR_ENTITY_ID: "button.test_name_ota_update_beta"},
-        blocking=True,
-    )
-    await hass.async_block_till_done()
-    assert rpc_wrapper.device.trigger_ota_update.call_count == 2
-    rpc_wrapper.device.trigger_ota_update.assert_called_with(beta=True)
-
     # reboot button
     state = hass.states.get("button.test_name_reboot")
 
diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py
index 4f3ca89e548..137149f1608 100644
--- a/tests/components/shelly/test_diagnostics.py
+++ b/tests/components/shelly/test_diagnostics.py
@@ -6,6 +6,8 @@ from homeassistant.components.shelly.const import DOMAIN
 from homeassistant.components.shelly.diagnostics import TO_REDACT
 from homeassistant.core import HomeAssistant
 
+from .conftest import MOCK_STATUS_COAP
+
 from tests.components.diagnostics import get_diagnostics_for_config_entry
 
 RELAY_BLOCK_ID = 0
@@ -33,15 +35,7 @@ async def test_block_config_entry_diagnostics(
             "sw_version": coap_wrapper.sw_version,
         },
         "device_settings": {"coiot": {"update_period": 15}},
-        "device_status": {
-            "update": {
-                "beta_version": "some_beta_version",
-                "has_update": True,
-                "new_version": "some_new_version",
-                "old_version": "some_old_version",
-                "status": "pending",
-            }
-        },
+        "device_status": MOCK_STATUS_COAP,
     }
 
 
diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py
new file mode 100644
index 00000000000..70c9b7a8e67
--- /dev/null
+++ b/tests/components/shelly/test_update.py
@@ -0,0 +1,101 @@
+"""Tests for Shelly update platform."""
+from homeassistant.components.shelly.const import DOMAIN
+from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
+from homeassistant.components.update.const import SERVICE_INSTALL
+from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNKNOWN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_component import async_update_entity
+from homeassistant.helpers.entity_registry import async_get
+
+
+async def test_block_update(hass: HomeAssistant, coap_wrapper, monkeypatch):
+    """Test block device update entity."""
+    assert coap_wrapper
+
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        "test_name_update",
+        suggested_object_id="test_name_update",
+        disabled_by=None,
+    )
+    hass.async_create_task(
+        hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, UPDATE_DOMAIN)
+    )
+    await hass.async_block_till_done()
+
+    # update entity
+    await async_update_entity(hass, "update.test_name_firmware_update")
+    await hass.async_block_till_done()
+    state = hass.states.get("update.test_name_firmware_update")
+
+    assert state
+    assert state.state == STATE_ON
+
+    await hass.services.async_call(
+        UPDATE_DOMAIN,
+        SERVICE_INSTALL,
+        {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+    assert coap_wrapper.device.trigger_ota_update.call_count == 1
+
+    monkeypatch.setitem(coap_wrapper.device.status["update"], "old_version", None)
+    monkeypatch.setitem(coap_wrapper.device.status["update"], "new_version", None)
+
+    # update entity
+    await async_update_entity(hass, "update.test_name_firmware_update")
+    await hass.async_block_till_done()
+    state = hass.states.get("update.test_name_firmware_update")
+
+    assert state
+    assert state.state == STATE_UNKNOWN
+
+
+async def test_rpc_update(hass: HomeAssistant, rpc_wrapper, monkeypatch):
+    """Test rpc device update entity."""
+    assert rpc_wrapper
+
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        "test_name_update",
+        suggested_object_id="test_name_update",
+        disabled_by=None,
+    )
+
+    hass.async_create_task(
+        hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, UPDATE_DOMAIN)
+    )
+    await hass.async_block_till_done()
+
+    # update entity
+    await async_update_entity(hass, "update.test_name_firmware_update")
+    await hass.async_block_till_done()
+    state = hass.states.get("update.test_name_firmware_update")
+
+    assert state
+    assert state.state == STATE_ON
+
+    await hass.services.async_call(
+        UPDATE_DOMAIN,
+        SERVICE_INSTALL,
+        {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+    assert rpc_wrapper.device.trigger_ota_update.call_count == 1
+
+    monkeypatch.setitem(rpc_wrapper.device.status["sys"], "available_updates", {})
+    rpc_wrapper.device.shelly = None
+
+    # update entity
+    await async_update_entity(hass, "update.test_name_firmware_update")
+    await hass.async_block_till_done()
+    state = hass.states.get("update.test_name_firmware_update")
+
+    assert state
+    assert state.state == STATE_UNKNOWN
-- 
GitLab