From eac104127737ab56d0c9a59dbacfa2d7e88b4569 Mon Sep 17 00:00:00 2001 From: Corbeno Date: Sun, 11 Apr 2021 22:14:11 -0500 Subject: [PATCH] Create DataUpdateCoordinator for each proxmoxve vm/container (#45171) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 +- .../components/proxmoxve/__init__.py | 145 ++++++++++-------- .../components/proxmoxve/binary_sensor.py | 47 +++--- .../components/proxmoxve/manifest.json | 2 +- 4 files changed, 110 insertions(+), 86 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 838ed6cb143..ff0372b0d1f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index a149c8b6034..5777bb3054c 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -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 node_config in host_config["nodes"]: + node_name = node_config["node"] + node_coordinators = coordinators[host_name][node_name] = {} - for host_config in config[DOMAIN]: - host_name = host_config["host"] + for vm_id in node_config["vms"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, vm_id, TYPE_VM + ) - data[host_name] = {} + # Fetch initial data + await coordinator.async_refresh() - for node_config in host_config["nodes"]: - node_name = node_config["node"] - data[host_name][node_name] = {} + node_coordinators[vm_id] = coordinator - for vm_id in node_config["vms"]: - data[host_name][node_name][vm_id] = {} + for container_id in node_config["containers"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER + ) - vm_status = call_api_container_vm( - proxmox, node_name, vm_id, TYPE_VM - ) + # Fetch initial data + await coordinator.async_refresh() - 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 + node_coordinators[container_id] = coordinator - 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() - - for platform in PLATFORMS: + 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 diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 1151c2ec332..fedb513e5b4 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -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 diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index a47ce0a28ee..0f0029dff32 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -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"] }