diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index fdb94b3bdb0..014355116c1 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -14,13 +14,11 @@ from aioshelly.rpc_device import RpcDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - ATTR_BETA, ATTR_CHANNEL, ATTR_CLICK_TYPE, ATTR_DEVICE, @@ -249,43 +247,6 @@ class ShellyBlockCoordinator(DataUpdateCoordinator): self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) - async def async_trigger_ota_update(self, beta: bool = False) -> None: - """Trigger or schedule an ota update.""" - update_data = self.device.status["update"] - LOGGER.debug("OTA update service - update_data: %s", update_data) - - if not update_data["has_update"] and not beta: - LOGGER.warning("No OTA update available for device %s", self.name) - return - - if beta and not update_data.get("beta_version"): - LOGGER.warning( - "No OTA update on beta channel available for device %s", self.name - ) - return - - if update_data["status"] == "updating": - LOGGER.warning("OTA update already in progress for %s", self.name) - return - - new_version = update_data["new_version"] - if beta: - new_version = update_data["beta_version"] - LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - self.device.firmware_version, - new_version, - ) - try: - result = await self.device.trigger_ota_update(beta=beta) - except DeviceConnectionError as err: - raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err - except InvalidAuthError: - self.entry.async_start_reauth(self.hass) - else: - LOGGER.debug("Result of OTA update call: %s", result) - def shutdown(self) -> None: """Shutdown the coordinator.""" self.device.shutdown() @@ -480,46 +441,6 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) - async def async_trigger_ota_update(self, beta: bool = False) -> None: - """Trigger an ota update.""" - - update_data = self.device.status["sys"]["available_updates"] - LOGGER.debug("OTA update service - update_data: %s", update_data) - - if not bool(update_data) or (not update_data.get("stable") and not beta): - LOGGER.warning("No OTA update available for device %s", self.name) - return - - if beta and not update_data.get(ATTR_BETA): - LOGGER.warning( - "No OTA update on beta channel available for device %s", self.name - ) - return - - new_version = update_data.get("stable", {"version": ""})["version"] - if beta: - new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] - - assert self.device.shelly - LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - self.device.firmware_version, - new_version, - ) - try: - await self.device.trigger_ota_update(beta=beta) - except DeviceConnectionError as err: - raise HomeAssistantError( - f"OTA update connection error: {repr(err)}" - ) from err - except RpcCallError as err: - raise HomeAssistantError(f"OTA update request error: {repr(err)}") from err - except InvalidAuthError: - self.entry.async_start_reauth(self.hass) - else: - LOGGER.debug("OTA update call successful") - async def shutdown(self) -> None: """Shutdown the coordinator.""" await self.device.shutdown() diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index faceea62a59..3d562bf86e5 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -6,6 +6,8 @@ from dataclasses import dataclass import logging from typing import Any, Final, cast +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError + from homeassistant.components.update import ( UpdateDeviceClass, UpdateEntity, @@ -14,11 +16,12 @@ from homeassistant.components.update import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_SLEEP_PERIOD -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator from .entity import ( RestEntityDescription, RpcEntityDescription, @@ -37,7 +40,7 @@ class RpcUpdateRequiredKeysMixin: """Class for RPC update required keys.""" latest_version: Callable[[dict], Any] - install: Callable + beta: bool @dataclass @@ -45,7 +48,7 @@ class RestUpdateRequiredKeysMixin: """Class for REST update required keys.""" latest_version: Callable[[dict], Any] - install: Callable + beta: bool @dataclass @@ -67,7 +70,7 @@ REST_UPDATES: Final = { name="Firmware Update", key="fwupdate", latest_version=lambda status: status["update"]["new_version"], - install=lambda coordinator: coordinator.async_trigger_ota_update(), + beta=False, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -76,7 +79,7 @@ REST_UPDATES: Final = { name="Beta Firmware Update", key="fwupdate", latest_version=lambda status: status["update"].get("beta_version"), - install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True), + beta=True, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -88,10 +91,8 @@ RPC_UPDATES: Final = { name="Firmware Update", key="sys", sub_key="available_updates", - latest_version=lambda status: status.get("stable", {"version": None})[ - "version" - ], - install=lambda coordinator: coordinator.async_trigger_ota_update(), + latest_version=lambda status: status.get("stable", {"version": ""})["version"], + beta=False, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -100,8 +101,8 @@ RPC_UPDATES: Final = { name="Beta Firmware Update", key="sys", sub_key="available_updates", - latest_version=lambda status: status.get("beta", {"version": None})["version"], - install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True), + latest_version=lambda status: status.get("beta", {"version": ""})["version"], + beta=True, device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -151,11 +152,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): @property def installed_version(self) -> str | None: """Version currently in use.""" - version = self.block_coordinator.device.status["update"]["old_version"] - if version is None: - return None - - return cast(str, version) + return cast(str, self.block_coordinator.device.status["update"]["old_version"]) @property def latest_version(self) -> str | None: @@ -163,7 +160,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): new_version = self.entity_description.latest_version( self.block_coordinator.device.status, ) - if new_version not in (None, ""): + if new_version: return cast(str, new_version) return self.installed_version @@ -177,10 +174,29 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install the latest firmware version.""" - config_entry = self.block_coordinator.entry - coordinator = get_entry_data(self.hass)[config_entry.entry_id].block self._in_progress_old_version = self.installed_version - await self.entity_description.install(coordinator) + beta = self.entity_description.beta + update_data = self.coordinator.device.status["update"] + LOGGER.debug("OTA update service - update_data: %s", update_data) + + new_version = update_data["new_version"] + if beta: + new_version = update_data["beta_version"] + + LOGGER.info( + "Starting OTA update of device %s from '%s' to '%s'", + self.name, + self.coordinator.device.firmware_version, + new_version, + ) + try: + result = await self.coordinator.device.trigger_ota_update(beta=beta) + except DeviceConnectionError as err: + raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err + except InvalidAuthError: + self.coordinator.entry.async_start_reauth(self.hass) + else: + LOGGER.debug("Result of OTA update call: %s", result) class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): @@ -205,9 +221,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): @property def installed_version(self) -> str | None: """Version currently in use.""" - if self.coordinator.device.shelly is None: - return None - + assert self.coordinator.device.shelly return cast(str, self.coordinator.device.shelly["ver"]) @property @@ -216,7 +230,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): new_version = self.entity_description.latest_version( self.coordinator.device.status[self.key][self.entity_description.sub_key], ) - if new_version is not None: + if new_version: return cast(str, new_version) return self.installed_version @@ -231,4 +245,29 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): ) -> None: """Install the latest firmware version.""" self._in_progress_old_version = self.installed_version - await self.entity_description.install(self.coordinator) + beta = self.entity_description.beta + update_data = self.coordinator.device.status["sys"]["available_updates"] + LOGGER.debug("OTA update service - update_data: %s", update_data) + + new_version = update_data.get("stable", {"version": ""})["version"] + if beta: + new_version = update_data.get("beta", {"version": ""})["version"] + + LOGGER.info( + "Starting OTA update of device %s from '%s' to '%s'", + self.coordinator.name, + self.coordinator.device.firmware_version, + new_version, + ) + try: + await self.coordinator.device.trigger_ota_update(beta=beta) + except DeviceConnectionError as err: + raise HomeAssistantError( + f"OTA update connection error: {repr(err)}" + ) from err + except RpcCallError as err: + raise HomeAssistantError(f"OTA update request error: {repr(err)}") from err + except InvalidAuthError: + self.coordinator.entry.async_start_reauth(self.hass) + else: + LOGGER.debug("OTA update call successful") diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index f5f713eb81e..6a1ecc58245 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -1,13 +1,29 @@ """Tests for Shelly update platform.""" -from homeassistant.components.shelly.const import DOMAIN -from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNKNOWN +from datetime import timedelta +from unittest.mock import AsyncMock + +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError +import pytest + +from homeassistant.components.shelly.const import DOMAIN, REST_SENSORS_UPDATE_INTERVAL +from homeassistant.components.update import ( + ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, +) +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.helpers.entity_component import async_update_entity +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt from . import MOCK_MAC, init_integration +from tests.common import async_fire_time_changed + async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch): """Test block device update entity.""" @@ -19,9 +35,15 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch) suggested_object_id="test_name_firmware_update", disabled_by=None, ) + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") await init_integration(hass, 1) - assert hass.states.get("update.test_name_firmware_update").state == STATE_ON + state = hass.states.get("update.test_name_firmware_update") + 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 await hass.services.async_call( UPDATE_DOMAIN, @@ -31,17 +53,162 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch) ) assert mock_block_device.trigger_ota_update.call_count == 1 - monkeypatch.setitem(mock_block_device.status["update"], "old_version", None) - monkeypatch.setitem(mock_block_device.status["update"], "new_version", None) + state = hass.states.get("update.test_name_firmware_update") + 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 True - # update entity - await async_update_entity(hass, "update.test_name_firmware_update") + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2") + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) + ) + await hass.async_block_till_done() - assert hass.states.get("update.test_name_firmware_update").state == STATE_UNKNOWN + state = hass.states.get("update.test_name_firmware_update") + 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 + + +async def test_block_beta_update(hass: HomeAssistant, mock_block_device, monkeypatch): + """Test block device beta update entity.""" + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + UPDATE_DOMAIN, + DOMAIN, + f"{MOCK_MAC}-fwupdate_beta", + suggested_object_id="test_name_beta_firmware_update", + disabled_by=None, + ) + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "") + await init_integration(hass, 1) + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "1" + assert state.attributes[ATTR_IN_PROGRESS] is False + + monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "2b") + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) + ) + await hass.async_block_till_done() + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_IN_PROGRESS] is False + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_name_beta_firmware_update"}, + blocking=True, + ) + assert mock_block_device.trigger_ota_update.call_count == 1 + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_IN_PROGRESS] is True + + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2b") + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) + ) + await hass.async_block_till_done() + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "2b" + assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_IN_PROGRESS] is False + + +async def test_block_update_connection_error( + hass: HomeAssistant, mock_block_device, monkeypatch, caplog +): + """Test block device update connection error.""" + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + UPDATE_DOMAIN, + DOMAIN, + f"{MOCK_MAC}-fwupdate", + suggested_object_id="test_name_firmware_update", + disabled_by=None, + ) + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setattr( + mock_block_device, + "trigger_ota_update", + AsyncMock(side_effect=DeviceConnectionError), + ) + await init_integration(hass, 1) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, + blocking=True, + ) + assert "Error starting OTA update" in caplog.text + + +async def test_block_update_auth_error( + hass: HomeAssistant, mock_block_device, monkeypatch +): + """Test block device update authentication error.""" + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + UPDATE_DOMAIN, + DOMAIN, + f"{MOCK_MAC}-fwupdate", + suggested_object_id="test_name_firmware_update", + disabled_by=None, + ) + monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") + monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setattr( + mock_block_device, + "trigger_ota_update", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 1) + + assert entry.state == ConfigEntryState.LOADED + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, + blocking=True, + ) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): - """Test rpc device update entity.""" + """Test RPC device update entity.""" entity_registry = async_get(hass) entity_registry.async_get_or_create( UPDATE_DOMAIN, @@ -50,9 +217,21 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): 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"], + "available_updates", + { + "stable": {"version": "2"}, + }, + ) await init_integration(hass, 2) - assert hass.states.get("update.test_name_firmware_update").state == STATE_ON + state = hass.states.get("update.test_name_firmware_update") + 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 await hass.services.async_call( UPDATE_DOMAIN, @@ -60,13 +239,189 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, blocking=True, ) - await hass.async_block_till_done() assert mock_rpc_device.trigger_ota_update.call_count == 1 - monkeypatch.setitem(mock_rpc_device.status["sys"], "available_updates", {}) - monkeypatch.setattr(mock_rpc_device, "shelly", None) + state = hass.states.get("update.test_name_firmware_update") + 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 True - # update entity - await async_update_entity(hass, "update.test_name_firmware_update") + monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2") + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) + ) + await hass.async_block_till_done() - assert hass.states.get("update.test_name_firmware_update").state == STATE_UNKNOWN + state = hass.states.get("update.test_name_firmware_update") + 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 + + +async def test_rpc_beta_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): + """Test RPC device beta update entity.""" + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + UPDATE_DOMAIN, + DOMAIN, + f"{MOCK_MAC}-sys-fwupdate_beta", + suggested_object_id="test_name_beta_firmware_update", + disabled_by=None, + ) + monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") + monkeypatch.setitem( + mock_rpc_device.status["sys"], + "available_updates", + { + "stable": {"version": "2"}, + "beta": {"version": ""}, + }, + ) + await init_integration(hass, 2) + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "1" + assert state.attributes[ATTR_IN_PROGRESS] is False + + monkeypatch.setitem( + mock_rpc_device.status["sys"], + "available_updates", + { + "stable": {"version": "2"}, + "beta": {"version": "2b"}, + }, + ) + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) + ) + await hass.async_block_till_done() + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_IN_PROGRESS] is False + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_name_beta_firmware_update"}, + blocking=True, + ) + assert mock_rpc_device.trigger_ota_update.call_count == 1 + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_IN_PROGRESS] is True + + monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2b") + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) + ) + await hass.async_block_till_done() + + state = hass.states.get("update.test_name_beta_firmware_update") + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "2b" + assert state.attributes[ATTR_LATEST_VERSION] == "2b" + assert state.attributes[ATTR_IN_PROGRESS] is False + + +@pytest.mark.parametrize( + "exc, error", + [ + (DeviceConnectionError, "Error starting OTA update"), + (RpcCallError(-1, "error"), "OTA update request error"), + ], +) +async def test_rpc_update__errors( + hass: HomeAssistant, exc, error, mock_rpc_device, monkeypatch, caplog +): + """Test RPC device update connection/call errors.""" + 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"], + "available_updates", + { + "stable": {"version": "2"}, + "beta": {"version": ""}, + }, + ) + monkeypatch.setattr( + mock_rpc_device, "trigger_ota_update", AsyncMock(side_effect=exc) + ) + await init_integration(hass, 2) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, + blocking=True, + ) + assert error in caplog.text + + +async def test_rpc_update_auth_error( + hass: HomeAssistant, mock_rpc_device, monkeypatch, caplog +): + """Test RPC device update authentication error.""" + 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"], + "available_updates", + { + "stable": {"version": "2"}, + "beta": {"version": ""}, + }, + ) + monkeypatch.setattr( + mock_rpc_device, + "trigger_ota_update", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 2) + + assert entry.state == ConfigEntryState.LOADED + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, + blocking=True, + ) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id