From 829e0a7c4268a643ea31969ae25c30305d58680c Mon Sep 17 00:00:00 2001
From: Jonas <5180118+K4ds3@users.noreply.github.com>
Date: Fri, 22 Nov 2019 23:03:41 +0100
Subject: [PATCH] Add Proxmox VE integration (#27315)

* Added the Proxmox VE integration

* Fixed code as described in PR #27315

* Fixed small linting error

* Fix code as described in PR #27315 code review

* Improve code as described in PR #27315
---
 .coveragerc                                   |   1 +
 CODEOWNERS                                    |   1 +
 .../components/proxmoxve/__init__.py          | 154 ++++++++++++++++++
 .../components/proxmoxve/binary_sensor.py     | 112 +++++++++++++
 .../components/proxmoxve/manifest.json        |   8 +
 requirements_all.txt                          |   3 +
 6 files changed, 279 insertions(+)
 create mode 100644 homeassistant/components/proxmoxve/__init__.py
 create mode 100644 homeassistant/components/proxmoxve/binary_sensor.py
 create mode 100644 homeassistant/components/proxmoxve/manifest.json

diff --git a/.coveragerc b/.coveragerc
index ab4e91e5efd..a241c9ab79c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -526,6 +526,7 @@ omit =
     homeassistant/components/proliphix/climate.py
     homeassistant/components/prometheus/*
     homeassistant/components/prowl/notify.py
+    homeassistant/components/proxmoxve/*
     homeassistant/components/proxy/camera.py
     homeassistant/components/ptvsd/*
     homeassistant/components/pulseaudio_loopback/switch.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 005c3e3489e..6b4b3e6a2f2 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -239,6 +239,7 @@ homeassistant/components/plant/* @ChristianKuehnel
 homeassistant/components/plex/* @jjlawren
 homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew
 homeassistant/components/point/* @fredrike
+homeassistant/components/proxmoxve/* @k4ds3
 homeassistant/components/ps4/* @ktnrg45
 homeassistant/components/ptvsd/* @swamp-ig
 homeassistant/components/push/* @dgomes
diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py
new file mode 100644
index 00000000000..246dc2d48ad
--- /dev/null
+++ b/homeassistant/components/proxmoxve/__init__.py
@@ -0,0 +1,154 @@
+"""Support for Proxmox VE."""
+from enum import Enum
+import logging
+import time
+
+from proxmoxer import ProxmoxAPI
+from proxmoxer.backends.https import AuthenticationError
+import voluptuous as vol
+
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_USERNAME,
+    CONF_VERIFY_SSL,
+)
+import homeassistant.helpers.config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = "proxmoxve"
+PROXMOX_CLIENTS = "proxmox_clients"
+CONF_REALM = "realm"
+CONF_NODE = "node"
+CONF_NODES = "nodes"
+CONF_VMS = "vms"
+CONF_CONTAINERS = "containers"
+
+DEFAULT_PORT = 8006
+DEFAULT_REALM = "pam"
+DEFAULT_VERIFY_SSL = True
+
+CONFIG_SCHEMA = vol.Schema(
+    {
+        DOMAIN: vol.All(
+            cv.ensure_list,
+            [
+                vol.Schema(
+                    {
+                        vol.Required(CONF_HOST): cv.string,
+                        vol.Required(CONF_USERNAME): cv.string,
+                        vol.Required(CONF_PASSWORD): cv.string,
+                        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+                        vol.Optional(CONF_REALM, default=DEFAULT_REALM): cv.string,
+                        vol.Optional(
+                            CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL
+                        ): cv.boolean,
+                        vol.Required(CONF_NODES): vol.All(
+                            cv.ensure_list,
+                            [
+                                vol.Schema(
+                                    {
+                                        vol.Required(CONF_NODE): cv.string,
+                                        vol.Optional(CONF_VMS, default=[]): [
+                                            cv.positive_int
+                                        ],
+                                        vol.Optional(CONF_CONTAINERS, default=[]): [
+                                            cv.positive_int
+                                        ],
+                                    }
+                                )
+                            ],
+                        ),
+                    }
+                )
+            ],
+        )
+    },
+    extra=vol.ALLOW_EXTRA,
+)
+
+
+def setup(hass, config):
+    """Set up the component."""
+
+    # Create API Clients for later use
+    hass.data[PROXMOX_CLIENTS] = {}
+    for entry in config[DOMAIN]:
+        host = entry[CONF_HOST]
+        port = entry[CONF_PORT]
+        user = entry[CONF_USERNAME]
+        realm = entry[CONF_REALM]
+        password = entry[CONF_PASSWORD]
+        verify_ssl = entry[CONF_VERIFY_SSL]
+
+        try:
+            # Construct an API client with the given data for the given host
+            proxmox_client = ProxmoxClient(
+                host, port, user, realm, password, verify_ssl
+            )
+            proxmox_client.build_client()
+        except AuthenticationError:
+            _LOGGER.warning(
+                "Invalid credentials for proxmox instance %s:%d", host, port
+            )
+            continue
+
+        hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client
+
+    if hass.data[PROXMOX_CLIENTS]:
+        hass.helpers.discovery.load_platform(
+            "binary_sensor", DOMAIN, {"entries": config[DOMAIN]}, config
+        )
+        return True
+
+    return False
+
+
+class ProxmoxItemType(Enum):
+    """Represents the different types of machines in Proxmox."""
+
+    qemu = 0
+    lxc = 1
+
+
+class ProxmoxClient:
+    """A wrapper for the proxmoxer ProxmoxAPI client."""
+
+    def __init__(self, host, port, user, realm, password, verify_ssl):
+        """Initialize the ProxmoxClient."""
+
+        self._host = host
+        self._port = port
+        self._user = user
+        self._realm = realm
+        self._password = password
+        self._verify_ssl = verify_ssl
+
+        self._proxmox = None
+        self._connection_start_time = None
+
+    def build_client(self):
+        """Construct the ProxmoxAPI client."""
+
+        self._proxmox = ProxmoxAPI(
+            self._host,
+            port=self._port,
+            user=f"{self._user}@{self._realm}",
+            password=self._password,
+            verify_ssl=self._verify_ssl,
+        )
+
+        self._connection_start_time = time.time()
+
+    def get_api_client(self):
+        """Return the ProxmoxAPI client and rebuild it if necessary."""
+
+        connection_age = time.time() - self._connection_start_time
+
+        # Workaround for the Proxmoxer bug where the connection stops working after some time
+        if connection_age > 30 * 60:
+            self.build_client()
+
+        return self._proxmox
diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py
new file mode 100644
index 00000000000..15b1f1483e1
--- /dev/null
+++ b/homeassistant/components/proxmoxve/binary_sensor.py
@@ -0,0 +1,112 @@
+"""Binary sensor to read Proxmox VE data."""
+import logging
+
+from homeassistant.components.binary_sensor import BinarySensorDevice
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT
+
+from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType
+
+ATTRIBUTION = "Data provided by Proxmox VE"
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+    """Set up the sensor platform."""
+
+    sensors = []
+
+    for entry in discovery_info["entries"]:
+        port = entry[CONF_PORT]
+
+        for node in entry[CONF_NODES]:
+            for virtual_machine in node[CONF_VMS]:
+                sensors.append(
+                    ProxmoxBinarySensor(
+                        hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"],
+                        node["node"],
+                        ProxmoxItemType.qemu,
+                        virtual_machine,
+                    )
+                )
+
+            for container in node[CONF_CONTAINERS]:
+                sensors.append(
+                    ProxmoxBinarySensor(
+                        hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"],
+                        node["node"],
+                        ProxmoxItemType.lxc,
+                        container,
+                    )
+                )
+
+    add_entities(sensors, True)
+
+
+class ProxmoxBinarySensor(BinarySensorDevice):
+    """A binary sensor for reading Proxmox VE data."""
+
+    def __init__(self, proxmox_client, item_node, item_type, item_id):
+        """Initialize the binary sensor."""
+        self._proxmox_client = proxmox_client
+        self._item_node = item_node
+        self._item_type = item_type
+        self._item_id = item_id
+
+        self._vmname = None
+        self._name = None
+
+        self._state = None
+
+    @property
+    def name(self):
+        """Return the name of the entity."""
+        return self._name
+
+    @property
+    def is_on(self):
+        """Return true if VM/container is running."""
+        return self._state
+
+    @property
+    def device_state_attributes(self):
+        """Return device attributes of the entity."""
+        return {
+            "node": self._item_node,
+            "vmid": self._item_id,
+            "vmname": self._vmname,
+            "type": self._item_type.name,
+            ATTR_ATTRIBUTION: ATTRIBUTION,
+        }
+
+    def update(self):
+        """Check if the VM/Container is running."""
+        item = self.poll_item()
+
+        if item is None:
+            _LOGGER.warning("Failed to poll VM/container %s", self._item_id)
+            return
+
+        self._state = item["status"] == "running"
+
+    def poll_item(self):
+        """Find the VM/Container with the set item_id."""
+        items = (
+            self._proxmox_client.get_api_client()
+            .nodes(self._item_node)
+            .get(self._item_type.name)
+        )
+        item = next(
+            (item for item in items if item["vmid"] == str(self._item_id)), None
+        )
+
+        if item is None:
+            _LOGGER.warning("Couldn't find VM/Container with the ID %s", self._item_id)
+            return None
+
+        if self._vmname is None:
+            self._vmname = item["name"]
+
+        if self._name is None:
+            self._name = f"{self._item_node} {self._vmname} running"
+
+        return item
diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json
new file mode 100644
index 00000000000..9c03038a630
--- /dev/null
+++ b/homeassistant/components/proxmoxve/manifest.json
@@ -0,0 +1,8 @@
+{
+    "domain": "proxmoxve",
+    "name": "Proxmox VE",
+    "documentation": "https://www.home-assistant.io/integrations/proxmoxve",
+    "dependencies": [],
+    "codeowners": ["@k4ds3"],
+    "requirements": ["proxmoxer==1.0.3"]
+  }
\ No newline at end of file
diff --git a/requirements_all.txt b/requirements_all.txt
index 3fd222ac268..7d7852add97 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1023,6 +1023,9 @@ prometheus_client==0.7.1
 # homeassistant.components.tensorflow
 protobuf==3.6.1
 
+# homeassistant.components.proxmoxve
+proxmoxer==1.0.3
+
 # homeassistant.components.systemmonitor
 psutil==5.6.5
 
-- 
GitLab