Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions homeassistant/components/demo/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ async def async_setup_platform(
latest_version="1.94.2",
support_progress=True,
release_summary="Added support for effects",
support_release_notes=True,
release_url="https://www.example.com/release/1.93.3",
device_class=UpdateDeviceClass.FIRMWARE,
),
Expand Down Expand Up @@ -109,6 +110,7 @@ def __init__(
release_url: str | None = None,
support_progress: bool = False,
support_install: bool = True,
support_release_notes: bool = False,
device_class: UpdateDeviceClass | None = None,
) -> None:
"""Initialize the Demo select entity."""
Expand All @@ -133,6 +135,9 @@ def __init__(
if support_progress:
self._attr_supported_features |= UpdateEntityFeature.PROGRESS

if support_release_notes:
self._attr_supported_features |= UpdateEntityFeature.RELEASE_NOTES

async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
Expand All @@ -148,3 +153,10 @@ async def async_install(
version if version is not None else self.latest_version
)
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}"
)
55 changes: 55 additions & 0 deletions homeassistant/components/update/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import voluptuous as vol

from homeassistant.backports.enum import StrEnum
from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, ServiceCall
Expand Down Expand Up @@ -93,6 +94,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{},
UpdateEntity.async_skip.__name__,
)
websocket_api.async_register_command(hass, websocket_release_notes)

return True

Expand Down Expand Up @@ -268,6 +270,22 @@ def install(self, version: str | None, backup: bool, **kwargs: Any) -> None:
"""
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
@final
def state(self) -> str | None:
Expand Down Expand Up @@ -343,3 +361,40 @@ async def async_internal_added_to_hass(self) -> None:
state = await self.async_get_last_state()
if state is not None and state.attributes.get(ATTR_SKIPPED_VERSION) is not None:
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(),
)
1 change: 1 addition & 0 deletions homeassistant/components/update/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class UpdateEntityFeature(IntEnum):
SPECIFIC_VERSION = 2
PROGRESS = 4
BACKUP = 8
RELEASE_NOTES = 16


SERVICE_INSTALL: Final = "install"
Expand Down
82 changes: 82 additions & 0 deletions tests/components/update/test_init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""The tests for the Update component."""
from collections.abc import Awaitable, Callable
from unittest.mock import MagicMock, patch

from aiohttp import ClientWebSocketResponse
import pytest

from homeassistant.components.update import (
Expand Down Expand Up @@ -587,3 +589,83 @@ async def test_restore_state(
assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0"
assert state.attributes[ATTR_LATEST_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"
11 changes: 11 additions & 0 deletions tests/testing_config/custom_components/test/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def install(self, version: str | None, backup: bool, **kwargs: Any) -> None:
self._values["current_version"] = self.latest_version
_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):
"""Initialize the platform with entities."""
Expand Down Expand Up @@ -124,6 +128,13 @@ def init(empty=False):
current_version="1.0.0",
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,
),
]
)

Expand Down