mirror of
https://github.com/bahmcloud/easy_proxmox.git
synced 2026-04-06 10:51:14 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ceb5353ae9 | |||
| 64b95c0f17 |
@@ -1,4 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.7.2
|
||||||
|
- Fixed service validation for device targets:
|
||||||
|
- Home Assistant may pass `device_id` as a list (target/data wrapper)
|
||||||
|
- Services now accept both `str` and `list[str]` for `device_id`
|
||||||
|
- Improved device target parsing for UI and script wrappers
|
||||||
|
|
||||||
## 0.7.1
|
## 0.7.1
|
||||||
- Fixed Home Assistant service UI integration:
|
- Fixed Home Assistant service UI integration:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ SERVICE_SHUTDOWN = "shutdown"
|
|||||||
SERVICE_STOP_HARD = "stop_hard"
|
SERVICE_STOP_HARD = "stop_hard"
|
||||||
SERVICE_REBOOT = "reboot"
|
SERVICE_REBOOT = "reboot"
|
||||||
|
|
||||||
ATTR_DEVICE_ID = "device_id" # fallback if user manually puts it into data
|
ATTR_DEVICE_ID = "device_id"
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||||
ATTR_HOST = "host"
|
ATTR_HOST = "host"
|
||||||
ATTR_NODE = "node"
|
ATTR_NODE = "node"
|
||||||
@@ -24,10 +24,11 @@ ATTR_TYPE = "type"
|
|||||||
|
|
||||||
VALID_TYPES = ("qemu", "lxc")
|
VALID_TYPES = ("qemu", "lxc")
|
||||||
|
|
||||||
# NOTE: device selection in UI goes via call.target, not via call.data.
|
# IMPORTANT:
|
||||||
|
# HA may pass device_id via target (list) OR data (list) depending on UI/script wrapper.
|
||||||
SERVICE_SCHEMA = vol.Schema(
|
SERVICE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_DEVICE_ID): str,
|
vol.Optional(ATTR_DEVICE_ID): vol.Any(str, [str]),
|
||||||
vol.Optional(ATTR_CONFIG_ENTRY_ID): str,
|
vol.Optional(ATTR_CONFIG_ENTRY_ID): str,
|
||||||
vol.Optional(ATTR_HOST): str,
|
vol.Optional(ATTR_HOST): str,
|
||||||
vol.Optional(ATTR_NODE): str,
|
vol.Optional(ATTR_NODE): str,
|
||||||
@@ -37,23 +38,31 @@ SERVICE_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_target_device_id(call: ServiceCall) -> str | None:
|
def _first_str(value: Any) -> str | None:
|
||||||
"""Return single device_id selected in UI (call.target) or fallback call.data."""
|
"""Return first string from str or list[str], else None."""
|
||||||
# UI target: {"device_id": ["..."]}
|
if isinstance(value, str) and value.strip():
|
||||||
if call.target and isinstance(call.target, dict):
|
return value.strip()
|
||||||
dev_ids = call.target.get("device_id")
|
if isinstance(value, list) and value:
|
||||||
if isinstance(dev_ids, list) and dev_ids:
|
v0 = value[0]
|
||||||
return dev_ids[0]
|
if isinstance(v0, str) and v0.strip():
|
||||||
if isinstance(dev_ids, str):
|
return v0.strip()
|
||||||
return dev_ids
|
|
||||||
|
|
||||||
# YAML fallback: data.device_id
|
|
||||||
dev_id = call.data.get(ATTR_DEVICE_ID)
|
|
||||||
if isinstance(dev_id, str) and dev_id.strip():
|
|
||||||
return dev_id.strip()
|
|
||||||
return 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
|
||||||
|
|
||||||
|
# fallback: data.device_id can be str or list[str]
|
||||||
|
return _first_str(call.data.get(ATTR_DEVICE_ID))
|
||||||
|
|
||||||
|
|
||||||
def _parse_guest_identifier(identifier: str) -> Tuple[str, str, int]:
|
def _parse_guest_identifier(identifier: str) -> Tuple[str, str, int]:
|
||||||
"""
|
"""
|
||||||
Guest device identifier format: "node:type:vmid"
|
Guest device identifier format: "node:type:vmid"
|
||||||
@@ -82,18 +91,15 @@ def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, i
|
|||||||
if not device:
|
if not device:
|
||||||
raise ValueError(f"Device not found: {device_id}")
|
raise ValueError(f"Device not found: {device_id}")
|
||||||
|
|
||||||
# Find our guest identifier in device.identifiers
|
|
||||||
for ident_domain, ident_value in device.identifiers:
|
for ident_domain, ident_value in device.identifiers:
|
||||||
if ident_domain != DOMAIN:
|
if ident_domain != DOMAIN:
|
||||||
continue
|
continue
|
||||||
# Node devices are "node:<name>" — ignore those
|
|
||||||
if ident_value.startswith("node:"):
|
if ident_value.startswith("node:"):
|
||||||
continue
|
continue
|
||||||
return _parse_guest_identifier(ident_value)
|
return _parse_guest_identifier(ident_value)
|
||||||
|
|
||||||
raise ValueError(f"Selected device has no Easy Proxmox guest identifier: {device_id}")
|
raise ValueError(f"Selected device has no Easy Proxmox guest identifier: {device_id}")
|
||||||
|
|
||||||
# manual mode
|
|
||||||
if not node or vmid is None:
|
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 target OR node + vmid (+ optional type/host/config_entry_id).")
|
||||||
|
|
||||||
@@ -111,7 +117,6 @@ def _get_domain_entries(hass: HomeAssistant) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _pick_entry_id_for_device(hass: HomeAssistant, device_id: str) -> str:
|
def _pick_entry_id_for_device(hass: HomeAssistant, device_id: str) -> str:
|
||||||
"""Pick correct config_entry_id by using device.config_entries."""
|
|
||||||
dev_reg = dr.async_get(hass)
|
dev_reg = dr.async_get(hass)
|
||||||
device = dev_reg.async_get(device_id)
|
device = dev_reg.async_get(device_id)
|
||||||
if not device:
|
if not device:
|
||||||
@@ -144,7 +149,6 @@ def _pick_entry_id_by_host(hass: HomeAssistant, host: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _pick_entry_id_by_guest_lookup(hass: HomeAssistant, node: str, vmtype: str, vmid: int) -> str:
|
def _pick_entry_id_by_guest_lookup(hass: HomeAssistant, node: str, vmtype: str, vmid: int) -> str:
|
||||||
"""Find correct entry by scanning each entry's resources list."""
|
|
||||||
domain_entries = _get_domain_entries(hass)
|
domain_entries = _get_domain_entries(hass)
|
||||||
matches = []
|
matches = []
|
||||||
|
|
||||||
@@ -178,33 +182,27 @@ def _pick_entry_id_by_guest_lookup(hass: HomeAssistant, node: str, vmtype: str,
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, target: Tuple[str, str, int]) -> str:
|
def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, target: Tuple[str, str, int]) -> str:
|
||||||
"""Resolve which config entry should execute this service call."""
|
|
||||||
domain_entries = _get_domain_entries(hass)
|
domain_entries = _get_domain_entries(hass)
|
||||||
|
|
||||||
# 1) explicit config_entry_id
|
|
||||||
config_entry_id = call.data.get(ATTR_CONFIG_ENTRY_ID)
|
config_entry_id = call.data.get(ATTR_CONFIG_ENTRY_ID)
|
||||||
if config_entry_id:
|
if config_entry_id:
|
||||||
if config_entry_id not in domain_entries:
|
if config_entry_id not in domain_entries:
|
||||||
raise ValueError(f"config_entry_id '{config_entry_id}' not found or not loaded.")
|
raise ValueError(f"config_entry_id '{config_entry_id}' not found or not loaded.")
|
||||||
return config_entry_id
|
return config_entry_id
|
||||||
|
|
||||||
# 2) by device target (best + unambiguous)
|
|
||||||
device_id = _get_target_device_id(call)
|
device_id = _get_target_device_id(call)
|
||||||
if device_id:
|
if device_id:
|
||||||
return _pick_entry_id_for_device(hass, device_id)
|
return _pick_entry_id_for_device(hass, device_id)
|
||||||
|
|
||||||
# 3) by host
|
|
||||||
host = call.data.get(ATTR_HOST)
|
host = call.data.get(ATTR_HOST)
|
||||||
if host:
|
if host:
|
||||||
return _pick_entry_id_by_host(hass, host)
|
return _pick_entry_id_by_host(hass, host)
|
||||||
|
|
||||||
# 4) last resort: guest lookup
|
|
||||||
node, vmtype, vmid = target
|
node, vmtype, vmid = target
|
||||||
return _pick_entry_id_by_guest_lookup(hass, node, vmtype, vmid)
|
return _pick_entry_id_by_guest_lookup(hass, node, vmtype, vmid)
|
||||||
|
|
||||||
|
|
||||||
async def async_register_services(hass: HomeAssistant) -> None:
|
async def async_register_services(hass: HomeAssistant) -> None:
|
||||||
"""Register services once per HA instance."""
|
|
||||||
if hass.services.has_service(DOMAIN, SERVICE_START):
|
if hass.services.has_service(DOMAIN, SERVICE_START):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -218,7 +216,6 @@ async def async_register_services(hass: HomeAssistant) -> None:
|
|||||||
raise ValueError(f"Selected config entry '{entry_id}' has no client (not loaded).")
|
raise ValueError(f"Selected config entry '{entry_id}' has no client (not loaded).")
|
||||||
|
|
||||||
client = entry_data["client"]
|
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", action, entry_id, node, vmtype, vmid)
|
||||||
await client.guest_action(node=node, vmid=vmid, vmtype=vmtype, action=action)
|
await client.guest_action(node=node, vmid=vmid, vmtype=vmtype, action=action)
|
||||||
|
|
||||||
@@ -241,7 +238,6 @@ async def async_register_services(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def async_unregister_services(hass: HomeAssistant) -> None:
|
async def async_unregister_services(hass: HomeAssistant) -> None:
|
||||||
"""Unregister services (optional cleanup)."""
|
|
||||||
for svc in (SERVICE_START, SERVICE_SHUTDOWN, SERVICE_STOP_HARD, SERVICE_REBOOT):
|
for svc in (SERVICE_START, SERVICE_SHUTDOWN, SERVICE_STOP_HARD, SERVICE_REBOOT):
|
||||||
if hass.services.has_service(DOMAIN, svc):
|
if hass.services.has_service(DOMAIN, svc):
|
||||||
hass.services.async_remove(DOMAIN, svc)
|
hass.services.async_remove(DOMAIN, svc)
|
||||||
Reference in New Issue
Block a user