mirror of
https://github.com/bahmcloud/easy_proxmox.git
synced 2026-04-06 10:51:14 +00:00
Implement Proxmox service handling for VMs
This file implements service handling for Proxmox virtual machines, including actions like start, shutdown, stop, and reboot. It defines service schemas, resolves target identifiers, and registers/unregisters the services with Home Assistant.
This commit is contained in:
133
custom_components/proxmox_pve/services.py
Normal file
133
custom_components/proxmox_pve/services.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import logging
|
||||
from typing import Any, Tuple
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_START = "start"
|
||||
SERVICE_SHUTDOWN = "shutdown"
|
||||
SERVICE_STOP_HARD = "stop_hard"
|
||||
SERVICE_REBOOT = "reboot"
|
||||
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_NODE = "node"
|
||||
ATTR_VMID = "vmid"
|
||||
ATTR_TYPE = "type"
|
||||
|
||||
VALID_TYPES = ("qemu", "lxc")
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_DEVICE_ID): str,
|
||||
vol.Optional(ATTR_NODE): str,
|
||||
vol.Optional(ATTR_VMID): vol.Coerce(int),
|
||||
vol.Optional(ATTR_TYPE, default="qemu"): vol.In(VALID_TYPES),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _parse_guest_identifier(identifier: str) -> Tuple[str, str, int]:
|
||||
"""
|
||||
Our device identifier for guests is: "node:type:vmid"
|
||||
Example: "pve1:qemu:100"
|
||||
"""
|
||||
parts = identifier.split(":")
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"Invalid guest identifier: {identifier}")
|
||||
node, vmtype, vmid_s = parts
|
||||
vmid = int(vmid_s)
|
||||
if vmtype not in VALID_TYPES:
|
||||
raise ValueError(f"Invalid VM type: {vmtype}")
|
||||
return node, vmtype, vmid
|
||||
|
||||
|
||||
def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, int]:
|
||||
"""Resolve node/type/vmid from either device_id or node+vmid."""
|
||||
device_id = call.data.get(ATTR_DEVICE_ID)
|
||||
node = call.data.get(ATTR_NODE)
|
||||
vmid = call.data.get(ATTR_VMID)
|
||||
vmtype = call.data.get(ATTR_TYPE, "qemu")
|
||||
|
||||
if device_id:
|
||||
dev_reg = dr.async_get(hass)
|
||||
device = dev_reg.async_get(device_id)
|
||||
if not device:
|
||||
raise ValueError(f"Device not found: {device_id}")
|
||||
|
||||
# Find our identifiers
|
||||
for ident_domain, ident_value in device.identifiers:
|
||||
if ident_domain != DOMAIN:
|
||||
continue
|
||||
# Node devices are "node:<name>" — we need guest identifiers only
|
||||
if ident_value.startswith("node:"):
|
||||
continue
|
||||
# Guest devices are "node:type:vmid"
|
||||
return _parse_guest_identifier(ident_value)
|
||||
|
||||
raise ValueError(f"Selected device has no Easy Proxmox guest identifier: {device_id}")
|
||||
|
||||
# Fallback: manual node/vmid
|
||||
if not node or vmid is None:
|
||||
raise ValueError("Provide either device_id OR node + vmid (+ optional type).")
|
||||
|
||||
if vmtype not in VALID_TYPES:
|
||||
raise ValueError(f"Invalid type: {vmtype} (allowed: {VALID_TYPES})")
|
||||
|
||||
return str(node), str(vmtype), int(vmid)
|
||||
|
||||
|
||||
async def async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Register domain services once."""
|
||||
if hass.services.has_service(DOMAIN, SERVICE_START):
|
||||
return
|
||||
|
||||
async def _call_action(call: ServiceCall, action: str) -> None:
|
||||
node, vmtype, vmid = _resolve_target(hass, call)
|
||||
|
||||
# Find the corresponding config entry client
|
||||
# If multiple entries exist, we just use the first one that is loaded.
|
||||
domain_data: dict[str, Any] = hass.data.get(DOMAIN, {})
|
||||
if not domain_data:
|
||||
raise ValueError("Easy Proxmox is not set up.")
|
||||
|
||||
client = None
|
||||
for entry_id, entry_data in domain_data.items():
|
||||
if isinstance(entry_data, dict) and entry_data.get("client"):
|
||||
client = entry_data["client"]
|
||||
break
|
||||
|
||||
if client is None:
|
||||
raise ValueError("No Proxmox client available (integration not loaded).")
|
||||
|
||||
_LOGGER.debug("Service action=%s target=%s/%s/%s", action, node, vmtype, vmid)
|
||||
await client.guest_action(node=node, vmid=vmid, vmtype=vmtype, action=action)
|
||||
|
||||
async def handle_start(call: ServiceCall) -> None:
|
||||
await _call_action(call, "start")
|
||||
|
||||
async def handle_shutdown(call: ServiceCall) -> None:
|
||||
await _call_action(call, "shutdown")
|
||||
|
||||
async def handle_stop_hard(call: ServiceCall) -> None:
|
||||
await _call_action(call, "stop")
|
||||
|
||||
async def handle_reboot(call: ServiceCall) -> None:
|
||||
await _call_action(call, "reboot")
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_START, handle_start, schema=SERVICE_SCHEMA)
|
||||
hass.services.async_register(DOMAIN, SERVICE_SHUTDOWN, handle_shutdown, schema=SERVICE_SCHEMA)
|
||||
hass.services.async_register(DOMAIN, SERVICE_STOP_HARD, handle_stop_hard, schema=SERVICE_SCHEMA)
|
||||
hass.services.async_register(DOMAIN, SERVICE_REBOOT, handle_reboot, schema=SERVICE_SCHEMA)
|
||||
|
||||
|
||||
async def async_unregister_services(hass: HomeAssistant) -> None:
|
||||
"""Unregister services (optional, usually not required, but clean)."""
|
||||
for svc in (SERVICE_START, SERVICE_SHUTDOWN, SERVICE_STOP_HARD, SERVICE_REBOOT):
|
||||
if hass.services.has_service(DOMAIN, svc):
|
||||
hass.services.async_remove(DOMAIN, svc)
|
||||
Reference in New Issue
Block a user