mirror of
https://github.com/bahmcloud/easy_proxmox.git
synced 2026-04-06 19:01:14 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39fc090944 | |||
| 8f8ac50905 | |||
| 66b0db4a6d | |||
| fea7f3c4af |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.3
|
||||
- Fixed service execution when using device targets in automations and scripts
|
||||
- Services now work correctly on Home Assistant versions where `ServiceCall.target` is not available
|
||||
- Improved target resolution:
|
||||
- Supports `device_id` passed via UI targets and via service data
|
||||
- Supports `entity_id` targets and automatically resolves them to the corresponding device
|
||||
- Accepts both `str` and `list[str]` formats for target identifiers
|
||||
- Fixed issue where service calls were accepted but no Proxmox action was executed
|
||||
- Improved compatibility with the Home Assistant automation editor and mobile UI
|
||||
|
||||
## 0.7.2
|
||||
- Fixed service validation for device targets:
|
||||
- Home Assistant may pass `device_id` as a list (target/data wrapper)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"domain": "proxmox_pve",
|
||||
"name": "Easy Proxmox (by René Bachmann)",
|
||||
"version": "0.7.1",
|
||||
"documentation": "https://example.invalid",
|
||||
"version": "0.7.3",
|
||||
"documentation": "https://github.com/bahmcloud/easy_proxmox ",
|
||||
"requirements": ["aiohttp>=3.9.0"],
|
||||
"codeowners": ["@BAHMCLOUD"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -5,6 +5,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -16,6 +17,7 @@ SERVICE_STOP_HARD = "stop_hard"
|
||||
SERVICE_REBOOT = "reboot"
|
||||
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_ENTITY_ID = "entity_id"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
ATTR_HOST = "host"
|
||||
ATTR_NODE = "node"
|
||||
@@ -24,11 +26,11 @@ ATTR_TYPE = "type"
|
||||
|
||||
VALID_TYPES = ("qemu", "lxc")
|
||||
|
||||
# IMPORTANT:
|
||||
# HA may pass device_id via target (list) OR data (list) depending on UI/script wrapper.
|
||||
# Accept device_id passed as str or list[str] (depends on HA UI/script)
|
||||
SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_DEVICE_ID): vol.Any(str, [str]),
|
||||
vol.Optional(ATTR_ENTITY_ID): vol.Any(str, [str]),
|
||||
vol.Optional(ATTR_CONFIG_ENTRY_ID): str,
|
||||
vol.Optional(ATTR_HOST): str,
|
||||
vol.Optional(ATTR_NODE): str,
|
||||
@@ -39,7 +41,6 @@ SERVICE_SCHEMA = vol.Schema(
|
||||
|
||||
|
||||
def _first_str(value: Any) -> str | None:
|
||||
"""Return first string from str or list[str], else None."""
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
if isinstance(value, list) and value:
|
||||
@@ -49,25 +50,54 @@ def _first_str(value: Any) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _get_target_device_id(call: ServiceCall) -> str | None:
|
||||
"""
|
||||
Prefer call.target.device_id (UI target).
|
||||
Fallback to call.data.device_id (some wrappers convert target to data).
|
||||
"""
|
||||
if call.target and isinstance(call.target, dict):
|
||||
dev_id = _first_str(call.target.get("device_id"))
|
||||
if dev_id:
|
||||
return dev_id
|
||||
def _first_str_from_target(target: Any, key: str) -> str | None:
|
||||
if isinstance(target, dict):
|
||||
return _first_str(target.get(key))
|
||||
return None
|
||||
|
||||
# fallback: data.device_id can be str or list[str]
|
||||
return _first_str(call.data.get(ATTR_DEVICE_ID))
|
||||
|
||||
def _get_device_id(hass: HomeAssistant, call: ServiceCall) -> str | None:
|
||||
"""
|
||||
Robust device_id extraction across HA versions.
|
||||
|
||||
Priority:
|
||||
1) call.target.device_id (newer HA)
|
||||
2) call.data.device_id (some wrappers)
|
||||
3) call.target.entity_id -> map to device_id
|
||||
4) call.data.entity_id -> map to device_id
|
||||
"""
|
||||
target = getattr(call, "target", None)
|
||||
|
||||
# 1) target.device_id
|
||||
dev_id = _first_str_from_target(target, "device_id")
|
||||
if dev_id:
|
||||
return dev_id
|
||||
|
||||
# 2) data.device_id
|
||||
dev_id = _first_str(call.data.get(ATTR_DEVICE_ID))
|
||||
if dev_id:
|
||||
return dev_id
|
||||
|
||||
# 3) target.entity_id -> device_id
|
||||
ent_id = _first_str_from_target(target, "entity_id")
|
||||
if ent_id:
|
||||
ent_reg = er.async_get(hass)
|
||||
ent = ent_reg.async_get(ent_id)
|
||||
if ent and ent.device_id:
|
||||
return ent.device_id
|
||||
|
||||
# 4) data.entity_id -> device_id
|
||||
ent_id = _first_str(call.data.get(ATTR_ENTITY_ID))
|
||||
if ent_id:
|
||||
ent_reg = er.async_get(hass)
|
||||
ent = ent_reg.async_get(ent_id)
|
||||
if ent and ent.device_id:
|
||||
return ent.device_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _parse_guest_identifier(identifier: str) -> Tuple[str, str, int]:
|
||||
"""
|
||||
Guest device identifier format: "node:type:vmid"
|
||||
Example: "pve1:qemu:100"
|
||||
"""
|
||||
parts = identifier.split(":")
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"Invalid guest identifier: {identifier}")
|
||||
@@ -79,8 +109,7 @@ def _parse_guest_identifier(identifier: str) -> Tuple[str, str, int]:
|
||||
|
||||
|
||||
def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, int]:
|
||||
"""Resolve node/type/vmid from device target OR node+vmid (+ optional type)."""
|
||||
device_id = _get_target_device_id(call)
|
||||
device_id = _get_device_id(hass, call)
|
||||
node = call.data.get(ATTR_NODE)
|
||||
vmid = call.data.get(ATTR_VMID)
|
||||
vmtype = call.data.get(ATTR_TYPE, "qemu")
|
||||
@@ -101,7 +130,7 @@ def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, i
|
||||
raise ValueError(f"Selected device has no Easy Proxmox guest identifier: {device_id}")
|
||||
|
||||
if not node or vmid is None:
|
||||
raise ValueError("Provide a Device target OR node + vmid (+ optional type/host/config_entry_id).")
|
||||
raise ValueError("Provide a Device/Entity target OR node + vmid (+ optional type/host/config_entry_id).")
|
||||
|
||||
if vmtype not in VALID_TYPES:
|
||||
raise ValueError(f"Invalid type: {vmtype} (allowed: {VALID_TYPES})")
|
||||
@@ -171,17 +200,17 @@ def _pick_entry_id_by_guest_lookup(hass: HomeAssistant, node: str, vmtype: str,
|
||||
if not matches:
|
||||
raise ValueError(
|
||||
f"Could not find guest {node}/{vmtype}/{vmid} in any configured Proxmox host. "
|
||||
"Provide host or config_entry_id, or use device target."
|
||||
"Provide host or config_entry_id, or use a Device/Entity target."
|
||||
)
|
||||
if len(matches) > 1:
|
||||
raise ValueError(
|
||||
f"Guest {node}/{vmtype}/{vmid} exists on multiple configured hosts (ambiguous). "
|
||||
"Please provide host or config_entry_id, or use device target."
|
||||
"Please provide host or config_entry_id, or use a Device/Entity target."
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, target: Tuple[str, str, int]) -> str:
|
||||
def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, node: str, vmtype: str, vmid: int) -> str:
|
||||
domain_entries = _get_domain_entries(hass)
|
||||
|
||||
config_entry_id = call.data.get(ATTR_CONFIG_ENTRY_ID)
|
||||
@@ -190,7 +219,7 @@ def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, target: Tuple[str,
|
||||
raise ValueError(f"config_entry_id '{config_entry_id}' not found or not loaded.")
|
||||
return config_entry_id
|
||||
|
||||
device_id = _get_target_device_id(call)
|
||||
device_id = _get_device_id(hass, call)
|
||||
if device_id:
|
||||
return _pick_entry_id_for_device(hass, device_id)
|
||||
|
||||
@@ -198,7 +227,6 @@ def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, target: Tuple[str,
|
||||
if host:
|
||||
return _pick_entry_id_by_host(hass, host)
|
||||
|
||||
node, vmtype, vmid = target
|
||||
return _pick_entry_id_by_guest_lookup(hass, node, vmtype, vmid)
|
||||
|
||||
|
||||
@@ -208,15 +236,14 @@ async def async_register_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def _call_action(call: ServiceCall, action: str) -> None:
|
||||
node, vmtype, vmid = _resolve_target(hass, call)
|
||||
entry_id = _resolve_entry_id(hass, call, (node, vmtype, vmid))
|
||||
entry_id = _resolve_entry_id(hass, call, node, vmtype, vmid)
|
||||
|
||||
domain_entries = _get_domain_entries(hass)
|
||||
entry_data = domain_entries.get(entry_id)
|
||||
entry_data = _get_domain_entries(hass).get(entry_id)
|
||||
if not isinstance(entry_data, dict) or not entry_data.get("client"):
|
||||
raise ValueError(f"Selected config entry '{entry_id}' has no client (not loaded).")
|
||||
|
||||
client = entry_data["client"]
|
||||
_LOGGER.debug("Service action=%s entry=%s target=%s/%s/%s", action, entry_id, node, vmtype, vmid)
|
||||
_LOGGER.debug("Service action=%s entry=%s target=%s/%s/%s data=%s", action, entry_id, node, vmtype, vmid, call.data)
|
||||
await client.guest_action(node=node, vmid=vmid, vmtype=vmtype, action=action)
|
||||
|
||||
async def handle_start(call: ServiceCall) -> None:
|
||||
|
||||
Reference in New Issue
Block a user