mirror of
https://github.com/home-assistant/core.git
synced 2026-07-08 07:15:09 +01:00
ef29183160
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
419 lines
14 KiB
Python
419 lines
14 KiB
Python
"""Button platform for Proxmox VE."""
|
|
|
|
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, ServiceValidationError
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from .const import DOMAIN, ProxmoxPermission
|
|
from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
|
|
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
|
from .helpers import is_granted
|
|
|
|
NO_PERM_VM_LXC_POWER = "no_permission_vm_lxc_power"
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription):
|
|
"""Class to hold Proxmox node button description."""
|
|
|
|
press_action: Callable[[ProxmoxCoordinator, str], None]
|
|
permission: ProxmoxPermission = ProxmoxPermission.SYSPOWER
|
|
permission_raise: str = "no_permission_node_power"
|
|
permission_target: str = "nodes"
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class ProxmoxVMButtonEntityDescription(ButtonEntityDescription):
|
|
"""Class to hold Proxmox VM button description."""
|
|
|
|
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
|
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
|
permission_raise: str = NO_PERM_VM_LXC_POWER
|
|
permission_target: str = "vms"
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class ProxmoxContainerButtonEntityDescription(ButtonEntityDescription):
|
|
"""Class to hold Proxmox container button description."""
|
|
|
|
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
|
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
|
permission_raise: str = NO_PERM_VM_LXC_POWER
|
|
permission_target: str = "vms"
|
|
|
|
|
|
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",
|
|
permission=ProxmoxPermission.POWER,
|
|
permission_raise=NO_PERM_VM_LXC_POWER,
|
|
permission_target="vms",
|
|
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
|
|
node
|
|
).startall.post(),
|
|
entity_category=EntityCategory.CONFIG,
|
|
),
|
|
ProxmoxNodeButtonNodeEntityDescription(
|
|
key="stop_all",
|
|
translation_key="stop_all",
|
|
permission=ProxmoxPermission.POWER,
|
|
permission_raise=NO_PERM_VM_LXC_POWER,
|
|
permission_target="vms",
|
|
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
|
|
node
|
|
).stopall.post(),
|
|
entity_category=EntityCategory.CONFIG,
|
|
),
|
|
ProxmoxNodeButtonNodeEntityDescription(
|
|
key="suspend_all",
|
|
translation_key="suspend_all",
|
|
permission=ProxmoxPermission.POWER,
|
|
permission_raise=NO_PERM_VM_LXC_POWER,
|
|
permission_target="vms",
|
|
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
|
|
node
|
|
).suspendall.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.reboot.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="resume",
|
|
translation_key="resume",
|
|
press_action=lambda coordinator, node, vmid: (
|
|
coordinator.proxmox.nodes(node).qemu(vmid).status.resume.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,
|
|
),
|
|
ProxmoxVMButtonEntityDescription(
|
|
key="shutdown",
|
|
translation_key="shutdown",
|
|
press_action=lambda coordinator, node, vmid: (
|
|
coordinator.proxmox.nodes(node).qemu(vmid).status.shutdown.post()
|
|
),
|
|
entity_category=EntityCategory.CONFIG,
|
|
),
|
|
ProxmoxVMButtonEntityDescription(
|
|
key="snapshot_create",
|
|
translation_key="snapshot_create",
|
|
press_action=lambda coordinator, node, vmid: (
|
|
coordinator.proxmox.nodes(node)
|
|
.qemu(vmid)
|
|
.snapshot.post(
|
|
name=(
|
|
"homeassistant_snapshot_"
|
|
f"{coordinator.data[node].vms[vmid]['name']}_"
|
|
f"{dt_util.utcnow().strftime('%Y%m%d%H%M%S')}"
|
|
)
|
|
)
|
|
),
|
|
permission=ProxmoxPermission.SNAPSHOT,
|
|
permission_raise="no_permission_snapshot",
|
|
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.reboot.post()
|
|
),
|
|
entity_category=EntityCategory.CONFIG,
|
|
device_class=ButtonDeviceClass.RESTART,
|
|
),
|
|
ProxmoxContainerButtonEntityDescription(
|
|
key="snapshot_create",
|
|
translation_key="snapshot_create",
|
|
press_action=lambda coordinator, node, vmid: (
|
|
coordinator.proxmox.nodes(node)
|
|
.lxc(vmid)
|
|
.snapshot.post(
|
|
name=(
|
|
"homeassistant_snapshot_"
|
|
f"{coordinator.data[node].containers[vmid]['name']}_"
|
|
f"{dt_util.utcnow().strftime('%Y%m%d%H%M%S')}"
|
|
)
|
|
)
|
|
),
|
|
permission=ProxmoxPermission.SNAPSHOT,
|
|
permission_raise="no_permission_snapshot",
|
|
entity_category=EntityCategory.CONFIG,
|
|
),
|
|
)
|
|
|
|
|
|
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.
|
|
|
|
Ensures 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
|
|
|
|
async def _async_press_call(self) -> None:
|
|
"""Execute the node button action via executor."""
|
|
node_id = self._node_data.node["node"]
|
|
if not is_granted(
|
|
self.coordinator.permissions,
|
|
p_type=self.entity_description.permission_target,
|
|
p_id=node_id,
|
|
permission=self.entity_description.permission,
|
|
):
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key=self.entity_description.permission_raise,
|
|
)
|
|
await self.hass.async_add_executor_job(
|
|
self.entity_description.press_action,
|
|
self.coordinator,
|
|
node_id,
|
|
)
|
|
|
|
|
|
class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
|
"""Represents a Proxmox VM button entity."""
|
|
|
|
entity_description: ProxmoxVMButtonEntityDescription
|
|
|
|
async def _async_press_call(self) -> None:
|
|
"""Execute the VM button action via executor."""
|
|
vmid = self.vm_data["vmid"]
|
|
if not is_granted(
|
|
self.coordinator.permissions,
|
|
p_type=self.entity_description.permission_target,
|
|
p_id=vmid,
|
|
permission=self.entity_description.permission,
|
|
):
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key=self.entity_description.permission_raise,
|
|
)
|
|
await self.hass.async_add_executor_job(
|
|
self.entity_description.press_action,
|
|
self.coordinator,
|
|
self._node_name,
|
|
vmid,
|
|
)
|
|
|
|
|
|
class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
|
"""Represents a Proxmox Container button entity."""
|
|
|
|
entity_description: ProxmoxContainerButtonEntityDescription
|
|
|
|
async def _async_press_call(self) -> None:
|
|
"""Execute the container button action via executor."""
|
|
vmid = self.container_data["vmid"]
|
|
# Container power actions fall under vms
|
|
if not is_granted(
|
|
self.coordinator.permissions,
|
|
p_type=self.entity_description.permission_target,
|
|
p_id=vmid,
|
|
permission=self.entity_description.permission,
|
|
):
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key=self.entity_description.permission_raise,
|
|
)
|
|
await self.hass.async_add_executor_job(
|
|
self.entity_description.press_action,
|
|
self.coordinator,
|
|
self._node_name,
|
|
vmid,
|
|
)
|