mirror of
https://github.com/bahmcloud/easy_proxmox.git
synced 2026-04-06 10:51:14 +00:00
Compare commits
7 Commits
v0.7.0
...
v0.7.2-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| fea7f3c4af | |||
| ceb5353ae9 | |||
| 64b95c0f17 | |||
| 24da6a9c31 | |||
| c7c12997ed | |||
| 4565bb3809 | |||
| 2b225ac15a |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,5 +1,20 @@
|
|||||||
# 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
|
||||||
|
- Fixed Home Assistant service UI integration:
|
||||||
|
- Services now properly expose the **Device selector** in the visual automation editor
|
||||||
|
- Implemented correct handling of `call.target.device_id`
|
||||||
|
- Services are now fully compatible with Home Assistant’s target system
|
||||||
|
- Fixed issue where only a YAML data field was shown instead of a device selection field
|
||||||
|
- Improved service robustness when used in multi-host environments
|
||||||
|
- This release is a bugfix release for v0.7.0
|
||||||
|
|
||||||
## 0.7.0
|
## 0.7.0
|
||||||
- Added full service support for automations and scripts:
|
- Added full service support for automations and scripts:
|
||||||
- `proxmox_pve.start`
|
- `proxmox_pve.start`
|
||||||
@@ -112,3 +127,4 @@
|
|||||||
- First experimental entities
|
- First experimental entities
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "proxmox_pve",
|
"domain": "proxmox_pve",
|
||||||
"name": "Easy Proxmox (by René Bachmann)",
|
"name": "Easy Proxmox (by René Bachmann)",
|
||||||
"version": "0.6.0-alpha",
|
"version": "0.7.1",
|
||||||
"documentation": "https://example.invalid",
|
"documentation": "https://example.invalid",
|
||||||
"requirements": ["aiohttp>=3.9.0"],
|
"requirements": ["aiohttp>=3.9.0"],
|
||||||
"codeowners": ["@BAHMCLOUD"],
|
"codeowners": ["@BAHMCLOUD"],
|
||||||
@@ -9,4 +9,3 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"integration_type": "service"
|
"integration_type": "service"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ ATTR_TYPE = "type"
|
|||||||
|
|
||||||
VALID_TYPES = ("qemu", "lxc")
|
VALID_TYPES = ("qemu", "lxc")
|
||||||
|
|
||||||
|
# HA may pass device_id as str or list[str] (especially when using UI targets).
|
||||||
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,
|
||||||
@@ -36,6 +37,25 @@ 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:
|
||||||
|
v0 = value[0]
|
||||||
|
if isinstance(v0, str) and v0.strip():
|
||||||
|
return v0.strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_device_id(call: ServiceCall) -> str | None:
|
||||||
|
"""
|
||||||
|
IMPORTANT: On some HA versions ServiceCall has no .target attribute.
|
||||||
|
UI targets are still provided, usually as data.device_id (often a list).
|
||||||
|
"""
|
||||||
|
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"
|
||||||
@@ -53,7 +73,7 @@ def _parse_guest_identifier(identifier: str) -> Tuple[str, str, int]:
|
|||||||
|
|
||||||
def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, int]:
|
def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, int]:
|
||||||
"""Resolve node/type/vmid from device_id OR node+vmid (+ optional type)."""
|
"""Resolve node/type/vmid from device_id OR node+vmid (+ optional type)."""
|
||||||
device_id = call.data.get(ATTR_DEVICE_ID)
|
device_id = _get_device_id(call)
|
||||||
node = call.data.get(ATTR_NODE)
|
node = call.data.get(ATTR_NODE)
|
||||||
vmid = call.data.get(ATTR_VMID)
|
vmid = call.data.get(ATTR_VMID)
|
||||||
vmtype = call.data.get(ATTR_TYPE, "qemu")
|
vmtype = call.data.get(ATTR_TYPE, "qemu")
|
||||||
@@ -77,7 +97,7 @@ def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, i
|
|||||||
|
|
||||||
# manual mode
|
# manual mode
|
||||||
if not node or vmid is None:
|
if not node or vmid is None:
|
||||||
raise ValueError("Provide either device_id OR node + vmid (+ optional type/host/config_entry_id).")
|
raise ValueError("Provide device_id OR node + vmid (+ optional type/host/config_entry_id).")
|
||||||
|
|
||||||
if vmtype not in VALID_TYPES:
|
if vmtype not in VALID_TYPES:
|
||||||
raise ValueError(f"Invalid type: {vmtype} (allowed: {VALID_TYPES})")
|
raise ValueError(f"Invalid type: {vmtype} (allowed: {VALID_TYPES})")
|
||||||
@@ -104,7 +124,6 @@ def _pick_entry_id_for_device(hass: HomeAssistant, device_id: str) -> str:
|
|||||||
if not candidates:
|
if not candidates:
|
||||||
raise ValueError("Device is not linked to any loaded Easy Proxmox config entry.")
|
raise ValueError("Device is not linked to any loaded Easy Proxmox config entry.")
|
||||||
if len(candidates) > 1:
|
if len(candidates) > 1:
|
||||||
# Very unlikely, but handle it
|
|
||||||
_LOGGER.warning("Device %s belongs to multiple Easy Proxmox entries, using first.", device_id)
|
_LOGGER.warning("Device %s belongs to multiple Easy Proxmox entries, using first.", device_id)
|
||||||
return candidates[0]
|
return candidates[0]
|
||||||
|
|
||||||
@@ -115,7 +134,6 @@ def _pick_entry_id_by_host(hass: HomeAssistant, host: str) -> str:
|
|||||||
for entry_id, data in domain_entries.items():
|
for entry_id, data in domain_entries.items():
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
continue
|
continue
|
||||||
# host is stored in entry.data, but we keep it accessible here via "client.host" too
|
|
||||||
client = data.get("client")
|
client = data.get("client")
|
||||||
if client and getattr(client, "host", None) == host:
|
if client and getattr(client, "host", None) == host:
|
||||||
matches.append(entry_id)
|
matches.append(entry_id)
|
||||||
@@ -128,10 +146,7 @@ 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."""
|
||||||
If user provides only node/vmid/type, try to find the correct entry by
|
|
||||||
scanning each entry's cluster resources list.
|
|
||||||
"""
|
|
||||||
domain_entries = _get_domain_entries(hass)
|
domain_entries = _get_domain_entries(hass)
|
||||||
matches = []
|
matches = []
|
||||||
|
|
||||||
@@ -154,7 +169,7 @@ def _pick_entry_id_by_guest_lookup(hass: HomeAssistant, node: str, vmtype: str,
|
|||||||
if not matches:
|
if not matches:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Could not find guest {node}/{vmtype}/{vmid} in any configured Proxmox host. "
|
f"Could not find guest {node}/{vmtype}/{vmid} in any configured Proxmox host. "
|
||||||
"Provide host or config_entry_id."
|
"Provide host or config_entry_id, or use device_id."
|
||||||
)
|
)
|
||||||
if len(matches) > 1:
|
if len(matches) > 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -168,24 +183,20 @@ def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, target: Tuple[str,
|
|||||||
"""Resolve which config entry should execute this service call."""
|
"""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_id (best + unambiguous)
|
device_id = _get_device_id(call)
|
||||||
device_id = call.data.get(ATTR_DEVICE_ID)
|
|
||||||
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 in resources list
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -205,30 +216,18 @@ 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)
|
||||||
|
|
||||||
async def handle_start(call: ServiceCall) -> None:
|
hass.services.async_register(DOMAIN, SERVICE_START, lambda call: _call_action(call, "start"), schema=SERVICE_SCHEMA)
|
||||||
await _call_action(call, "start")
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_SHUTDOWN, lambda call: _call_action(call, "shutdown"), schema=SERVICE_SCHEMA
|
||||||
async def handle_shutdown(call: ServiceCall) -> None:
|
)
|
||||||
await _call_action(call, "shutdown")
|
hass.services.async_register(DOMAIN, SERVICE_STOP_HARD, lambda call: _call_action(call, "stop"), schema=SERVICE_SCHEMA)
|
||||||
|
hass.services.async_register(DOMAIN, SERVICE_REBOOT, lambda call: _call_action(call, "reboot"), schema=SERVICE_SCHEMA)
|
||||||
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:
|
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)
|
||||||
@@ -1,99 +1,111 @@
|
|||||||
start:
|
start:
|
||||||
name: Start guest
|
name: Start guest
|
||||||
description: Start a VM or container.
|
description: Start a VM or container.
|
||||||
|
target:
|
||||||
|
device:
|
||||||
|
integration: proxmox_pve
|
||||||
fields:
|
fields:
|
||||||
device_id:
|
|
||||||
name: Device
|
|
||||||
description: Select the VM/CT device (recommended, works with multi-host).
|
|
||||||
example: "a1b2c3d4e5f6..."
|
|
||||||
config_entry_id:
|
config_entry_id:
|
||||||
name: Config entry id
|
name: Config entry id
|
||||||
description: Optional. Force a specific Proxmox host configuration entry.
|
description: Optional. Force a specific Proxmox host configuration entry.
|
||||||
|
example: "8d9f2e7b1c3d4a5f..."
|
||||||
host:
|
host:
|
||||||
name: Host
|
name: Host
|
||||||
description: Optional. Proxmox host/IP of the configured entry (only used when device_id is not provided).
|
description: Optional. Proxmox host/IP of the configured entry (only used when device is not selected).
|
||||||
example: "192.168.178.101"
|
example: "192.168.178.101"
|
||||||
node:
|
node:
|
||||||
name: Node
|
name: Node
|
||||||
description: Proxmox node name (only used when device_id is not provided).
|
description: Proxmox node name (only used when device is not selected).
|
||||||
example: "pve1"
|
example: "pve1"
|
||||||
vmid:
|
vmid:
|
||||||
name: VMID
|
name: VMID
|
||||||
description: Guest VMID (only used when device_id is not provided).
|
description: Guest VMID (only used when device is not selected).
|
||||||
example: 100
|
example: 100
|
||||||
type:
|
type:
|
||||||
name: Type
|
name: Type
|
||||||
description: Guest type.
|
description: Guest type (only used when device is not selected).
|
||||||
example: "qemu"
|
example: "qemu"
|
||||||
|
|
||||||
shutdown:
|
shutdown:
|
||||||
name: Shutdown guest
|
name: Shutdown guest
|
||||||
description: Soft shutdown a VM or container.
|
description: Gracefully shutdown a VM or container.
|
||||||
|
target:
|
||||||
|
device:
|
||||||
|
integration: proxmox_pve
|
||||||
fields:
|
fields:
|
||||||
device_id:
|
|
||||||
name: Device
|
|
||||||
description: Select the VM/CT device (recommended, works with multi-host).
|
|
||||||
config_entry_id:
|
config_entry_id:
|
||||||
name: Config entry id
|
name: Config entry id
|
||||||
description: Optional. Force a specific Proxmox host configuration entry.
|
description: Optional. Force a specific Proxmox host configuration entry.
|
||||||
|
example: "8d9f2e7b1c3d4a5f..."
|
||||||
host:
|
host:
|
||||||
name: Host
|
name: Host
|
||||||
description: Optional. Proxmox host/IP of the configured entry (only used when device_id is not provided).
|
description: Optional. Proxmox host/IP of the configured entry (only used when device is not selected).
|
||||||
|
example: "192.168.178.101"
|
||||||
node:
|
node:
|
||||||
name: Node
|
name: Node
|
||||||
description: Proxmox node name (only used when device_id is not provided).
|
description: Proxmox node name (only used when device is not selected).
|
||||||
|
example: "pve1"
|
||||||
vmid:
|
vmid:
|
||||||
name: VMID
|
name: VMID
|
||||||
description: Guest VMID (only used when device_id is not provided).
|
description: Guest VMID (only used when device is not selected).
|
||||||
|
example: 100
|
||||||
type:
|
type:
|
||||||
name: Type
|
name: Type
|
||||||
description: Guest type.
|
description: Guest type (only used when device is not selected).
|
||||||
example: "qemu"
|
example: "qemu"
|
||||||
|
|
||||||
stop_hard:
|
stop_hard:
|
||||||
name: Stop guest (hard)
|
name: Stop guest (hard)
|
||||||
description: Hard stop a VM or container (equivalent to Stop).
|
description: Hard stop a VM or container (equivalent to Stop).
|
||||||
|
target:
|
||||||
|
device:
|
||||||
|
integration: proxmox_pve
|
||||||
fields:
|
fields:
|
||||||
device_id:
|
|
||||||
name: Device
|
|
||||||
description: Select the VM/CT device (recommended, works with multi-host).
|
|
||||||
config_entry_id:
|
config_entry_id:
|
||||||
name: Config entry id
|
name: Config entry id
|
||||||
description: Optional. Force a specific Proxmox host configuration entry.
|
description: Optional. Force a specific Proxmox host configuration entry.
|
||||||
|
example: "8d9f2e7b1c3d4a5f..."
|
||||||
host:
|
host:
|
||||||
name: Host
|
name: Host
|
||||||
description: Optional. Proxmox host/IP of the configured entry (only used when device_id is not provided).
|
description: Optional. Proxmox host/IP of the configured entry (only used when device is not selected).
|
||||||
|
example: "192.168.178.101"
|
||||||
node:
|
node:
|
||||||
name: Node
|
name: Node
|
||||||
description: Proxmox node name (only used when device_id is not provided).
|
description: Proxmox node name (only used when device is not selected).
|
||||||
|
example: "pve1"
|
||||||
vmid:
|
vmid:
|
||||||
name: VMID
|
name: VMID
|
||||||
description: Guest VMID (only used when device_id is not provided).
|
description: Guest VMID (only used when device is not selected).
|
||||||
|
example: 100
|
||||||
type:
|
type:
|
||||||
name: Type
|
name: Type
|
||||||
description: Guest type.
|
description: Guest type (only used when device is not selected).
|
||||||
example: "qemu"
|
example: "qemu"
|
||||||
|
|
||||||
reboot:
|
reboot:
|
||||||
name: Reboot guest
|
name: Reboot guest
|
||||||
description: Reboot a VM or container.
|
description: Reboot a VM or container.
|
||||||
|
target:
|
||||||
|
device:
|
||||||
|
integration: proxmox_pve
|
||||||
fields:
|
fields:
|
||||||
device_id:
|
|
||||||
name: Device
|
|
||||||
description: Select the VM/CT device (recommended, works with multi-host).
|
|
||||||
config_entry_id:
|
config_entry_id:
|
||||||
name: Config entry id
|
name: Config entry id
|
||||||
description: Optional. Force a specific Proxmox host configuration entry.
|
description: Optional. Force a specific Proxmox host configuration entry.
|
||||||
|
example: "8d9f2e7b1c3d4a5f..."
|
||||||
host:
|
host:
|
||||||
name: Host
|
name: Host
|
||||||
description: Optional. Proxmox host/IP of the configured entry (only used when device_id is not provided).
|
description: Optional. Proxmox host/IP of the configured entry (only used when device is not selected).
|
||||||
|
example: "192.168.178.101"
|
||||||
node:
|
node:
|
||||||
name: Node
|
name: Node
|
||||||
description: Proxmox node name (only used when device_id is not provided).
|
description: Proxmox node name (only used when device is not selected).
|
||||||
|
example: "pve1"
|
||||||
vmid:
|
vmid:
|
||||||
name: VMID
|
name: VMID
|
||||||
description: Guest VMID (only used when device_id is not provided).
|
description: Guest VMID (only used when device is not selected).
|
||||||
|
example: 100
|
||||||
type:
|
type:
|
||||||
name: Type
|
name: Type
|
||||||
description: Guest type.
|
description: Guest type (only used when device is not selected).
|
||||||
example: "qemu"
|
example: "qemu"
|
||||||
|
|||||||
Reference in New Issue
Block a user