Add Update Entity for Linn devices (#95217)

* added update entity for Linn devices

* Update homeassistant/components/openhome/update.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* use parent methods for version attributes

* fixed issue with mocking openhome device

* Update homeassistant/components/openhome/update.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* update entity name in tests

---------

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Barry Williams 2023-06-28 21:06:24 +01:00 committed by GitHub
parent 25dc9f5942
commit aaf2846a53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 281 additions and 7 deletions

View file

@ -17,7 +17,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.MEDIA_PLAYER]
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.UPDATE]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

View file

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/openhome",
"iot_class": "local_polling",
"loggers": ["async_upnp_client", "openhomedevice"],
"requirements": ["openhomedevice==2.0.2"],
"requirements": ["openhomedevice==2.2.0"],
"ssdp": [
{
"st": "urn:av-openhome-org:service:Product:1"

View file

@ -126,9 +126,9 @@ class OpenhomeDevice(MediaPlayerEntity):
identifiers={
(DOMAIN, self._device.uuid()),
},
manufacturer=self._device.device.manufacturer,
model=self._device.device.model_name,
name=self._device.device.friendly_name,
manufacturer=self._device.manufacturer(),
model=self._device.model_name(),
name=self._device.friendly_name(),
)
@property

View file

@ -0,0 +1,102 @@
"""Update entities for Linn devices."""
from __future__ import annotations
import asyncio
import logging
from typing import Any
import aiohttp
from async_upnp_client.client import UpnpError
from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
UpdateEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up update entities for Reolink component."""
_LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
device = hass.data[DOMAIN][config_entry.entry_id]
entity = OpenhomeUpdateEntity(device)
await entity.async_update()
async_add_entities([entity])
class OpenhomeUpdateEntity(UpdateEntity):
"""Update entity for a Linn DS device."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = UpdateEntityFeature.INSTALL
_attr_has_entity_name = True
def __init__(self, device):
"""Initialize a Linn DS update entity."""
self._device = device
self._attr_unique_id = f"{device.uuid()}-update"
@property
def device_info(self):
"""Return a device description for device registry."""
return DeviceInfo(
identifiers={
(DOMAIN, self._device.uuid()),
},
manufacturer=self._device.manufacturer(),
model=self._device.model_name(),
name=self._device.friendly_name(),
)
async def async_update(self) -> None:
"""Update state of entity."""
software_status = await self._device.software_status()
if not software_status:
self._attr_installed_version = None
self._attr_latest_version = None
self._attr_release_summary = None
self._attr_release_url = None
return
self._attr_installed_version = software_status["current_software"]["version"]
if software_status["status"] == "update_available":
self._attr_latest_version = software_status["update_info"]["updates"][0][
"version"
]
self._attr_release_summary = software_status["update_info"]["updates"][0][
"description"
]
self._attr_release_url = software_status["update_info"]["releasenotesuri"]
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install the latest firmware version."""
try:
if self.latest_version:
await self._device.update_firmware()
except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError) as err:
raise HomeAssistantError(
f"Error updating {self._device.device.friendly_name}: {err}"
) from err

View file

@ -1348,7 +1348,7 @@ openerz-api==0.2.0
openevsewifi==1.1.2
# homeassistant.components.openhome
openhomedevice==2.0.2
openhomedevice==2.2.0
# homeassistant.components.opensensemap
opensensemap-api==0.2.0

View file

@ -1026,7 +1026,7 @@ openai==0.27.2
openerz-api==0.2.0
# homeassistant.components.openhome
openhomedevice==2.0.2
openhomedevice==2.2.0
# homeassistant.components.oralb
oralb-ble==0.17.6

View file

@ -0,0 +1,172 @@
"""Tests for the Openhome update platform."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.openhome.const import DOMAIN
from homeassistant.components.update import (
ATTR_INSTALLED_VERSION,
ATTR_LATEST_VERSION,
ATTR_RELEASE_SUMMARY,
ATTR_RELEASE_URL,
DOMAIN as PLATFORM_DOMAIN,
SERVICE_INSTALL,
UpdateDeviceClass,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
CONF_HOST,
STATE_ON,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from tests.common import MockConfigEntry
LATEST_FIRMWARE_INSTALLED = {
"status": "on_latest",
"current_software": {"version": "4.100.502", "topic": "main", "channel": "release"},
}
FIRMWARE_UPDATE_AVAILABLE = {
"status": "update_available",
"current_software": {"version": "4.99.491", "topic": "main", "channel": "release"},
"update_info": {
"legal": {
"licenseurl": "http://products.linn.co.uk/VersionInfo/licenseV2.txt",
"privacyurl": "https://www.linn.co.uk/privacy",
"privacyuri": "https://products.linn.co.uk/VersionInfo/PrivacyV1.json",
"privacyversion": 1,
},
"releasenotesuri": "http://docs.linn.co.uk/wiki/index.php/ReleaseNotes",
"updates": [
{
"channel": "release",
"date": "07 Jun 2023 12:29:48",
"description": "Release build version 4.100.502 (07 Jun 2023 12:29:48)",
"exaktlink": "3",
"manifest": "https://cloud.linn.co.uk/update/components/836/4.100.502/manifest.json",
"topic": "main",
"variant": "836",
"version": "4.100.502",
}
],
"exaktUpdates": [],
},
}
async def setup_integration(
hass: HomeAssistant,
software_status: dict,
update_firmware: AsyncMock,
) -> None:
"""Load an openhome platform with mocked device."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "http://localhost"},
)
entry.add_to_hass(hass)
with patch("homeassistant.components.openhome.PLATFORMS", [Platform.UPDATE]), patch(
"homeassistant.components.openhome.Device", MagicMock()
) as mock_device:
mock_device.return_value.init = AsyncMock()
mock_device.return_value.uuid = MagicMock(return_value="uuid")
mock_device.return_value.manufacturer = MagicMock(return_value="manufacturer")
mock_device.return_value.model_name = MagicMock(return_value="model_name")
mock_device.return_value.friendly_name = MagicMock(return_value="friendly_name")
mock_device.return_value.software_status = AsyncMock(
return_value=software_status
)
mock_device.return_value.update_firmware = update_firmware
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
async def test_not_supported(hass: HomeAssistant):
"""Ensure update entity works if service not supported."""
update_firmware = AsyncMock()
await setup_integration(hass, None, update_firmware)
state = hass.states.get("update.friendly_name")
assert state
assert state.state == STATE_UNKNOWN
assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE
assert state.attributes[ATTR_INSTALLED_VERSION] is None
assert state.attributes[ATTR_LATEST_VERSION] is None
assert state.attributes[ATTR_RELEASE_URL] is None
assert state.attributes[ATTR_RELEASE_SUMMARY] is None
update_firmware.assert_not_called()
async def test_on_latest_firmware(hass: HomeAssistant):
"""Test device on latest firmware."""
update_firmware = AsyncMock()
await setup_integration(hass, LATEST_FIRMWARE_INSTALLED, update_firmware)
state = hass.states.get("update.friendly_name")
assert state
assert state.state == STATE_UNKNOWN
assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE
assert state.attributes[ATTR_INSTALLED_VERSION] == "4.100.502"
assert state.attributes[ATTR_LATEST_VERSION] is None
assert state.attributes[ATTR_RELEASE_URL] is None
assert state.attributes[ATTR_RELEASE_SUMMARY] is None
update_firmware.assert_not_called()
async def test_update_available(hass: HomeAssistant):
"""Test device has firmware update available."""
update_firmware = AsyncMock()
await setup_integration(hass, FIRMWARE_UPDATE_AVAILABLE, update_firmware)
state = hass.states.get("update.friendly_name")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE
assert state.attributes[ATTR_INSTALLED_VERSION] == "4.99.491"
assert state.attributes[ATTR_LATEST_VERSION] == "4.100.502"
assert (
state.attributes[ATTR_RELEASE_URL]
== "http://docs.linn.co.uk/wiki/index.php/ReleaseNotes"
)
assert (
state.attributes[ATTR_RELEASE_SUMMARY]
== "Release build version 4.100.502 (07 Jun 2023 12:29:48)"
)
await hass.services.async_call(
PLATFORM_DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.friendly_name"},
blocking=True,
)
await hass.async_block_till_done()
update_firmware.assert_called_once()
async def test_firmware_update_not_required(hass: HomeAssistant):
"""Ensure firmware install does nothing if up to date."""
update_firmware = AsyncMock()
await setup_integration(hass, LATEST_FIRMWARE_INSTALLED, update_firmware)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
PLATFORM_DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: "update.friendly_name"},
blocking=True,
)
update_firmware.assert_not_called()