13 Commits

Author SHA1 Message Date
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
eec020f0e6 Update CHANGELOG for version 0.7.0
Added new version 0.7.0 with full service support for automations and scripts, including multi-host capabilities and improved error handling.
2026-01-13 18:04:27 +01:00
44b0779797 Document services and automations features
Added section for services and automations in Easy Proxmox.
2026-01-13 18:03:54 +01:00
8b8a651979 Add config_entry_id and host attributes to service schema 2026-01-13 17:46:08 +01:00
084b9d28bb Register and unregister services in Proxmox PVE
Added service registration and unregistration for Proxmox PVE integration.
2026-01-13 17:45:33 +01:00
7b09ed70f4 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.
2026-01-13 17:45:01 +01:00
68b4ef5534 Add Proxmox service definitions for VM management
Added service definitions for managing Proxmox VMs and containers including start, shutdown, hard stop, and reboot actions.
2026-01-13 17:43:51 +01:00
a5807e1e7a Add HACS badge to README
Added HACS badge to README for easier integration.
2026-01-13 17:12:25 +01:00
f63e548b8b Update README.md 2026-01-13 17:08:40 +01:00
ef47ce134a Enhance installation instructions in README
Updated installation instructions for Easy Proxmox integration via HACS, including repository details and steps.
2026-01-13 17:02:09 +01:00
6 changed files with 543 additions and 35 deletions

View File

@@ -1,5 +1,33 @@
# Changelog
## 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`
- `proxmox_pve.shutdown`
- `proxmox_pve.stop_hard`
- `proxmox_pve.reboot`
- Services are now fully multi-host capable:
- Automatic detection of the correct Proxmox host when using `device_id`
- Optional selection via `config_entry_id`
- Optional selection via `host`
- Automatic lookup by `node/vmid/type` if no host is given
- Clear error handling for ambiguous multi-host targets
- Services can be used in:
- Automations
- Scripts
- Dashboards
- Added `services.yaml` for proper UI descriptions in Home Assistant
- Easy Proxmox can now be fully controlled without any buttons or switches, purely via automations
## 0.6.1
- Fix correct autor in hacs
@@ -92,3 +120,5 @@
- Basic connectivity test via Config Flow
- First experimental entities

160
README.md
View File

@@ -1,3 +1,11 @@
# Easy Proxmox (by René Bachmann)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bahmcloud/easy_proxmox?sort=semver)
![HACS Integration](https://img.shields.io/badge/HACS%20Integration-blue)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
[![Add to HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=bahmcloud&repository=easy_proxmox)
<img width="1230" height="300" alt="logo" src="https://github.com/user-attachments/assets/7649cc04-bdcd-449e-bf83-c4f980f3de74" />
A powerful Home Assistant integration to monitor and control Proxmox VE. With Easy Proxmox you can monitor nodes, VMs and containers directly in Home Assistant, start/stop/reboot guests and display detailed system metrics.
@@ -35,20 +43,30 @@ A powerful Home Assistant integration to monitor and control Proxmox VE. With Ea
---
## Installation
## 📦 Installation via HACS (Recommended)
1. Create the folder:
/config/custom_components/proxmox_pve/
To install Easy Proxmox through HACS:
1. **Ensure HACS is already installed** in your Home Assistant instance.
If not, follow the official guide: https://hacs.xyz/
2. Copy all integration files into that folder.
2. Open **HACS → Integrations**.
3. Restart Home Assistant.
3. Click the **“⋯” (three dots)** → **Custom repositories**.
4. Add the integration:
Settings → Devices & Services → Add Integration → Easy Proxmox
4. Add the following repository:
- Repository URL:
```
https://github.com/bahmcloud/easy_proxmox
```
- Category: **Integration**
- Version: **Tags (recommended)**
5. Click **Add**, then locate **Easy Proxmox** in the HACS list and click **Install**.
6. After installation, **restart Home Assistant**.
7. Go to **Settings → Devices & Services → Add Integration → Easy Proxmox**.
---
## Proxmox: Create User & API Token
@@ -64,8 +82,6 @@ Example:
- Realm: `pam` or `pve`
- Set a password (only for management; the token will be used for API access)
---
### 2) Create an API Token
Datacenter → Permissions → API Tokens → Add
@@ -81,9 +97,6 @@ You will get:
Token Name: homeassistant@pve!easyproxmox
Token Secret: <long secret string>
---
### 3) Assign Permissions (Admin Rights)
To ensure full functionality (monitoring + guest controls), assign admin rights:
@@ -121,11 +134,116 @@ After saving:
---
## 🚀 Services & Automations (since v0.7.0)
Easy Proxmox provides Home Assistant services so you can fully control your VMs and containers in automations and scripts without using buttons or switches.
Available services:
| Service | Description |
|--------|------------|
| `proxmox_pve.start` | Start a VM or container |
| `proxmox_pve.shutdown` | Gracefully shutdown a VM or container |
| `proxmox_pve.stop_hard` | Hard stop a VM or container |
| `proxmox_pve.reboot` | Reboot a VM or container |
All services are **multi-host aware** and automatically select the correct Proxmox server.
### ✅ Recommended usage: Device based
This is the safest and easiest way, especially for multi-host setups.
In an automation or script:
```yaml
service: proxmox_pve.shutdown
target:
device_id: YOUR_DEVICE_ID
```
In the UI, you can simply select the VM/CT device from the dropdown.
Home Assistant will automatically:
- Find the correct Proxmox host
- Find the correct node
- Execute the action
### 🔧 Manual usage: Node / VMID based
You can also call services manually:
```yaml
service: proxmox_pve.reboot
data:
node: pve1
vmid: 100
type: qemu
```
If you have multiple Proxmox servers configured, you should also specify one of:
```yaml
host: 192.168.178.101
```
or
```yaml
config_entry_id: 8d9f2e7b1c3d4a5f...
```
This avoids ambiguity.
### 🧠 Resolution priority
When a service is called, Easy Proxmox resolves the target in this order:
1. `config_entry_id`
2. `device_id`
3. `host`
4. Guest lookup by `node + vmid + type`
If a guest exists on multiple Proxmox hosts, the call fails and asks for clarification.
### 🏗 Example automations
Shutdown all test systems at night:
```yaml
alias: Stop Test VM at Night
trigger:
- platform: time
at: "23:00:00"
action:
- service: proxmox_pve.shutdown
target:
device_id: 123456abcdef...
```
Start a VM when electricity price is low:
```yaml
alias: Start VM on cheap power
trigger:
- platform: numeric_state
entity_id: sensor.power_price
below: 0.20
action:
- service: proxmox_pve.start
target:
device_id: 123456abcdef...
```
### 🧩 Why this matters
With services you can:
- Fully automate your Proxmox infrastructure
- Remove dependency on dashboard buttons
- Build power-saving or maintenance automations
- Control multiple Proxmox clusters cleanly
---
## Options (Gear Icon)
After setup, open:
Settings → Devices & Services → Easy Proxmox → Options (gear icon)
### Polling Interval
How often data is fetched from Proxmox.
@@ -138,8 +256,6 @@ How often data is fetched from Proxmox.
Changes are applied immediately (no restart required).
---
### IP Preference Mode
Controls which IP is shown as the “primary” IP for a guest:
@@ -151,8 +267,6 @@ Controls which IP is shown as the “primary” IP for a guest:
| any | Use the first available IP |
| custom_prefix | Use a custom prefix |
---
### Custom IP Prefix
Only relevant if `custom_prefix` is selected.
@@ -217,10 +331,16 @@ The API token has admin rights. Treat it like a root password:
---
## License / Support
## Support & Contributing
Look into LICENSE file.
If you need help, open an issue in GitHub.
Want to contribute? Feel free to submit PRs!
---
**Easy Proxmox aims to provide a complete Proxmox VE experience in Home Assistant.**
See [CHANGELOG.md](CHANGELOG.md) for full version history.
Releases: https://github.com/bahmcloud/easy_proxmox/releases

View File

@@ -17,6 +17,7 @@ from .const import (
DEFAULT_IP_PREFIX,
)
from .coordinator import ProxmoxResourcesCoordinator, ProxmoxNodesCoordinator
from .services import async_register_services, async_unregister_services
_LOGGER = logging.getLogger(__name__)
@@ -33,14 +34,12 @@ async def _apply_options_now(hass: HomeAssistant, entry: ConfigEntry) -> None:
new_ip_mode = str(_opt(entry, CONF_IP_MODE, DEFAULT_IP_MODE))
new_ip_prefix = str(_opt(entry, CONF_IP_PREFIX, DEFAULT_IP_PREFIX))
# store new values for future coordinator creations
data["scan_interval"] = new_scan_interval
data["ip_mode"] = new_ip_mode
data["ip_prefix"] = new_ip_prefix
td = timedelta(seconds=new_scan_interval)
# cluster coordinators
resources = data.get("resources")
if resources:
resources.update_interval = td
@@ -49,11 +48,9 @@ async def _apply_options_now(hass: HomeAssistant, entry: ConfigEntry) -> None:
if nodes:
nodes.update_interval = td
# node coordinators
for node_coord in (data.get("node_coordinators") or {}).values():
node_coord.update_interval = td
# guest coordinators (also update ip preference config)
for guest_coord in (data.get("guest_coordinators") or {}).values():
guest_coord.update_interval = td
guest_coord.ip_mode = new_ip_mode
@@ -72,10 +69,8 @@ async def _apply_options_now(hass: HomeAssistant, entry: ConfigEntry) -> None:
for guest_coord in (data.get("guest_coordinators") or {}).values():
tasks.append(guest_coord.async_request_refresh())
if tasks:
# don't fail all if one refresh fails
for t in tasks:
hass.async_create_task(t)
for t in tasks:
hass.async_create_task(t)
_LOGGER.debug(
"Applied options live for %s: scan_interval=%s ip_mode=%s ip_prefix=%s",
@@ -87,7 +82,6 @@ async def _apply_options_now(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Called by HA when options are changed."""
await _apply_options_now(hass, entry)
@@ -122,12 +116,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"scan_interval": scan_interval,
"ip_mode": ip_mode,
"ip_prefix": ip_prefix,
"guest_coordinators": {}, # (node, vmtype, vmid) -> ProxmoxGuestCoordinator
"node_coordinators": {}, # node -> ProxmoxNodeCoordinator
"platform_cache": {}, # per-platform caches + unsub handles
"guest_coordinators": {},
"node_coordinators": {},
"platform_cache": {},
}
# Apply option updates live (gear icon -> save)
# Register services once
await async_register_services(hass)
# Apply option updates live
entry.async_on_unload(entry.add_update_listener(_update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -152,4 +149,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if data.get("session"):
await data["session"].close()
# If no entries remain, unregister services
if not hass.data.get(DOMAIN):
await async_unregister_services(hass)
return unload_ok

View File

@@ -1,7 +1,7 @@
{
"domain": "proxmox_pve",
"name": "Easy Proxmox (by René Bachmann)",
"version": "0.6.0-alpha",
"version": "0.7.1",
"documentation": "https://example.invalid",
"requirements": ["aiohttp>=3.9.0"],
"codeowners": ["@BAHMCLOUD"],
@@ -9,4 +9,3 @@
"iot_class": "local_polling",
"integration_type": "service"
}

View File

@@ -0,0 +1,247 @@
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" # fallback if user manually puts it into data
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_HOST = "host"
ATTR_NODE = "node"
ATTR_VMID = "vmid"
ATTR_TYPE = "type"
VALID_TYPES = ("qemu", "lxc")
# NOTE: device selection in UI goes via call.target, not via call.data.
SERVICE_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_DEVICE_ID): str,
vol.Optional(ATTR_CONFIG_ENTRY_ID): str,
vol.Optional(ATTR_HOST): str,
vol.Optional(ATTR_NODE): str,
vol.Optional(ATTR_VMID): vol.Coerce(int),
vol.Optional(ATTR_TYPE, default="qemu"): vol.In(VALID_TYPES),
}
)
def _get_target_device_id(call: ServiceCall) -> str | None:
"""Return single device_id selected in UI (call.target) or fallback call.data."""
# UI target: {"device_id": ["..."]}
if call.target and isinstance(call.target, dict):
dev_ids = call.target.get("device_id")
if isinstance(dev_ids, list) and dev_ids:
return dev_ids[0]
if isinstance(dev_ids, str):
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
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}")
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 device target OR node+vmid (+ optional type)."""
device_id = _get_target_device_id(call)
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 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 a Device 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})")
return str(node), str(vmtype), int(vmid)
def _get_domain_entries(hass: HomeAssistant) -> dict[str, Any]:
domain_data: dict[str, Any] = hass.data.get(DOMAIN, {})
if not domain_data:
raise ValueError("Easy Proxmox is not set up.")
return domain_data
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:
raise ValueError(f"Device not found: {device_id}")
domain_entries = _get_domain_entries(hass)
candidates = [eid for eid in device.config_entries if eid in domain_entries]
if not candidates:
raise ValueError("Device is not linked to any loaded Easy Proxmox config entry.")
if len(candidates) > 1:
_LOGGER.warning("Device %s belongs to multiple Easy Proxmox entries, using first.", device_id)
return candidates[0]
def _pick_entry_id_by_host(hass: HomeAssistant, host: str) -> str:
domain_entries = _get_domain_entries(hass)
matches = []
for entry_id, data in domain_entries.items():
if not isinstance(data, dict):
continue
client = data.get("client")
if client and getattr(client, "host", None) == host:
matches.append(entry_id)
if not matches:
raise ValueError(f"No Easy Proxmox entry found for host '{host}'.")
if len(matches) > 1:
raise ValueError(f"Multiple Easy Proxmox entries found for host '{host}'. Please use config_entry_id.")
return matches[0]
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)
matches = []
for entry_id, data in domain_entries.items():
if not isinstance(data, dict):
continue
resources = data.get("resources")
res_list = getattr(resources, "data", None)
if not res_list:
continue
for r in res_list:
try:
if r.get("type") == vmtype and str(r.get("node")) == node and int(r.get("vmid")) == vmid:
matches.append(entry_id)
break
except Exception:
continue
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."
)
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."
)
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."""
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 target (best + unambiguous)
device_id = _get_target_device_id(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
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))
domain_entries = _get_domain_entries(hass)
entry_data = domain_entries.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)
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 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)

View File

@@ -0,0 +1,111 @@
start:
name: Start guest
description: Start a VM or container.
target:
device:
integration: proxmox_pve
fields:
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 is not selected).
example: "192.168.178.101"
node:
name: Node
description: Proxmox node name (only used when device is not selected).
example: "pve1"
vmid:
name: VMID
description: Guest VMID (only used when device is not selected).
example: 100
type:
name: Type
description: Guest type (only used when device is not selected).
example: "qemu"
shutdown:
name: Shutdown guest
description: Gracefully shutdown a VM or container.
target:
device:
integration: proxmox_pve
fields:
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 is not selected).
example: "192.168.178.101"
node:
name: Node
description: Proxmox node name (only used when device is not selected).
example: "pve1"
vmid:
name: VMID
description: Guest VMID (only used when device is not selected).
example: 100
type:
name: 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:
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 is not selected).
example: "192.168.178.101"
node:
name: Node
description: Proxmox node name (only used when device is not selected).
example: "pve1"
vmid:
name: VMID
description: Guest VMID (only used when device is not selected).
example: 100
type:
name: 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:
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 is not selected).
example: "192.168.178.101"
node:
name: Node
description: Proxmox node name (only used when device is not selected).
example: "pve1"
vmid:
name: VMID
description: Guest VMID (only used when device is not selected).
example: 100
type:
name: Type
description: Guest type (only used when device is not selected).
example: "qemu"