13 Commits

Author SHA1 Message Date
f0e17bf90a Update version to 0.7.4 in manifest.json 2026-01-14 07:40:38 +01:00
c374740503 Add diagnostics export instructions to README
Added a diagnostics section to README for issue reporting.
2026-01-14 07:39:23 +01:00
d4cbf1d303 Add Home Assistant Diagnostics support
Added support for Home Assistant Diagnostics, including a new feature to download diagnostics for each Easy Proxmox config entry. Diagnostics include sanitized config entry data, runtime client information, and more, with sensitive data redacted.
2026-01-14 07:38:27 +01:00
39fc090944 Change to v0.7.3 2026-01-13 22:42:06 +01:00
8f8ac50905 Change version. Add Dokumentation Link to Readme. 2026-01-13 22:40:12 +01:00
66b0db4a6d Add 0.7.3 2026-01-13 22:38:40 +01:00
fea7f3c4af Aktualisieren von services.py 2026-01-13 22:16:53 +01:00
ceb5353ae9 Aktualisieren von CHANGELOG.md 2026-01-13 22:00:01 +01:00
64b95c0f17 Aktualisieren von services.py 2026-01-13 21:58:27 +01:00
24da6a9c31 Fix service UI integration and enhance robustness
Fixed Home Assistant service UI integration and improved service robustness in multi-host environments. This release is a bugfix release for v0.7.0.
2026-01-13 18:28:06 +01:00
c7c12997ed Refactor device ID handling and error messages
Refactor device ID retrieval to support UI target selection and improve error messages.
2026-01-13 18:26:45 +01:00
4565bb3809 Enhance service definitions for VM operations
Updated descriptions and added target device integration for VM operations.
2026-01-13 18:26:21 +01:00
2b225ac15a Update manifest version to 0.7.1 2026-01-13 18:26:00 +01:00
5 changed files with 162 additions and 68 deletions

View File

@@ -1,5 +1,41 @@
# Changelog
## 0.7.4
- Added Home Assistant Diagnostics support
- New “Download diagnostics” feature for each Easy Proxmox config entry
- Diagnostics include:
- Config entry data and options (sanitized)
- Runtime client information
- Coordinator states (last update success, exceptions, update interval)
- Safe previews of nodes and guests
- Sensitive data such as API tokens and credentials are automatically redacted
- Diagnostics are fully JSON-serializable and suitable for GitHub issue attachments
## 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)
- 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 Assistants 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
- Added full service support for automations and scripts:
- `proxmox_pve.start`
@@ -112,3 +148,5 @@
- First experimental entities

View File

@@ -329,6 +329,14 @@ The API token has admin rights. Treat it like a root password:
| Buttons dont work | Check Proxmox permissions (PVEAdmin role) |
| Old devices remain | Fully cleaned up automatically since version 0.4.1 |
### Diagnostics
If you open an issue on GitHub, please attach a diagnostics export:
Settings → Devices & Services → Easy Proxmox → (⋮) → Download diagnostics
This export is automatically sanitized (API token is redacted).
---
## Support & Contributing

View File

@@ -1,8 +1,8 @@
{
"domain": "proxmox_pve",
"name": "Easy Proxmox (by René Bachmann)",
"version": "0.6.0-alpha",
"documentation": "https://example.invalid",
"version": "0.7.4",
"documentation": "https://github.com/bahmcloud/easy_proxmox ",
"requirements": ["aiohttp>=3.9.0"],
"codeowners": ["@BAHMCLOUD"],
"config_flow": true,

View File

@@ -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,9 +26,11 @@ ATTR_TYPE = "type"
VALID_TYPES = ("qemu", "lxc")
# Accept device_id passed as str or list[str] (depends on HA UI/script)
SERVICE_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_DEVICE_ID): str,
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,
@@ -36,11 +40,64 @@ SERVICE_SCHEMA = vol.Schema(
)
def _first_str(value: Any) -> str | 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 _first_str_from_target(target: Any, key: str) -> str | None:
if isinstance(target, dict):
return _first_str(target.get(key))
return None
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}")
@@ -52,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_id OR node+vmid (+ optional type)."""
device_id = call.data.get(ATTR_DEVICE_ID)
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")
@@ -64,20 +120,17 @@ def _resolve_target(hass: HomeAssistant, call: ServiceCall) -> Tuple[str, str, i
if not device:
raise ValueError(f"Device not found: {device_id}")
# Find our guest identifier in device.identifiers
for ident_domain, ident_value in device.identifiers:
if ident_domain != DOMAIN:
continue
# Node devices are "node:<name>" — ignore those
if ident_value.startswith("node:"):
continue
return _parse_guest_identifier(ident_value)
raise ValueError(f"Selected device has no Easy Proxmox guest identifier: {device_id}")
# manual mode
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 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})")
@@ -93,7 +146,6 @@ def _get_domain_entries(hass: HomeAssistant) -> dict[str, Any]:
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)
device = dev_reg.async_get(device_id)
if not device:
@@ -104,7 +156,6 @@ def _pick_entry_id_for_device(hass: HomeAssistant, device_id: str) -> str:
if not candidates:
raise ValueError("Device is not linked to any loaded Easy Proxmox config entry.")
if len(candidates) > 1:
# Very unlikely, but handle it
_LOGGER.warning("Device %s belongs to multiple Easy Proxmox entries, using first.", device_id)
return candidates[0]
@@ -115,7 +166,6 @@ def _pick_entry_id_by_host(hass: HomeAssistant, host: str) -> str:
for entry_id, data in domain_entries.items():
if not isinstance(data, dict):
continue
# host is stored in entry.data, but we keep it accessible here via "client.host" too
client = data.get("client")
if client and getattr(client, "host", None) == host:
matches.append(entry_id)
@@ -128,10 +178,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:
"""
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)
matches = []
@@ -154,59 +200,50 @@ 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."
"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_id."
"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:
"""Resolve which config entry should execute this service call."""
def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall, node: str, vmtype: str, vmid: int) -> str:
domain_entries = _get_domain_entries(hass)
# 1) explicit config_entry_id
config_entry_id = call.data.get(ATTR_CONFIG_ENTRY_ID)
if config_entry_id:
if config_entry_id not in domain_entries:
raise ValueError(f"config_entry_id '{config_entry_id}' not found or not loaded.")
return config_entry_id
# 2) by device_id (best + unambiguous)
device_id = call.data.get(ATTR_DEVICE_ID)
device_id = _get_device_id(hass, call)
if device_id:
return _pick_entry_id_for_device(hass, device_id)
# 3) by host
host = call.data.get(ATTR_HOST)
if host:
return _pick_entry_id_by_host(hass, host)
# 4) last resort: guest lookup in resources list
node, vmtype, vmid = target
return _pick_entry_id_by_guest_lookup(hass, node, vmtype, vmid)
async def async_register_services(hass: HomeAssistant) -> None:
"""Register services once per HA instance."""
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)
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:
@@ -228,7 +265,6 @@ async def async_register_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):
if hass.services.has_service(DOMAIN, svc):
hass.services.async_remove(DOMAIN, svc)
hass.services.async_remove(DOMAIN, svc)

View File

@@ -1,99 +1,111 @@
start:
name: Start guest
description: Start a VM or container.
target:
device:
integration: proxmox_pve
fields:
device_id:
name: Device
description: Select the VM/CT device (recommended, works with multi-host).
example: "a1b2c3d4e5f6..."
config_entry_id:
name: Config entry id
description: Optional. Force a specific Proxmox host configuration entry.
example: "8d9f2e7b1c3d4a5f..."
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:
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:
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:
name: Type
description: Guest type.
description: Guest type (only used when device is not selected).
example: "qemu"
shutdown:
name: Shutdown guest
description: Soft shutdown a VM or container.
description: Gracefully shutdown a VM or container.
target:
device:
integration: proxmox_pve
fields:
device_id:
name: Device
description: Select the VM/CT device (recommended, works with multi-host).
config_entry_id:
name: Config entry id
description: Optional. Force a specific Proxmox host configuration entry.
example: "8d9f2e7b1c3d4a5f..."
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:
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:
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:
name: Type
description: Guest type.
description: Guest type (only used when device is not selected).
example: "qemu"
stop_hard:
name: Stop guest (hard)
description: Hard stop a VM or container (equivalent to Stop).
target:
device:
integration: proxmox_pve
fields:
device_id:
name: Device
description: Select the VM/CT device (recommended, works with multi-host).
config_entry_id:
name: Config entry id
description: Optional. Force a specific Proxmox host configuration entry.
example: "8d9f2e7b1c3d4a5f..."
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:
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:
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:
name: Type
description: Guest type.
description: Guest type (only used when device is not selected).
example: "qemu"
reboot:
name: Reboot guest
description: Reboot a VM or container.
target:
device:
integration: proxmox_pve
fields:
device_id:
name: Device
description: Select the VM/CT device (recommended, works with multi-host).
config_entry_id:
name: Config entry id
description: Optional. Force a specific Proxmox host configuration entry.
example: "8d9f2e7b1c3d4a5f..."
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:
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:
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:
name: Type
description: Guest type.
description: Guest type (only used when device is not selected).
example: "qemu"