From 07b9877f641cc98ee71688bbbf973ffa6cddcd9e Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 24 Feb 2026 14:24:20 +0100 Subject: [PATCH] Add button platform to Proxmox (#163791) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joostlek --- .../components/proxmoxve/__init__.py | 5 +- homeassistant/components/proxmoxve/button.py | 339 +++++ homeassistant/components/proxmoxve/icons.json | 18 + .../components/proxmoxve/strings.json | 35 + tests/components/proxmoxve/conftest.py | 46 +- .../proxmoxve/snapshots/test_button.ambr | 1183 +++++++++++++++++ tests/components/proxmoxve/test_button.py | 315 +++++ 7 files changed, 1922 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/proxmoxve/button.py create mode 100644 homeassistant/components/proxmoxve/icons.json create mode 100644 tests/components/proxmoxve/snapshots/test_button.ambr create mode 100644 tests/components/proxmoxve/test_button.py diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 1f5e3eae2f9..d3e74f7981c 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -37,7 +37,10 @@ from .const import ( ) from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, +] CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/proxmoxve/button.py b/homeassistant/components/proxmoxve/button.py new file mode 100644 index 00000000000..8f8e3ddeb72 --- /dev/null +++ b/homeassistant/components/proxmoxve/button.py @@ -0,0 +1,339 @@ +"""Button platform for Proxmox VE.""" + +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from proxmoxer import AuthenticationError +from proxmoxer.core import ResourceException +import requests +from requests.exceptions import ConnectTimeout, SSLError + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData +from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity + + +@dataclass(frozen=True, kw_only=True) +class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription): + """Class to hold Proxmox node button description.""" + + press_action: Callable[[ProxmoxCoordinator, str], None] + + +@dataclass(frozen=True, kw_only=True) +class ProxmoxVMButtonEntityDescription(ButtonEntityDescription): + """Class to hold Proxmox VM button description.""" + + press_action: Callable[[ProxmoxCoordinator, str, int], None] + + +@dataclass(frozen=True, kw_only=True) +class ProxmoxContainerButtonEntityDescription(ButtonEntityDescription): + """Class to hold Proxmox container button description.""" + + press_action: Callable[[ProxmoxCoordinator, str, int], None] + + +NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = ( + ProxmoxNodeButtonNodeEntityDescription( + key="reboot", + press_action=lambda coordinator, node: coordinator.proxmox.nodes( + node + ).status.post(command="reboot"), + entity_category=EntityCategory.CONFIG, + device_class=ButtonDeviceClass.RESTART, + ), + ProxmoxNodeButtonNodeEntityDescription( + key="shutdown", + translation_key="shutdown", + press_action=lambda coordinator, node: coordinator.proxmox.nodes( + node + ).status.post(command="shutdown"), + entity_category=EntityCategory.CONFIG, + ), + ProxmoxNodeButtonNodeEntityDescription( + key="start_all", + translation_key="start_all", + press_action=lambda coordinator, node: coordinator.proxmox.nodes( + node + ).startall.post(), + entity_category=EntityCategory.CONFIG, + ), + ProxmoxNodeButtonNodeEntityDescription( + key="stop_all", + translation_key="stop_all", + press_action=lambda coordinator, node: coordinator.proxmox.nodes( + node + ).stopall.post(), + entity_category=EntityCategory.CONFIG, + ), +) + +VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = ( + ProxmoxVMButtonEntityDescription( + key="start", + translation_key="start", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).qemu(vmid).status.start.post() + ), + entity_category=EntityCategory.CONFIG, + ), + ProxmoxVMButtonEntityDescription( + key="stop", + translation_key="stop", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).qemu(vmid).status.stop.post() + ), + entity_category=EntityCategory.CONFIG, + ), + ProxmoxVMButtonEntityDescription( + key="restart", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post() + ), + entity_category=EntityCategory.CONFIG, + device_class=ButtonDeviceClass.RESTART, + ), + ProxmoxVMButtonEntityDescription( + key="hibernate", + translation_key="hibernate", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).qemu(vmid).status.hibernate.post() + ), + entity_category=EntityCategory.CONFIG, + ), + ProxmoxVMButtonEntityDescription( + key="reset", + translation_key="reset", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).qemu(vmid).status.reset.post() + ), + entity_category=EntityCategory.CONFIG, + ), +) + +CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = ( + ProxmoxContainerButtonEntityDescription( + key="start", + translation_key="start", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).lxc(vmid).status.start.post() + ), + entity_category=EntityCategory.CONFIG, + ), + ProxmoxContainerButtonEntityDescription( + key="stop", + translation_key="stop", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).lxc(vmid).status.stop.post() + ), + entity_category=EntityCategory.CONFIG, + ), + ProxmoxContainerButtonEntityDescription( + key="restart", + press_action=lambda coordinator, node, vmid: ( + coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post() + ), + entity_category=EntityCategory.CONFIG, + device_class=ButtonDeviceClass.RESTART, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ProxmoxConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up ProxmoxVE buttons.""" + coordinator = entry.runtime_data + + def _async_add_new_nodes(nodes: list[ProxmoxNodeData]) -> None: + """Add new node buttons.""" + async_add_entities( + ProxmoxNodeButtonEntity(coordinator, entity_description, node) + for node in nodes + for entity_description in NODE_BUTTONS + ) + + def _async_add_new_vms( + vms: list[tuple[ProxmoxNodeData, dict[str, Any]]], + ) -> None: + """Add new VM buttons.""" + async_add_entities( + ProxmoxVMButtonEntity(coordinator, entity_description, vm, node_data) + for (node_data, vm) in vms + for entity_description in VM_BUTTONS + ) + + def _async_add_new_containers( + containers: list[tuple[ProxmoxNodeData, dict[str, Any]]], + ) -> None: + """Add new container buttons.""" + async_add_entities( + ProxmoxContainerButtonEntity( + coordinator, entity_description, container, node_data + ) + for (node_data, container) in containers + for entity_description in CONTAINER_BUTTONS + ) + + coordinator.new_nodes_callbacks.append(_async_add_new_nodes) + coordinator.new_vms_callbacks.append(_async_add_new_vms) + coordinator.new_containers_callbacks.append(_async_add_new_containers) + + _async_add_new_nodes( + [ + node_data + for node_data in coordinator.data.values() + if node_data.node["node"] in coordinator.known_nodes + ] + ) + _async_add_new_vms( + [ + (node_data, vm_data) + for node_data in coordinator.data.values() + for vmid, vm_data in node_data.vms.items() + if (node_data.node["node"], vmid) in coordinator.known_vms + ] + ) + _async_add_new_containers( + [ + (node_data, container_data) + for node_data in coordinator.data.values() + for vmid, container_data in node_data.containers.items() + if (node_data.node["node"], vmid) in coordinator.known_containers + ] + ) + + +class ProxmoxBaseButton(ButtonEntity): + """Common base for Proxmox buttons. Basically to ensure the async_press logic isn't duplicated.""" + + entity_description: ButtonEntityDescription + coordinator: ProxmoxCoordinator + + @abstractmethod + async def _async_press_call(self) -> None: + """Abstract method used per Proxmox button class.""" + + async def async_press(self) -> None: + """Trigger the Proxmox button press service.""" + try: + await self._async_press_call() + except AuthenticationError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_connect_no_details", + ) from err + except SSLError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="invalid_auth_no_details", + ) from err + except ConnectTimeout as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="timeout_connect_no_details", + ) from err + except (ResourceException, requests.exceptions.ConnectionError) as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_error_no_details", + ) from err + + +class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton): + """Represents a Proxmox Node button entity.""" + + entity_description: ProxmoxNodeButtonNodeEntityDescription + + def __init__( + self, + coordinator: ProxmoxCoordinator, + entity_description: ProxmoxNodeButtonNodeEntityDescription, + node_data: ProxmoxNodeData, + ) -> None: + """Initialize the Proxmox Node button entity.""" + self.entity_description = entity_description + super().__init__(coordinator, node_data) + + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{node_data.node['id']}_{entity_description.key}" + + async def _async_press_call(self) -> None: + """Execute the node button action via executor.""" + await self.hass.async_add_executor_job( + self.entity_description.press_action, + self.coordinator, + self._node_data.node["node"], + ) + + +class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton): + """Represents a Proxmox VM button entity.""" + + entity_description: ProxmoxVMButtonEntityDescription + + def __init__( + self, + coordinator: ProxmoxCoordinator, + entity_description: ProxmoxVMButtonEntityDescription, + vm_data: dict[str, Any], + node_data: ProxmoxNodeData, + ) -> None: + """Initialize the Proxmox VM button entity.""" + self.entity_description = entity_description + super().__init__(coordinator, vm_data, node_data) + + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_id}_{entity_description.key}" + + async def _async_press_call(self) -> None: + """Execute the VM button action via executor.""" + await self.hass.async_add_executor_job( + self.entity_description.press_action, + self.coordinator, + self._node_name, + self.vm_data["vmid"], + ) + + +class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton): + """Represents a Proxmox Container button entity.""" + + entity_description: ProxmoxContainerButtonEntityDescription + + def __init__( + self, + coordinator: ProxmoxCoordinator, + entity_description: ProxmoxContainerButtonEntityDescription, + container_data: dict[str, Any], + node_data: ProxmoxNodeData, + ) -> None: + """Initialize the Proxmox Container button entity.""" + self.entity_description = entity_description + super().__init__(coordinator, container_data, node_data) + + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_id}_{entity_description.key}" + + async def _async_press_call(self) -> None: + """Execute the container button action via executor.""" + await self.hass.async_add_executor_job( + self.entity_description.press_action, + self.coordinator, + self._node_name, + self.container_data["vmid"], + ) diff --git a/homeassistant/components/proxmoxve/icons.json b/homeassistant/components/proxmoxve/icons.json new file mode 100644 index 00000000000..023b977608b --- /dev/null +++ b/homeassistant/components/proxmoxve/icons.json @@ -0,0 +1,18 @@ +{ + "entity": { + "button": { + "hibernate": { + "default": "mdi:power-sleep" + }, + "reset": { + "default": "mdi:restart" + }, + "start": { + "default": "mdi:play" + }, + "stop": { + "default": "mdi:stop" + } + } + } +} diff --git a/homeassistant/components/proxmoxve/strings.json b/homeassistant/components/proxmoxve/strings.json index b6e63ee802e..e8aa8b6f66a 100644 --- a/homeassistant/components/proxmoxve/strings.json +++ b/homeassistant/components/proxmoxve/strings.json @@ -54,15 +54,47 @@ "status": { "name": "Status" } + }, + "button": { + "hibernate": { + "name": "Hibernate" + }, + "reset": { + "name": "Reset" + }, + "shutdown": { + "name": "Shutdown" + }, + "start": { + "name": "Start" + }, + "start_all": { + "name": "Start all" + }, + "stop": { + "name": "Stop" + }, + "stop_all": { + "name": "Stop all" + } } }, "exceptions": { + "api_error_no_details": { + "message": "An error occurred while communicating with the Proxmox VE instance." + }, "cannot_connect": { "message": "An error occurred while trying to connect to the Proxmox VE instance: {error}" }, + "cannot_connect_no_details": { + "message": "Could not connect to the Proxmox VE instance." + }, "invalid_auth": { "message": "An error occurred while trying to authenticate: {error}" }, + "invalid_auth_no_details": { + "message": "Authentication failed for the Proxmox VE instance." + }, "no_nodes_found": { "message": "No active nodes were found on the Proxmox VE server." }, @@ -71,6 +103,9 @@ }, "timeout_connect": { "message": "A timeout occurred while trying to connect to the Proxmox VE instance: {error}" + }, + "timeout_connect_no_details": { + "message": "A timeout occurred while trying to connect to the Proxmox VE instance." } }, "issues": { diff --git a/tests/components/proxmoxve/conftest.py b/tests/components/proxmoxve/conftest.py index 934c93eeeb1..9ece3f99e45 100644 --- a/tests/components/proxmoxve/conftest.py +++ b/tests/components/proxmoxve/conftest.py @@ -89,31 +89,41 @@ def mock_proxmox_client(): qemu_by_vmid = {vm["vmid"]: vm for vm in qemu_list} lxc_by_vmid = {vm["vmid"]: vm for vm in lxc_list} - # Note to reviewer: I will expand on these fixtures in a next PR - # Necessary evil to handle the binary_sensor tests properly + # Cache resource mocks by vmid so callers (e.g. button tests) can + # inspect specific call counts after pressing a button. + qemu_mocks: dict[int, MagicMock] = {} + lxc_mocks: dict[int, MagicMock] = {} + def _qemu_resource(vmid: int) -> MagicMock: - """Return a mock resource the QEMU.""" - resource = MagicMock() - vm = qemu_by_vmid[vmid] - resource.status.current.get.return_value = { - "name": vm["name"], - "status": vm["status"], - } - return resource + """Return a cached mock resource for a QEMU VM.""" + if vmid not in qemu_mocks: + resource = MagicMock() + vm = qemu_by_vmid[vmid] + resource.status.current.get.return_value = { + "name": vm["name"], + "status": vm["status"], + } + qemu_mocks[vmid] = resource + return qemu_mocks[vmid] def _lxc_resource(vmid: int) -> MagicMock: - """Return a mock resource the LXC.""" - resource = MagicMock() - ct = lxc_by_vmid[vmid] - resource.status.current.get.return_value = { - "name": ct["name"], - "status": ct["status"], - } - return resource + """Return a cached mock resource for an LXC container.""" + if vmid not in lxc_mocks: + resource = MagicMock() + ct = lxc_by_vmid[vmid] + resource.status.current.get.return_value = { + "name": ct["name"], + "status": ct["status"], + } + lxc_mocks[vmid] = resource + return lxc_mocks[vmid] node_mock.qemu.side_effect = _qemu_resource node_mock.lxc.side_effect = _lxc_resource + mock_instance._qemu_mocks = qemu_mocks + mock_instance._lxc_mocks = lxc_mocks + nodes_mock = MagicMock() nodes_mock.get.return_value = load_json_array_fixture( "nodes/nodes.json", DOMAIN diff --git a/tests/components/proxmoxve/snapshots/test_button.ambr b/tests/components/proxmoxve/snapshots/test_button.ambr new file mode 100644 index 00000000000..b25914b5443 --- /dev/null +++ b/tests/components/proxmoxve/snapshots/test_button.ambr @@ -0,0 +1,1183 @@ +# serializer version: 1 +# name: test_all_button_entities[button.ct_backup_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ct_backup_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Restart', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234_201_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.ct_backup_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'ct-backup Restart', + }), + 'context': , + 'entity_id': 'button.ct_backup_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.ct_backup_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ct_backup_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Start', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': '1234_201_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.ct_backup_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ct-backup Start', + }), + 'context': , + 'entity_id': 'button.ct_backup_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.ct_backup_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ct_backup_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Stop', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': '1234_201_stop', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.ct_backup_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ct-backup Stop', + }), + 'context': , + 'entity_id': 'button.ct_backup_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.ct_nginx_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ct_nginx_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Restart', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234_200_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.ct_nginx_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'ct-nginx Restart', + }), + 'context': , + 'entity_id': 'button.ct_nginx_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.ct_nginx_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ct_nginx_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Start', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': '1234_200_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.ct_nginx_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ct-nginx Start', + }), + 'context': , + 'entity_id': 'button.ct_nginx_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.ct_nginx_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ct_nginx_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Stop', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': '1234_200_stop', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.ct_nginx_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ct-nginx Stop', + }), + 'context': , + 'entity_id': 'button.ct_nginx_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve1_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve1_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Restart', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234_node/pve1_reboot', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve1_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'pve1 Restart', + }), + 'context': , + 'entity_id': 'button.pve1_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve1_shutdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve1_shutdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Shutdown', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Shutdown', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'shutdown', + 'unique_id': '1234_node/pve1_shutdown', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve1_shutdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pve1 Shutdown', + }), + 'context': , + 'entity_id': 'button.pve1_shutdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve1_start_all-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve1_start_all', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Start all', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start all', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'start_all', + 'unique_id': '1234_node/pve1_start_all', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve1_start_all-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pve1 Start all', + }), + 'context': , + 'entity_id': 'button.pve1_start_all', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve1_stop_all-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve1_stop_all', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Stop all', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop all', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'stop_all', + 'unique_id': '1234_node/pve1_stop_all', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve1_stop_all-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pve1 Stop all', + }), + 'context': , + 'entity_id': 'button.pve1_stop_all', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve2_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve2_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Restart', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234_node/pve2_reboot', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve2_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'pve2 Restart', + }), + 'context': , + 'entity_id': 'button.pve2_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve2_shutdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve2_shutdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Shutdown', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Shutdown', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'shutdown', + 'unique_id': '1234_node/pve2_shutdown', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve2_shutdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pve2 Shutdown', + }), + 'context': , + 'entity_id': 'button.pve2_shutdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve2_start_all-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve2_start_all', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Start all', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start all', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'start_all', + 'unique_id': '1234_node/pve2_start_all', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve2_start_all-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pve2 Start all', + }), + 'context': , + 'entity_id': 'button.pve2_start_all', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.pve2_stop_all-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pve2_stop_all', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Stop all', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop all', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'stop_all', + 'unique_id': '1234_node/pve2_stop_all', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.pve2_stop_all-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pve2 Stop all', + }), + 'context': , + 'entity_id': 'button.pve2_stop_all', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_db_hibernate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_db_hibernate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Hibernate', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hibernate', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'hibernate', + 'unique_id': '1234_101_hibernate', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_db_hibernate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-db Hibernate', + }), + 'context': , + 'entity_id': 'button.vm_db_hibernate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_db_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_db_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset', + 'unique_id': '1234_101_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_db_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-db Reset', + }), + 'context': , + 'entity_id': 'button.vm_db_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_db_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_db_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Restart', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234_101_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_db_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'vm-db Restart', + }), + 'context': , + 'entity_id': 'button.vm_db_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_db_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_db_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Start', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': '1234_101_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_db_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-db Start', + }), + 'context': , + 'entity_id': 'button.vm_db_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_db_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_db_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Stop', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': '1234_101_stop', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_db_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-db Stop', + }), + 'context': , + 'entity_id': 'button.vm_db_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_web_hibernate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_web_hibernate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Hibernate', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hibernate', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'hibernate', + 'unique_id': '1234_100_hibernate', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_web_hibernate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-web Hibernate', + }), + 'context': , + 'entity_id': 'button.vm_web_hibernate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_web_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_web_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset', + 'unique_id': '1234_100_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_web_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-web Reset', + }), + 'context': , + 'entity_id': 'button.vm_web_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_web_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_web_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Restart', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234_100_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_web_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'vm-web Restart', + }), + 'context': , + 'entity_id': 'button.vm_web_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_web_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_web_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Start', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': '1234_100_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_web_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-web Start', + }), + 'context': , + 'entity_id': 'button.vm_web_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_button_entities[button.vm_web_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vm_web_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Stop', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'proxmoxve', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': '1234_100_stop', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_button_entities[button.vm_web_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'vm-web Stop', + }), + 'context': , + 'entity_id': 'button.vm_web_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/proxmoxve/test_button.py b/tests/components/proxmoxve/test_button.py new file mode 100644 index 00000000000..f2d6a462c2b --- /dev/null +++ b/tests/components/proxmoxve/test_button.py @@ -0,0 +1,315 @@ +"""Tests for the ProxmoxVE button platform.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from proxmoxer import AuthenticationError +from proxmoxer.core import ResourceException +import pytest +from requests.exceptions import ConnectTimeout, SSLError +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +BUTTON_DOMAIN = "button" + + +@pytest.fixture(autouse=True) +def enable_all_entities(entity_registry_enabled_by_default: None) -> None: + """Enable all entities for button tests.""" + + +async def test_all_button_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Snapshot test for all ProxmoxVE button entities.""" + with patch( + "homeassistant.components.proxmoxve.PLATFORMS", + [Platform.BUTTON], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.parametrize( + ("entity_id", "command"), + [ + ("button.pve1_restart", "reboot"), + ("button.pve1_shutdown", "shutdown"), + ], +) +async def test_node_buttons( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + command: str, +) -> None: + """Test pressing a ProxmoxVE node action button triggers the correct API call.""" + await setup_integration(hass, mock_config_entry) + + method_mock = mock_proxmox_client._node_mock.status.post + pre_calls = len(method_mock.mock_calls) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(method_mock.mock_calls) == pre_calls + 1 + method_mock.assert_called_with(command=command) + + +@pytest.mark.parametrize( + ("entity_id", "attr"), + [ + ("button.pve1_start_all", "startall"), + ("button.pve1_stop_all", "stopall"), + ], +) +async def test_node_startall_stopall_buttons( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + attr: str, +) -> None: + """Test pressing a ProxmoxVE node start all / stop all button triggers the correct API call.""" + await setup_integration(hass, mock_config_entry) + + method_mock = getattr(mock_proxmox_client._node_mock, attr).post + pre_calls = len(method_mock.mock_calls) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(method_mock.mock_calls) == pre_calls + 1 + + +@pytest.mark.parametrize( + ("entity_id", "vmid", "action"), + [ + ("button.vm_web_start", 100, "start"), + ("button.vm_web_stop", 100, "stop"), + ("button.vm_web_restart", 100, "restart"), + ("button.vm_web_hibernate", 100, "hibernate"), + ("button.vm_web_reset", 100, "reset"), + ], +) +async def test_vm_buttons( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + vmid: int, + action: str, +) -> None: + """Test pressing a ProxmoxVE VM action button triggers the correct API call.""" + await setup_integration(hass, mock_config_entry) + + mock_proxmox_client._node_mock.qemu(vmid) + method_mock = getattr(mock_proxmox_client._qemu_mocks[vmid].status, action).post + pre_calls = len(method_mock.mock_calls) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(method_mock.mock_calls) == pre_calls + 1 + + +@pytest.mark.parametrize( + ("entity_id", "vmid", "action"), + [ + ("button.ct_nginx_start", 200, "start"), + ("button.ct_nginx_stop", 200, "stop"), + ("button.ct_nginx_restart", 200, "restart"), + ], +) +async def test_container_buttons( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + vmid: int, + action: str, +) -> None: + """Test pressing a ProxmoxVE container action button triggers the correct API call.""" + await setup_integration(hass, mock_config_entry) + + mock_proxmox_client._node_mock.lxc(vmid) + method_mock = getattr(mock_proxmox_client._lxc_mocks[vmid].status, action).post + pre_calls = len(method_mock.mock_calls) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(method_mock.mock_calls) == pre_calls + 1 + + +@pytest.mark.parametrize( + ("entity_id", "exception"), + [ + ("button.pve1_restart", AuthenticationError("auth failed")), + ("button.pve1_restart", SSLError("ssl error")), + ("button.pve1_restart", ConnectTimeout("timeout")), + ("button.pve1_shutdown", ResourceException(500, "error", {})), + ], +) +async def test_node_buttons_exceptions( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + exception: Exception, +) -> None: + """Test that ProxmoxVE node button errors are raised as HomeAssistantError.""" + await setup_integration(hass, mock_config_entry) + + mock_proxmox_client._node_mock.status.post.side_effect = exception + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + +@pytest.mark.parametrize( + ("entity_id", "vmid", "action", "exception"), + [ + ( + "button.vm_web_start", + 100, + "start", + AuthenticationError("auth failed"), + ), + ( + "button.vm_web_start", + 100, + "start", + SSLError("ssl error"), + ), + ( + "button.vm_web_hibernate", + 100, + "hibernate", + ConnectTimeout("timeout"), + ), + ( + "button.vm_web_reset", + 100, + "reset", + ResourceException(500, "error", {}), + ), + ], +) +async def test_vm_buttons_exceptions( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + vmid: int, + action: str, + exception: Exception, +) -> None: + """Test that ProxmoxVE VM button errors are raised as HomeAssistantError.""" + await setup_integration(hass, mock_config_entry) + + mock_proxmox_client._node_mock.qemu(vmid) + getattr( + mock_proxmox_client._qemu_mocks[vmid].status, action + ).post.side_effect = exception + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + +@pytest.mark.parametrize( + ("entity_id", "vmid", "action", "exception"), + [ + ( + "button.ct_nginx_start", + 200, + "start", + AuthenticationError("auth failed"), + ), + ( + "button.ct_nginx_start", + 200, + "start", + SSLError("ssl error"), + ), + ( + "button.ct_nginx_restart", + 200, + "restart", + ConnectTimeout("timeout"), + ), + ( + "button.ct_nginx_stop", + 200, + "stop", + ResourceException(500, "error", {}), + ), + ], +) +async def test_container_buttons_exceptions( + hass: HomeAssistant, + mock_proxmox_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + vmid: int, + action: str, + exception: Exception, +) -> None: + """Test that ProxmoxVE container button errors are raised as HomeAssistantError.""" + await setup_integration(hass, mock_config_entry) + + mock_proxmox_client._node_mock.lxc(vmid) + getattr( + mock_proxmox_client._lxc_mocks[vmid].status, action + ).post.side_effect = exception + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + )