From c93f3d3781d657bff779d020b1653f8baa9c3d68 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 29 Aug 2026 12:15:06 +0200 Subject: [PATCH] Add initial update platform to Proxmox (#167620) --- .../components/proxmoxve/__init__.py | 1 + homeassistant/components/proxmoxve/const.py | 1 + .../components/proxmoxve/coordinator.py | 37 +++++- .../components/proxmoxve/diagnostics.py | 12 +- homeassistant/components/proxmoxve/helpers.py | 60 +++++++++ .../components/proxmoxve/strings.json | 5 + homeassistant/components/proxmoxve/update.py | 120 +++++++++++++++++ tests/components/proxmoxve/__init__.py | 5 + tests/components/proxmoxve/conftest.py | 6 + .../proxmoxve/fixtures/nodes/update.json | 57 +++++++++ .../proxmoxve/fixtures/nodes/version.json | 5 + .../proxmoxve/snapshots/test_diagnostics.ambr | 5 + .../proxmoxve/snapshots/test_update.ambr | 63 +++++++++ tests/components/proxmoxve/test_update.py | 121 ++++++++++++++++++ 14 files changed, 490 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/proxmoxve/update.py create mode 100644 tests/components/proxmoxve/fixtures/nodes/update.json create mode 100644 tests/components/proxmoxve/fixtures/nodes/version.json create mode 100644 tests/components/proxmoxve/snapshots/test_update.ambr create mode 100644 tests/components/proxmoxve/test_update.py diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 1f39c5d0a6d1..57b5a4730b81 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -20,6 +20,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, + Platform.UPDATE, ] diff --git a/homeassistant/components/proxmoxve/const.py b/homeassistant/components/proxmoxve/const.py index 8985a2a77ec9..5963711b6ed3 100644 --- a/homeassistant/components/proxmoxve/const.py +++ b/homeassistant/components/proxmoxve/const.py @@ -42,5 +42,6 @@ class ProxmoxPermission(StrEnum): POWER = "VM.PowerMgmt" SNAPSHOT = "VM.Snapshot" SYSAUDIT = "Sys.Audit" + SYSMOD = "Sys.Modify" SYSPOWER = "Sys.PowerMgmt" VMAUDIT = "VM.Audit" diff --git a/homeassistant/components/proxmoxve/coordinator.py b/homeassistant/components/proxmoxve/coordinator.py index d3f47e2d9543..bd550cd41d18 100644 --- a/homeassistant/components/proxmoxve/coordinator.py +++ b/homeassistant/components/proxmoxve/coordinator.py @@ -35,7 +35,9 @@ from .const import ( DEFAULT_VERIFY_SSL, DOMAIN, NODE_ONLINE, + ProxmoxPermission, ) +from .helpers import is_granted type ProxmoxConfigEntry = ConfigEntry[ProxmoxCoordinator] @@ -52,6 +54,8 @@ class NodeResources: containers: list[dict[str, Any]] storages: list[dict[str, Any]] backups: list[dict[str, Any]] + version: dict[str, Any] + update: list[dict[str, Any]] | bool @dataclass(slots=True, kw_only=True) @@ -63,6 +67,8 @@ class ProxmoxNodeData: containers: dict[int, dict[str, Any]] = field(default_factory=dict) storages: dict[str, dict[str, Any]] = field(default_factory=dict) backups: list[dict[str, Any]] = field(default_factory=list) + version: dict[str, Any] = field(default_factory=dict) + update: list[dict[str, Any]] | bool = False def proxmox_base_url(coordinator: ProxmoxCoordinator) -> URL: @@ -212,6 +218,8 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]): }, storages={s["storage"]: s for s in resources.storages}, backups=resources.backups, + version=resources.version, + update=resources.update, ) self._async_add_remove_nodes(data) @@ -258,7 +266,7 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]): raise ProxmoxServerError from err def _fetch_all_nodes(self) -> list[tuple[dict[str, Any], NodeResources]]: - """Fetch all nodes with their VMs, containers, storages, and backups.""" + """Fetch all nodes with their VMs, containers, storages, etc.""" nodes = self.proxmox.nodes.get() or [] return [(node, self._get_node_data(node)) for node in nodes] @@ -266,13 +274,20 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]): self, node: dict[str, Any], ) -> NodeResources: - """Get vms, containers, storages, and backups for a node.""" + """Get vms, containers, storages, etc. for a node.""" if node.get("status") != NODE_ONLINE: _LOGGER.debug( "Node %s is offline, skipping VM/container/storage fetch", node[CONF_NODE], ) - return NodeResources(vms=[], containers=[], storages=[], backups=[]) + return NodeResources( + vms=[], + containers=[], + storages=[], + backups=[], + version={}, + update=False, + ) vms = self.proxmox.nodes(node[CONF_NODE]).qemu.get() or [] containers = self.proxmox.nodes(node[CONF_NODE]).lxc.get() or [] @@ -281,9 +296,23 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]): self.proxmox.nodes(node[CONF_NODE]).tasks.get(typefilter="vzdump", limit=1) or [] ) + version = self.proxmox.nodes(node[CONF_NODE]).version.get() or {} + update: list | bool = False + if is_granted( + self.permissions, + p_type="nodes", + p_id=node[CONF_NODE], + permission=ProxmoxPermission.SYSMOD, + ): + update = self.proxmox.nodes(node[CONF_NODE]).apt.update.get() or [] return NodeResources( - vms=vms, containers=containers, storages=storages, backups=backups + vms=vms, + containers=containers, + storages=storages, + backups=backups, + version=version, + update=update, ) def _async_add_remove_nodes(self, data: dict[str, ProxmoxNodeData]) -> None: diff --git a/homeassistant/components/proxmoxve/diagnostics.py b/homeassistant/components/proxmoxve/diagnostics.py index a42133dfd51b..0581bca7a659 100644 --- a/homeassistant/components/proxmoxve/diagnostics.py +++ b/homeassistant/components/proxmoxve/diagnostics.py @@ -18,10 +18,14 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a Proxmox VE config entry.""" + devices = {} + # De-noise updates list + for node, node_data in config_entry.runtime_data.data.items(): + d = asdict(node_data) + d.pop("update", None) + devices[node] = d + return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), - "devices": { - node: asdict(node_data) - for node, node_data in config_entry.runtime_data.data.items() - }, + "devices": devices, } diff --git a/homeassistant/components/proxmoxve/helpers.py b/homeassistant/components/proxmoxve/helpers.py index c7e96bcd300b..c968cc5583a9 100644 --- a/homeassistant/components/proxmoxve/helpers.py +++ b/homeassistant/components/proxmoxve/helpers.py @@ -1,8 +1,24 @@ """Helpers for Proxmox VE.""" +from dataclasses import dataclass +from typing import Any + +from packaging.version import parse as parse_version + from .const import ProxmoxPermission +@dataclass(frozen=True) +class ProxmoxUpdateInfo: + """Describes Proxmox VE update information.""" + + latest_version: str | None = None + latest_version_id: str | None = None + total_updates: int = 0 + proxmox_updates: int = 0 + other_updates: int = 0 + + def is_granted( permissions: dict[str, dict[str, int]], p_type: str = "vms", @@ -16,3 +32,47 @@ def is_granted( if value is not None: return value == 1 return False + + +def is_proxmox_package(update: dict[str, Any]) -> bool: + """Indicate if the given update is related to Proxmox VE.""" + package = update.get("Package", "") + origin = update.get("Origin", "") + title = update.get("Title", "") + return ( + package.startswith(("pve-", "libpve-")) + or "proxmox" in origin.lower() + or "proxmox" in title.lower() + ) + + +def latest_version(versions: list[str]) -> str: + """Return the latest version from a list of version strings.""" + # Fix proxmox -pve1 style suffixes + safe_versions = [v.split("-")[0] for v in versions] + return max(safe_versions, key=parse_version) + + +def update_version( + current_version: str, + updates: list[dict[str, Any]], +) -> ProxmoxUpdateInfo: + """Return the updated version based on the current version and updates.""" + + count = len(updates) + pve_count = sum(is_proxmox_package(u) for u in updates) + other_count = len(updates) - pve_count + versions = [current_version] + [ + u["Version"] for u in updates if is_proxmox_package(u) + ] + + latest = latest_version(versions) if pve_count else current_version + return ProxmoxUpdateInfo( + latest_version=latest if count else current_version, + latest_version_id=f"{latest}-p{pve_count}-d{other_count}" + if count + else current_version, + total_updates=count, + proxmox_updates=pve_count, + other_updates=other_count, + ) diff --git a/homeassistant/components/proxmoxve/strings.json b/homeassistant/components/proxmoxve/strings.json index 2637252502ee..b5b75b26e327 100644 --- a/homeassistant/components/proxmoxve/strings.json +++ b/homeassistant/components/proxmoxve/strings.json @@ -296,6 +296,11 @@ "vm_uptime": { "name": "Uptime" } + }, + "update": { + "node_update": { + "name": "Software update" + } } }, "exceptions": { diff --git a/homeassistant/components/proxmoxve/update.py b/homeassistant/components/proxmoxve/update.py new file mode 100644 index 000000000000..75fbac78339e --- /dev/null +++ b/homeassistant/components/proxmoxve/update.py @@ -0,0 +1,120 @@ +"""Update platform for Proxmox VE.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import override + +from homeassistant.components.update import ( + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import ProxmoxConfigEntry, ProxmoxNodeData +from .entity import ProxmoxNodeEntity +from .helpers import ProxmoxUpdateInfo, update_version + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class ProxmoxNodeUpdateEntityDescription(UpdateEntityDescription): + """Describes Proxmox node update entity.""" + + installed_version: Callable[[ProxmoxNodeData], str] + update_info: Callable[[ProxmoxNodeData], ProxmoxUpdateInfo | bool] + + +NODE_UPDATES: tuple[ProxmoxNodeUpdateEntityDescription, ...] = ( + ProxmoxNodeUpdateEntityDescription( + key="node_update", + translation_key="node_update", + entity_category=EntityCategory.CONFIG, + installed_version=lambda node_data: node_data.version.get("version", "unknown"), + update_info=lambda node_data: update_version( + node_data.version.get("version", "unknown"), + node_data.update if isinstance(node_data.update, list) else [], + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ProxmoxConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Proxmox VE update entities.""" + coordinator = entry.runtime_data + + def _async_add_new_nodes(nodes: list[ProxmoxNodeData]) -> None: + """Add update entities for newly discovered nodes.""" + async_add_entities( + ProxmoxNodeUpdateEntity(coordinator, entity_description, node) + for node in nodes + for entity_description in NODE_UPDATES + if node.update + is not False # Only create entities for nodes where update information is available + ) + + coordinator.new_nodes_callbacks.append(_async_add_new_nodes) + _async_add_new_nodes( + [ + node_data + for node_data in coordinator.data.values() + if node_data.node["node"] in coordinator.known_nodes + ] + ) + + +class ProxmoxNodeUpdateEntity(ProxmoxNodeEntity, UpdateEntity): + """Represents a Proxmox VE node software update.""" + + _attr_supported_features = UpdateEntityFeature.RELEASE_NOTES + entity_description: ProxmoxNodeUpdateEntityDescription + + @property + @override + def installed_version(self) -> str | None: + """Return installed version.""" + return self.entity_description.installed_version( + self.coordinator.data[self.device_name] + ) + + @property + @override + def latest_version(self) -> str | None: + """Return latest version.""" + update_info = self._update_info() + return update_info.latest_version_id if update_info else None + + @property + @override + def release_summary(self) -> str | None: + """Return the release summary for the update.""" + url = self.device_info.get("configuration_url") if self.device_info else None + update_info = self._update_info() + if update_info and update_info.total_updates > 0: + return f"A total of {update_info.total_updates} package update(s) are pending installation: of these {update_info.proxmox_updates} relate to Proxmox and {update_info.other_updates} to other updates. Please visit the [Proxmox VE node]({url}) for details on the pending updates and to upgrade to {update_info.latest_version}." + return None + + @property + @override + def available(self) -> bool: + """Return if the update platform is available.""" + return self._update_info() is not None + + @override + def release_notes(self) -> str | None: + """Return the release notes for the update.""" + return self.release_summary + + def _update_info(self) -> ProxmoxUpdateInfo | None: + """Return update info or None if unavailable.""" + info = self.entity_description.update_info( + self.coordinator.data[self.device_name] + ) + return info if isinstance(info, ProxmoxUpdateInfo) else None diff --git a/tests/components/proxmoxve/__init__.py b/tests/components/proxmoxve/__init__.py index 07c70348383c..3667225079e5 100644 --- a/tests/components/proxmoxve/__init__.py +++ b/tests/components/proxmoxve/__init__.py @@ -43,16 +43,21 @@ SNAPSHOT_PERMISSIONS = { "/vms": {"VM.Snapshot": 1}, "/vms/101": {"VM.Snapshot": 0}, } +SYSMOD_PERMISSIONS = { + "/": {"Sys.Modify": 1}, +} MERGED_PERMISSIONS = { key: { **AUDIT_PERMISSIONS.get(key, {}), **POWER_PERMISSIONS.get(key, {}), **SNAPSHOT_PERMISSIONS.get(key, {}), + **SYSMOD_PERMISSIONS.get(key, {}), } for key in set(AUDIT_PERMISSIONS) | set(POWER_PERMISSIONS) | set(SNAPSHOT_PERMISSIONS) + | set(SYSMOD_PERMISSIONS) } PVEVMUSER_PERMISSIONS = deepcopy(MERGED_PERMISSIONS) diff --git a/tests/components/proxmoxve/conftest.py b/tests/components/proxmoxve/conftest.py index 66e5209ccddc..e1f050ccb2d0 100644 --- a/tests/components/proxmoxve/conftest.py +++ b/tests/components/proxmoxve/conftest.py @@ -132,6 +132,12 @@ def mock_proxmox_client(): not in mock_instance.access.permissions.get.return_value.get("/nodes", []) else load_json_array_fixture("nodes/tasks.json", DOMAIN) ) + node_mock.version.get.return_value = load_json_object_fixture( + "nodes/version.json", DOMAIN + ) + node_mock.apt.update.get.return_value = load_json_array_fixture( + "nodes/update.json", DOMAIN + ) qemu_by_vmid = {int(vm["vmid"]): vm for vm in qemu_list} lxc_by_vmid = {int(vm["vmid"]): vm for vm in lxc_list} diff --git a/tests/components/proxmoxve/fixtures/nodes/update.json b/tests/components/proxmoxve/fixtures/nodes/update.json new file mode 100644 index 000000000000..7a272f14d025 --- /dev/null +++ b/tests/components/proxmoxve/fixtures/nodes/update.json @@ -0,0 +1,57 @@ +[ + { + "Arch": "amd64", + "Description": "This package provides the 'host' DNS lookup utility in the form that is bundled with the BIND 9 sources.", + "OldVersion": "1:9.20.18-1~deb13u1", + "Origin": "Debian", + "Package": "bind9-host", + "Priority": "standard", + "Section": "net", + "Title": "DNS Lookup Utility", + "Version": "1:9.20.21-1~deb13u1" + }, + { + "Arch": "all", + "Description": "This package contains the Proxmox Virtual Environment management tools.", + "OldVersion": "9.1.6", + "Origin": "Proxmox", + "Package": "pve-manager", + "Priority": "optional", + "Section": "admin", + "Title": "Proxmox Virtual Environment Management Tools", + "Version": "9.1.7" + }, + { + "Arch": "all", + "Description": "This package contains the base library used by other Proxmox VE components.", + "OldVersion": "9.1.7", + "Origin": "Proxmox", + "Package": "libpve-common-perl", + "Priority": "optional", + "Section": "perl", + "Title": "Proxmox VE base library", + "Version": "9.1.9" + }, + { + "Arch": "amd64", + "Description": "The Corosync Cluster Engine is a Group Communication System with additional features for implementing high availability within applications. The project provides four C Application Programming Interface features: . * A closed process group communication model with virtual synchrony guarantees for creating replicated state machines. * A simple availability manager that restarts the application process when it has failed. * A configuration and statistics in-memory database that provide the ability to set, retrieve, and receive change notifications of information. * A quorum system that notifies applications when quorum is achieved or lost. . This package contains the Corosync daemon and some administration tools.", + "OldVersion": "3.1.10-pve1", + "Origin": "Proxmox", + "Package": "corosync", + "Priority": "optional", + "Section": "admin", + "Title": "cluster engine daemon and utilities", + "Version": "3.1.10-pve2" + }, + { + "Arch": "all", + "Description": "This package contains the binary firmware for various modules used in the pve-kernel.", + "OldVersion": "3.18-1", + "Origin": "Proxmox", + "Package": "pve-firmware", + "Priority": "optional", + "Section": "misc", + "Title": "Binary firmware code for the pve-kernel", + "Version": "3.18-2" + } +] diff --git a/tests/components/proxmoxve/fixtures/nodes/version.json b/tests/components/proxmoxve/fixtures/nodes/version.json new file mode 100644 index 000000000000..b76caed3299b --- /dev/null +++ b/tests/components/proxmoxve/fixtures/nodes/version.json @@ -0,0 +1,5 @@ +{ + "release": "9.1", + "repoid": "abcdefghijklmnop", + "version": "9.1.6" +} diff --git a/tests/components/proxmoxve/snapshots/test_diagnostics.ambr b/tests/components/proxmoxve/snapshots/test_diagnostics.ambr index b14f3294e236..690e3fb73495 100644 --- a/tests/components/proxmoxve/snapshots/test_diagnostics.ambr +++ b/tests/components/proxmoxve/snapshots/test_diagnostics.ambr @@ -129,6 +129,11 @@ 'used_fraction': 0.73, }), }), + 'version': dict({ + 'release': '9.1', + 'repoid': 'abcdefghijklmnop', + 'version': '9.1.6', + }), 'vms': dict({ '100': dict({ 'cpu': 0.15, diff --git a/tests/components/proxmoxve/snapshots/test_update.ambr b/tests/components/proxmoxve/snapshots/test_update.ambr new file mode 100644 index 000000000000..169283b73a1b --- /dev/null +++ b/tests/components/proxmoxve/snapshots/test_update.ambr @@ -0,0 +1,63 @@ +# serializer version: 1 +# name: test_all_entities[update.pve1_software_update-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.pve1_software_update', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Software update', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Software update', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'node_update', + 'unique_id': '1234_node/pve1_node_update', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[update.pve1_software_update-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : False, + : 0, + : '/api/brands/integration/proxmoxve/icon.png', + : 'pve1 Software update', + : False, + : '9.1.6', + : '9.1.9-p4-d1', + : 'A total of 5 package update(s) are pending installation: of these 4 relate to Proxmox and 1 to other updates. Please visit the [Proxmox VE node](https://127.0.0.1:8006/#v1:0:=node/pve1) for details on the pending updates and to upgrade to 9.1.9.', + : None, + : None, + : , + : None, + : None, + }), + 'context': , + 'entity_id': 'update.pve1_software_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/proxmoxve/test_update.py b/tests/components/proxmoxve/test_update.py new file mode 100644 index 000000000000..d15f2327ca3d --- /dev/null +++ b/tests/components/proxmoxve/test_update.py @@ -0,0 +1,121 @@ +"""Tests for the Proxmox VE update platform.""" + +from unittest.mock import MagicMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import AUDIT_PERMISSIONS, MERGED_PERMISSIONS, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform +from tests.typing import WebSocketGenerator + +ENTITY_ID = "update.pve1_software_update" + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + # Ensure Sys.Modify permissions to ensure update status can be determined + mock_proxmox_client.access.permissions.get.return_value = MERGED_PERMISSIONS + with patch( + "homeassistant.components.proxmoxve.PLATFORMS", + [Platform.UPDATE], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_update_entities_ignored( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that updates entities are not created with only auditor permissions.""" + mock_proxmox_client.access.permissions.get.return_value = AUDIT_PERMISSIONS + + with patch( + "homeassistant.components.proxmoxve.PLATFORMS", + [Platform.UPDATE], + ): + await setup_integration(hass, mock_config_entry) + + assert hass.states.get(ENTITY_ID) is None + + +async def test_update_unavailable_on_permission_change( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that updates entities are unavailable with only auditor permissions.""" + mock_proxmox_client.access.permissions.get.return_value = MERGED_PERMISSIONS + + with patch( + "homeassistant.components.proxmoxve.PLATFORMS", + [Platform.UPDATE], + ): + await setup_integration(hass, mock_config_entry) + + assert hass.states.get(ENTITY_ID).state is not None + + mock_proxmox_client.access.permissions.get.return_value = AUDIT_PERMISSIONS + + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + +async def test_update_up_to_date( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that updates are up to date when no updates are pending.""" + mock_proxmox_client.access.permissions.get.return_value = MERGED_PERMISSIONS + mock_proxmox_client.nodes.return_value.apt.update.get.return_value = [] + + with patch( + "homeassistant.components.proxmoxve.PLATFORMS", + [Platform.UPDATE], + ): + await setup_integration(hass, mock_config_entry) + + state = hass.states.get(ENTITY_ID) + assert state.attributes.get("latest_version") == "9.1.6" + assert state.state == STATE_OFF + + +async def test_update_release_notes( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test that updates release notes are correctly set.""" + mock_proxmox_client.access.permissions.get.return_value = MERGED_PERMISSIONS + + with patch( + "homeassistant.components.proxmoxve.PLATFORMS", + [Platform.UPDATE], + ): + await setup_integration(hass, mock_config_entry) + + ws_client = await hass_ws_client(hass) + await ws_client.send_json( + {"id": 1, "type": "update/release_notes", "entity_id": ENTITY_ID} + ) + result = await ws_client.receive_json() + assert "5 package" in result["result"]