Create DataUpdateCoordinator for each proxmoxve vm/container (#45171)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Corbeno 2021-04-11 22:14:11 -05:00 committed by GitHub
parent f538ea1827
commit eac1041277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 110 additions and 86 deletions

View File

@ -367,7 +367,7 @@ homeassistant/components/powerwall/* @bdraco @jrester
homeassistant/components/profiler/* @bdraco
homeassistant/components/progettihwsw/* @ardaseremet
homeassistant/components/prometheus/* @knyar
homeassistant/components/proxmoxve/* @k4ds3 @jhollowe
homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @Corbeno
homeassistant/components/ps4/* @ktnrg45
homeassistant/components/push/* @dgomes
homeassistant/components/pvoutput/* @fabaff

View File

@ -5,7 +5,8 @@ import logging
from proxmoxer import ProxmoxAPI
from proxmoxer.backends.https import AuthenticationError
from proxmoxer.core import ResourceException
from requests.exceptions import SSLError
import requests.exceptions
from requests.exceptions import ConnectTimeout, SSLError
import voluptuous as vol
from homeassistant.const import (
@ -31,7 +32,7 @@ CONF_NODES = "nodes"
CONF_VMS = "vms"
CONF_CONTAINERS = "containers"
COORDINATOR = "coordinator"
COORDINATORS = "coordinators"
API_DATA = "api_data"
DEFAULT_PORT = 8006
@ -90,6 +91,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
def build_client() -> ProxmoxAPI:
"""Build the Proxmox client connection."""
hass.data[PROXMOX_CLIENTS] = {}
for entry in config[DOMAIN]:
host = entry[CONF_HOST]
port = entry[CONF_PORT]
@ -98,6 +100,8 @@ async def async_setup(hass: HomeAssistant, config: dict):
password = entry[CONF_PASSWORD]
verify_ssl = entry[CONF_VERIFY_SSL]
hass.data[PROXMOX_CLIENTS][host] = None
try:
# Construct an API client with the given data for the given host
proxmox_client = ProxmoxClient(
@ -111,92 +115,101 @@ async def async_setup(hass: HomeAssistant, config: dict):
continue
except SSLError:
_LOGGER.error(
'Unable to verify proxmox server SSL. Try using "verify_ssl: false"'
"Unable to verify proxmox server SSL. "
'Try using "verify_ssl: false" for proxmox instance %s:%d',
host,
port,
)
continue
except ConnectTimeout:
_LOGGER.warning("Connection to host %s timed out during setup", host)
continue
return proxmox_client
hass.data[PROXMOX_CLIENTS][host] = proxmox_client
proxmox_client = await hass.async_add_executor_job(build_client)
await hass.async_add_executor_job(build_client)
async def async_update_data() -> dict:
"""Fetch data from API endpoint."""
coordinators = hass.data[DOMAIN][COORDINATORS] = {}
# Create a coordinator for each vm/container
for host_config in config[DOMAIN]:
host_name = host_config["host"]
coordinators[host_name] = {}
proxmox_client = hass.data[PROXMOX_CLIENTS][host_name]
# Skip invalid hosts
if proxmox_client is None:
continue
proxmox = proxmox_client.get_api_client()
def poll_api() -> dict:
data = {}
for host_config in config[DOMAIN]:
host_name = host_config["host"]
data[host_name] = {}
for node_config in host_config["nodes"]:
node_name = node_config["node"]
data[host_name][node_name] = {}
node_coordinators = coordinators[host_name][node_name] = {}
for vm_id in node_config["vms"]:
data[host_name][node_name][vm_id] = {}
vm_status = call_api_container_vm(
proxmox, node_name, vm_id, TYPE_VM
coordinator = create_coordinator_container_vm(
hass, proxmox, host_name, node_name, vm_id, TYPE_VM
)
if vm_status is None:
_LOGGER.warning("Vm/Container %s unable to be found", vm_id)
data[host_name][node_name][vm_id] = None
continue
data[host_name][node_name][vm_id] = parse_api_container_vm(
vm_status
)
for container_id in node_config["containers"]:
data[host_name][node_name][container_id] = {}
container_status = call_api_container_vm(
proxmox, node_name, container_id, TYPE_CONTAINER
)
if container_status is None:
_LOGGER.error(
"Vm/Container %s unable to be found", container_id
)
data[host_name][node_name][container_id] = None
continue
data[host_name][node_name][
container_id
] = parse_api_container_vm(container_status)
return data
return await hass.async_add_executor_job(poll_api)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="proxmox_coordinator",
update_method=async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
hass.data[DOMAIN][COORDINATOR] = coordinator
# Fetch initial data
await coordinator.async_config_entry_first_refresh()
await coordinator.async_refresh()
for platform in PLATFORMS:
node_coordinators[vm_id] = coordinator
for container_id in node_config["containers"]:
coordinator = create_coordinator_container_vm(
hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER
)
# Fetch initial data
await coordinator.async_refresh()
node_coordinators[container_id] = coordinator
for component in PLATFORMS:
await hass.async_create_task(
hass.helpers.discovery.async_load_platform(
platform, DOMAIN, {"config": config}, config
component, DOMAIN, {"config": config}, config
)
)
return True
def create_coordinator_container_vm(
hass, proxmox, host_name, node_name, vm_id, vm_type
):
"""Create and return a DataUpdateCoordinator for a vm/container."""
async def async_update_data():
"""Call the api and handle the response."""
def poll_api():
"""Call the api."""
vm_status = call_api_container_vm(proxmox, node_name, vm_id, vm_type)
return vm_status
vm_status = await hass.async_add_executor_job(poll_api)
if vm_status is None:
_LOGGER.warning(
"Vm/Container %s unable to be found in node %s", vm_id, node_name
)
return None
return parse_api_container_vm(vm_status)
return DataUpdateCoordinator(
hass,
_LOGGER,
name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}",
update_method=async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
def parse_api_container_vm(status):
"""Get the container or vm api data and return it formatted in a dictionary.
@ -216,7 +229,7 @@ def call_api_container_vm(proxmox, node_name, vm_id, machine_type):
status = proxmox.nodes(node_name).qemu(vm_id).status.current.get()
elif machine_type == TYPE_CONTAINER:
status = proxmox.nodes(node_name).lxc(vm_id).status.current.get()
except ResourceException:
except (ResourceException, requests.exceptions.ConnectionError):
return None
return status

View File

@ -1,8 +1,9 @@
"""Binary sensor to read Proxmox VE data."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import COORDINATOR, DOMAIN, ProxmoxEntity
from . import COORDINATORS, DOMAIN, PROXMOX_CLIENTS, ProxmoxEntity
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
@ -10,41 +11,45 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None:
return
coordinator = hass.data[DOMAIN][COORDINATOR]
sensors = []
for host_config in discovery_info["config"][DOMAIN]:
host_name = host_config["host"]
host_name_coordinators = hass.data[DOMAIN][COORDINATORS][host_name]
if hass.data[PROXMOX_CLIENTS][host_name] is None:
continue
for node_config in host_config["nodes"]:
node_name = node_config["node"]
for vm_id in node_config["vms"]:
coordinator_data = coordinator.data[host_name][node_name][vm_id]
coordinator = host_name_coordinators[node_name][vm_id]
coordinator_data = coordinator.data
# unfound vm case
if coordinator_data is None:
continue
vm_name = coordinator_data["name"]
vm_status = create_binary_sensor(
vm_sensor = create_binary_sensor(
coordinator, host_name, node_name, vm_id, vm_name
)
sensors.append(vm_status)
sensors.append(vm_sensor)
for container_id in node_config["containers"]:
coordinator_data = coordinator.data[host_name][node_name][container_id]
coordinator = host_name_coordinators[node_name][container_id]
coordinator_data = coordinator.data
# unfound container case
if coordinator_data is None:
continue
container_name = coordinator_data["name"]
container_status = create_binary_sensor(
container_sensor = create_binary_sensor(
coordinator, host_name, node_name, container_id, container_name
)
sensors.append(container_status)
sensors.append(container_sensor)
add_entities(sensors)
@ -62,7 +67,7 @@ def create_binary_sensor(coordinator, host_name, node_name, vm_id, name):
)
class ProxmoxBinarySensor(ProxmoxEntity):
class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity):
"""A binary sensor for reading Proxmox VE data."""
def __init__(
@ -80,12 +85,18 @@ class ProxmoxBinarySensor(ProxmoxEntity):
coordinator, unique_id, name, icon, host_name, node_name, vm_id
)
self._state = None
@property
def is_on(self):
"""Return the state of the binary sensor."""
data = self.coordinator.data
if data is None:
return None
return data["status"] == "running"
@property
def state(self):
"""Return the state of the binary sensor."""
data = self.coordinator.data[self._host_name][self._node_name][self._vm_id]
if data["status"] == "running":
return STATE_ON
return STATE_OFF
def available(self):
"""Return sensor availability."""
return super().available and self.coordinator.data is not None

View File

@ -2,6 +2,6 @@
"domain": "proxmoxve",
"name": "Proxmox VE",
"documentation": "https://www.home-assistant.io/integrations/proxmoxve",
"codeowners": ["@k4ds3", "@jhollowe"],
"codeowners": ["@k4ds3", "@jhollowe", "@Corbeno"],
"requirements": ["proxmoxer==1.1.1"]
}