Add release_notes method to update entities (#68842)

This commit is contained in:
Joakim Sørensen 2022-03-30 02:38:56 +02:00 committed by GitHub
parent 94df0844b3
commit 9a150c2234
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 0 deletions

View File

@ -71,6 +71,7 @@ async def async_setup_platform(
latest_version="1.94.2", latest_version="1.94.2",
support_progress=True, support_progress=True,
release_summary="Added support for effects", release_summary="Added support for effects",
support_release_notes=True,
release_url="https://www.example.com/release/1.93.3", release_url="https://www.example.com/release/1.93.3",
device_class=UpdateDeviceClass.FIRMWARE, device_class=UpdateDeviceClass.FIRMWARE,
), ),
@ -109,6 +110,7 @@ class DemoUpdate(UpdateEntity):
release_url: str | None = None, release_url: str | None = None,
support_progress: bool = False, support_progress: bool = False,
support_install: bool = True, support_install: bool = True,
support_release_notes: bool = False,
device_class: UpdateDeviceClass | None = None, device_class: UpdateDeviceClass | None = None,
) -> None: ) -> None:
"""Initialize the Demo select entity.""" """Initialize the Demo select entity."""
@ -133,6 +135,9 @@ class DemoUpdate(UpdateEntity):
if support_progress: if support_progress:
self._attr_supported_features |= UpdateEntityFeature.PROGRESS self._attr_supported_features |= UpdateEntityFeature.PROGRESS
if support_release_notes:
self._attr_supported_features |= UpdateEntityFeature.RELEASE_NOTES
async def async_install( async def async_install(
self, version: str | None, backup: bool, **kwargs: Any self, version: str | None, backup: bool, **kwargs: Any
) -> None: ) -> None:
@ -148,3 +153,10 @@ class DemoUpdate(UpdateEntity):
version if version is not None else self.latest_version version if version is not None else self.latest_version
) )
self.async_write_ha_state() self.async_write_ha_state()
def release_notes(self) -> str | None:
"""Return the release notes."""
return (
"Long release notes.\n\n**With** "
f"markdown support!\n\n***\n\n{self.release_summary}"
)

View File

@ -9,6 +9,7 @@ from typing import Any, Final, final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.enum import StrEnum from homeassistant.backports.enum import StrEnum
from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
@ -93,6 +94,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{}, {},
UpdateEntity.async_skip.__name__, UpdateEntity.async_skip.__name__,
) )
websocket_api.async_register_command(hass, websocket_release_notes)
return True return True
@ -268,6 +270,22 @@ class UpdateEntity(RestoreEntity):
""" """
raise NotImplementedError() raise NotImplementedError()
async def async_release_notes(self) -> str | None:
"""Return full release notes.
This is suitable for a long changelog that does not fit in the release_summary property.
The returned string can contain markdown.
"""
return await self.hass.async_add_executor_job(self.release_notes)
def release_notes(self) -> str | None:
"""Return full release notes.
This is suitable for a long changelog that does not fit in the release_summary property.
The returned string can contain markdown.
"""
raise NotImplementedError()
@property @property
@final @final
def state(self) -> str | None: def state(self) -> str | None:
@ -343,3 +361,40 @@ class UpdateEntity(RestoreEntity):
state = await self.async_get_last_state() state = await self.async_get_last_state()
if state is not None and state.attributes.get(ATTR_SKIPPED_VERSION) is not None: if state is not None and state.attributes.get(ATTR_SKIPPED_VERSION) is not None:
self.__skipped_version = state.attributes[ATTR_SKIPPED_VERSION] self.__skipped_version = state.attributes[ATTR_SKIPPED_VERSION]
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "update/release_notes",
vol.Required("entity_id"): cv.entity_id,
}
)
@websocket_api.async_response
async def websocket_release_notes(
hass: HomeAssistant,
connection: websocket_api.connection.ActiveConnection,
msg: dict,
) -> None:
"""Get the full release notes for a entity."""
component = hass.data[DOMAIN]
entity: UpdateEntity | None = component.get_entity(msg["entity_id"])
if entity is None:
connection.send_error(
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Entity not found"
)
return
if not entity.supported_features & UpdateEntityFeature.RELEASE_NOTES:
connection.send_error(
msg["id"],
websocket_api.const.ERR_NOT_SUPPORTED,
"Entity does not support release notes",
)
return
connection.send_result(
msg["id"],
await entity.async_release_notes(),
)

View File

@ -14,6 +14,7 @@ class UpdateEntityFeature(IntEnum):
SPECIFIC_VERSION = 2 SPECIFIC_VERSION = 2
PROGRESS = 4 PROGRESS = 4
BACKUP = 8 BACKUP = 8
RELEASE_NOTES = 16
SERVICE_INSTALL: Final = "install" SERVICE_INSTALL: Final = "install"

View File

@ -1,6 +1,8 @@
"""The tests for the Update component.""" """The tests for the Update component."""
from collections.abc import Awaitable, Callable
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from aiohttp import ClientWebSocketResponse
import pytest import pytest
from homeassistant.components.update import ( from homeassistant.components.update import (
@ -587,3 +589,83 @@ async def test_restore_state(
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1"
assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1"
async def test_release_notes(
hass: HomeAssistant,
enable_custom_integrations: None,
hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]],
) -> None:
"""Test getting the release notes over the websocket connection."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json(
{
"id": 1,
"type": "update/release_notes",
"entity_id": "update.update_with_release_notes",
}
)
result = await client.receive_json()
assert result["result"] == "Release notes"
async def test_release_notes_entity_not_found(
hass: HomeAssistant,
enable_custom_integrations: None,
hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]],
) -> None:
"""Test getting the release notes for not found entity."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json(
{
"id": 1,
"type": "update/release_notes",
"entity_id": "update.entity_not_found",
}
)
result = await client.receive_json()
assert result["error"]["code"] == "not_found"
assert result["error"]["message"] == "Entity not found"
async def test_release_notes_entity_does_not_support_release_notes(
hass: HomeAssistant,
enable_custom_integrations: None,
hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]],
) -> None:
"""Test getting the release notes for entity that does not support release notes."""
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json(
{
"id": 1,
"type": "update/release_notes",
"entity_id": "update.update_available",
}
)
result = await client.receive_json()
assert result["error"]["code"] == "not_supported"
assert result["error"]["message"] == "Entity does not support release notes"

View File

@ -62,6 +62,10 @@ class MockUpdateEntity(MockEntity, UpdateEntity):
self._values["current_version"] = self.latest_version self._values["current_version"] = self.latest_version
_LOGGER.info("Installed latest update") _LOGGER.info("Installed latest update")
def release_notes(self) -> str | None:
"""Return the release notes of the latest version."""
return "Release notes"
def init(empty=False): def init(empty=False):
"""Initialize the platform with entities.""" """Initialize the platform with entities."""
@ -124,6 +128,13 @@ def init(empty=False):
current_version="1.0.0", current_version="1.0.0",
latest_version="1.0.1", latest_version="1.0.1",
), ),
MockUpdateEntity(
name="Update with release notes",
unique_id="with_release_notes",
current_version="1.0.0",
latest_version="1.0.1",
supported_features=UpdateEntityFeature.RELEASE_NOTES,
),
] ]
) )