mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Cleanup Shelly update platform (#80845)
This commit is contained in:
parent
2ddf1f9416
commit
dce4753510
@ -14,13 +14,11 @@ from aioshelly.rpc_device import RpcDevice
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers import device_registry
|
from homeassistant.helpers import device_registry
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_BETA,
|
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
ATTR_CLICK_TYPE,
|
ATTR_CLICK_TYPE,
|
||||||
ATTR_DEVICE,
|
ATTR_DEVICE,
|
||||||
@ -249,43 +247,6 @@ class ShellyBlockCoordinator(DataUpdateCoordinator):
|
|||||||
self.device_id = entry.id
|
self.device_id = entry.id
|
||||||
self.device.subscribe_updates(self.async_set_updated_data)
|
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:
|
def shutdown(self) -> None:
|
||||||
"""Shutdown the coordinator."""
|
"""Shutdown the coordinator."""
|
||||||
self.device.shutdown()
|
self.device.shutdown()
|
||||||
@ -480,46 +441,6 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
|
|||||||
self.device_id = entry.id
|
self.device_id = entry.id
|
||||||
self.device.subscribe_updates(self.async_set_updated_data)
|
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:
|
async def shutdown(self) -> None:
|
||||||
"""Shutdown the coordinator."""
|
"""Shutdown the coordinator."""
|
||||||
await self.device.shutdown()
|
await self.device.shutdown()
|
||||||
|
@ -6,6 +6,8 @@ from dataclasses import dataclass
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, cast
|
from typing import Any, Final, cast
|
||||||
|
|
||||||
|
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||||
|
|
||||||
from homeassistant.components.update import (
|
from homeassistant.components.update import (
|
||||||
UpdateDeviceClass,
|
UpdateDeviceClass,
|
||||||
UpdateEntity,
|
UpdateEntity,
|
||||||
@ -14,11 +16,12 @@ from homeassistant.components.update import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import CONF_SLEEP_PERIOD
|
from .const import CONF_SLEEP_PERIOD
|
||||||
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
|
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
|
||||||
from .entity import (
|
from .entity import (
|
||||||
RestEntityDescription,
|
RestEntityDescription,
|
||||||
RpcEntityDescription,
|
RpcEntityDescription,
|
||||||
@ -37,7 +40,7 @@ class RpcUpdateRequiredKeysMixin:
|
|||||||
"""Class for RPC update required keys."""
|
"""Class for RPC update required keys."""
|
||||||
|
|
||||||
latest_version: Callable[[dict], Any]
|
latest_version: Callable[[dict], Any]
|
||||||
install: Callable
|
beta: bool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -45,7 +48,7 @@ class RestUpdateRequiredKeysMixin:
|
|||||||
"""Class for REST update required keys."""
|
"""Class for REST update required keys."""
|
||||||
|
|
||||||
latest_version: Callable[[dict], Any]
|
latest_version: Callable[[dict], Any]
|
||||||
install: Callable
|
beta: bool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -67,7 +70,7 @@ REST_UPDATES: Final = {
|
|||||||
name="Firmware Update",
|
name="Firmware Update",
|
||||||
key="fwupdate",
|
key="fwupdate",
|
||||||
latest_version=lambda status: status["update"]["new_version"],
|
latest_version=lambda status: status["update"]["new_version"],
|
||||||
install=lambda coordinator: coordinator.async_trigger_ota_update(),
|
beta=False,
|
||||||
device_class=UpdateDeviceClass.FIRMWARE,
|
device_class=UpdateDeviceClass.FIRMWARE,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -76,7 +79,7 @@ REST_UPDATES: Final = {
|
|||||||
name="Beta Firmware Update",
|
name="Beta Firmware Update",
|
||||||
key="fwupdate",
|
key="fwupdate",
|
||||||
latest_version=lambda status: status["update"].get("beta_version"),
|
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,
|
device_class=UpdateDeviceClass.FIRMWARE,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -88,10 +91,8 @@ RPC_UPDATES: Final = {
|
|||||||
name="Firmware Update",
|
name="Firmware Update",
|
||||||
key="sys",
|
key="sys",
|
||||||
sub_key="available_updates",
|
sub_key="available_updates",
|
||||||
latest_version=lambda status: status.get("stable", {"version": None})[
|
latest_version=lambda status: status.get("stable", {"version": ""})["version"],
|
||||||
"version"
|
beta=False,
|
||||||
],
|
|
||||||
install=lambda coordinator: coordinator.async_trigger_ota_update(),
|
|
||||||
device_class=UpdateDeviceClass.FIRMWARE,
|
device_class=UpdateDeviceClass.FIRMWARE,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -100,8 +101,8 @@ RPC_UPDATES: Final = {
|
|||||||
name="Beta Firmware Update",
|
name="Beta Firmware Update",
|
||||||
key="sys",
|
key="sys",
|
||||||
sub_key="available_updates",
|
sub_key="available_updates",
|
||||||
latest_version=lambda status: status.get("beta", {"version": None})["version"],
|
latest_version=lambda status: status.get("beta", {"version": ""})["version"],
|
||||||
install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True),
|
beta=True,
|
||||||
device_class=UpdateDeviceClass.FIRMWARE,
|
device_class=UpdateDeviceClass.FIRMWARE,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -151,11 +152,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
|
|||||||
@property
|
@property
|
||||||
def installed_version(self) -> str | None:
|
def installed_version(self) -> str | None:
|
||||||
"""Version currently in use."""
|
"""Version currently in use."""
|
||||||
version = self.block_coordinator.device.status["update"]["old_version"]
|
return cast(str, self.block_coordinator.device.status["update"]["old_version"])
|
||||||
if version is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return cast(str, version)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> str | None:
|
def latest_version(self) -> str | None:
|
||||||
@ -163,7 +160,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
|
|||||||
new_version = self.entity_description.latest_version(
|
new_version = self.entity_description.latest_version(
|
||||||
self.block_coordinator.device.status,
|
self.block_coordinator.device.status,
|
||||||
)
|
)
|
||||||
if new_version not in (None, ""):
|
if new_version:
|
||||||
return cast(str, new_version)
|
return cast(str, new_version)
|
||||||
|
|
||||||
return self.installed_version
|
return self.installed_version
|
||||||
@ -177,10 +174,29 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
|
|||||||
self, version: str | None, backup: bool, **kwargs: Any
|
self, version: str | None, backup: bool, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Install the latest firmware version."""
|
"""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
|
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):
|
class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||||
@ -205,9 +221,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
|||||||
@property
|
@property
|
||||||
def installed_version(self) -> str | None:
|
def installed_version(self) -> str | None:
|
||||||
"""Version currently in use."""
|
"""Version currently in use."""
|
||||||
if self.coordinator.device.shelly is None:
|
assert self.coordinator.device.shelly
|
||||||
return None
|
|
||||||
|
|
||||||
return cast(str, self.coordinator.device.shelly["ver"])
|
return cast(str, self.coordinator.device.shelly["ver"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -216,7 +230,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
|||||||
new_version = self.entity_description.latest_version(
|
new_version = self.entity_description.latest_version(
|
||||||
self.coordinator.device.status[self.key][self.entity_description.sub_key],
|
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 cast(str, new_version)
|
||||||
|
|
||||||
return self.installed_version
|
return self.installed_version
|
||||||
@ -231,4 +245,29 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Install the latest firmware version."""
|
"""Install the latest firmware version."""
|
||||||
self._in_progress_old_version = self.installed_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")
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
"""Tests for Shelly update platform."""
|
"""Tests for Shelly update platform."""
|
||||||
from homeassistant.components.shelly.const import DOMAIN
|
from datetime import timedelta
|
||||||
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
|
from unittest.mock import AsyncMock
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNKNOWN
|
|
||||||
|
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.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.helpers.entity_registry import async_get
|
||||||
|
from homeassistant.util import dt
|
||||||
|
|
||||||
from . import MOCK_MAC, init_integration
|
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):
|
async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch):
|
||||||
"""Test block device update entity."""
|
"""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",
|
suggested_object_id="test_name_firmware_update",
|
||||||
disabled_by=None,
|
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)
|
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(
|
await hass.services.async_call(
|
||||||
UPDATE_DOMAIN,
|
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
|
assert mock_block_device.trigger_ota_update.call_count == 1
|
||||||
|
|
||||||
monkeypatch.setitem(mock_block_device.status["update"], "old_version", None)
|
state = hass.states.get("update.test_name_firmware_update")
|
||||||
monkeypatch.setitem(mock_block_device.status["update"], "new_version", None)
|
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
|
monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2")
|
||||||
await async_update_entity(hass, "update.test_name_firmware_update")
|
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):
|
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(hass)
|
||||||
entity_registry.async_get_or_create(
|
entity_registry.async_get_or_create(
|
||||||
UPDATE_DOMAIN,
|
UPDATE_DOMAIN,
|
||||||
@ -50,9 +217,21 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch):
|
|||||||
suggested_object_id="test_name_firmware_update",
|
suggested_object_id="test_name_firmware_update",
|
||||||
disabled_by=None,
|
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)
|
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(
|
await hass.services.async_call(
|
||||||
UPDATE_DOMAIN,
|
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"},
|
{ATTR_ENTITY_ID: "update.test_name_firmware_update"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert mock_rpc_device.trigger_ota_update.call_count == 1
|
assert mock_rpc_device.trigger_ota_update.call_count == 1
|
||||||
|
|
||||||
monkeypatch.setitem(mock_rpc_device.status["sys"], "available_updates", {})
|
state = hass.states.get("update.test_name_firmware_update")
|
||||||
monkeypatch.setattr(mock_rpc_device, "shelly", None)
|
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
|
monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2")
|
||||||
await async_update_entity(hass, "update.test_name_firmware_update")
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user