mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
QNAP Sensor (#5666)
* Implement the QNAP sensor * Add sensors immediately * Remove unnecessary check * Use CONF_SSL instead of CONF_PROTOCOL
This commit is contained in:
committed by
Paulus Schoutsen
parent
2fc3dfff67
commit
bc65452efb
@ -330,6 +330,7 @@ omit =
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
|
406
homeassistant/components/sensor/qnap.py
Normal file
406
homeassistant/components/sensor/qnap.py
Normal file
@ -0,0 +1,406 @@
|
||||
"""
|
||||
Support for QNAP NAS Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.qnap/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_CELSIUS)
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
REQUIREMENTS = ['qnapstats==0.2.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DRIVE = 'Drive'
|
||||
ATTR_DRIVE_SIZE = 'Drive Size'
|
||||
ATTR_IP = 'IP Address'
|
||||
ATTR_MAC = 'MAC Address'
|
||||
ATTR_MASK = 'Mask'
|
||||
ATTR_MAX_SPEED = 'Max Speed'
|
||||
ATTR_MEMORY_SIZE = 'Memory Size'
|
||||
ATTR_MODEL = 'Model'
|
||||
ATTR_NAME = 'Name'
|
||||
ATTR_PACKETS_TX = 'Packets (TX)'
|
||||
ATTR_PACKETS_RX = 'Packets (RX)'
|
||||
ATTR_PACKETS_ERR = 'Packets (Err)'
|
||||
ATTR_SERIAL = 'Serial #'
|
||||
ATTR_TYPE = 'Type'
|
||||
ATTR_UPTIME = 'Uptime'
|
||||
ATTR_VOLUME_SIZE = 'Volume Size'
|
||||
|
||||
CONF_DRIVES = 'drives'
|
||||
CONF_NICS = 'nics'
|
||||
CONF_VOLUMES = 'volumes'
|
||||
DEFAULT_NAME = 'QNAP'
|
||||
DEFAULT_PORT = 8080
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
||||
_HEALTH_MON_COND = {
|
||||
'status': ['Status', None, 'mdi:checkbox-marked-circle-outline'],
|
||||
}
|
||||
_CPU_MON_COND = {
|
||||
'cpu_temp': ['CPU Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||
'cpu_usage': ['CPU Usage', '%', 'mdi:chip'],
|
||||
}
|
||||
_MEMORY_MON_COND = {
|
||||
'memory_free': ['Memory Available', 'GB', 'mdi:memory'],
|
||||
'memory_used': ['Memory Used', 'GB', 'mdi:memory'],
|
||||
'memory_percent_used': ['Memory Usage', '%', 'mdi:memory'],
|
||||
}
|
||||
_NETWORK_MON_COND = {
|
||||
'network_link_status': ['Network Link', None,
|
||||
'mdi:checkbox-marked-circle-outline'],
|
||||
'network_tx': ['Network Up', 'MB/s', 'mdi:upload'],
|
||||
'network_rx': ['Network Down', 'MB/s', 'mdi:download'],
|
||||
}
|
||||
_DRIVE_MON_COND = {
|
||||
'drive_smart_status': ['SMART Status', None,
|
||||
'mdi:checkbox-marked-circle-outline'],
|
||||
'drive_temp': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||
}
|
||||
_VOLUME_MON_COND = {
|
||||
'volume_size_used': ['Used Space', 'GB', 'mdi:chart-pie'],
|
||||
'volume_size_free': ['Free Space', 'GB', 'mdi:chart-pie'],
|
||||
'volume_percentage_used': ['Volume Used', '%', 'mdi:chart-pie'],
|
||||
}
|
||||
|
||||
_MONITORED_CONDITIONS = list(_HEALTH_MON_COND.keys()) + \
|
||||
list(_CPU_MON_COND.keys()) + \
|
||||
list(_MEMORY_MON_COND.keys()) + \
|
||||
list(_NETWORK_MON_COND.keys()) + \
|
||||
list(_DRIVE_MON_COND.keys()) + \
|
||||
list(_VOLUME_MON_COND.keys())
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(_MONITORED_CONDITIONS)]),
|
||||
vol.Optional(CONF_NICS, default=None): cv.ensure_list,
|
||||
vol.Optional(CONF_DRIVES, default=None): cv.ensure_list,
|
||||
vol.Optional(CONF_VOLUMES, default=None): cv.ensure_list,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the QNAP NAS sensor."""
|
||||
api = QNAPStatsAPI(config)
|
||||
api.update()
|
||||
|
||||
sensors = []
|
||||
|
||||
# Basic sensors
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||
if variable in _HEALTH_MON_COND:
|
||||
sensors.append(QNAPHealthStatus(api, variable,
|
||||
_HEALTH_MON_COND[variable]))
|
||||
if variable in _CPU_MON_COND:
|
||||
sensors.append(QNAPCPUSensor(api, variable,
|
||||
_CPU_MON_COND[variable]))
|
||||
if variable in _MEMORY_MON_COND:
|
||||
sensors.append(QNAPMemorySensor(api, variable,
|
||||
_MEMORY_MON_COND[variable]))
|
||||
|
||||
# Network sensors
|
||||
nics = config[CONF_NICS]
|
||||
if nics is None:
|
||||
nics = api.data["system_stats"]["nics"].keys()
|
||||
|
||||
for nic in nics:
|
||||
sensors += [QNAPNetworkSensor(api, variable,
|
||||
_NETWORK_MON_COND[variable], nic)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
if variable in _NETWORK_MON_COND]
|
||||
|
||||
# Drive sensors
|
||||
drives = config[CONF_DRIVES]
|
||||
if drives is None:
|
||||
drives = api.data["smart_drive_health"].keys()
|
||||
|
||||
for drive in drives:
|
||||
sensors += [QNAPDriveSensor(api, variable,
|
||||
_DRIVE_MON_COND[variable], drive)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
if variable in _DRIVE_MON_COND]
|
||||
|
||||
# Volume sensors
|
||||
volumes = config[CONF_VOLUMES]
|
||||
if volumes is None:
|
||||
volumes = api.data["volumes"].keys()
|
||||
|
||||
for volume in volumes:
|
||||
sensors += [QNAPVolumeSensor(api, variable,
|
||||
_VOLUME_MON_COND[variable], volume)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
if variable in _VOLUME_MON_COND]
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
def round_nicely(number):
|
||||
"""Round a number based on its size (so it looks nice)."""
|
||||
if number < 10:
|
||||
return round(number, 2)
|
||||
if number < 100:
|
||||
return round(number, 1)
|
||||
|
||||
return round(number)
|
||||
|
||||
|
||||
class QNAPStatsAPI(object):
|
||||
"""Class to interface with the API."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the API wrapper."""
|
||||
from qnapstats import QNAPStats
|
||||
|
||||
protocol = "https" if config.get(CONF_SSL) else "http"
|
||||
self._api = QNAPStats(
|
||||
protocol + "://" + config.get(CONF_HOST),
|
||||
config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
|
||||
self.data = {}
|
||||
|
||||
# pylint: disable=bare-except
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update API information and store locally."""
|
||||
try:
|
||||
self.data["system_stats"] = self._api.get_system_stats()
|
||||
self.data["system_health"] = self._api.get_system_health()
|
||||
self.data["smart_drive_health"] = self._api.get_smart_disk_health()
|
||||
self.data["volumes"] = self._api.get_volumes()
|
||||
self.data["bandwidth"] = self._api.get_bandwidth()
|
||||
except:
|
||||
_LOGGER.exception("Failed to fetch QNAP stats from the NAS.")
|
||||
|
||||
|
||||
class QNAPSensor(Entity):
|
||||
"""Base class for a QNAP sensor."""
|
||||
|
||||
def __init__(self, api, variable, variable_info, monitor_device=None):
|
||||
"""Initialize the sensor."""
|
||||
self.var_id = variable
|
||||
self.var_name = variable_info[0]
|
||||
self.var_units = variable_info[1]
|
||||
self.var_icon = variable_info[2]
|
||||
self.monitor_device = monitor_device
|
||||
self._api = api
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor, if any."""
|
||||
server_name = self._api.data["system_stats"]["system"]["name"]
|
||||
|
||||
if self.monitor_device is not None:
|
||||
return "{} {} ({})".format(server_name,
|
||||
self.var_name,
|
||||
self.monitor_device)
|
||||
else:
|
||||
return "{} {}".format(server_name,
|
||||
self.var_name)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self.var_icon
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self.var_units
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data for the states."""
|
||||
self._api.update()
|
||||
|
||||
|
||||
class QNAPCPUSensor(QNAPSensor):
|
||||
"""A QNAP sensor that monitors CPU stats."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self.var_id == "cpu_temp":
|
||||
return self._api.data["system_stats"]["cpu"]["temp_c"]
|
||||
elif self.var_id == "cpu_usage":
|
||||
return self._api.data["system_stats"]["cpu"]["usage_percent"]
|
||||
|
||||
|
||||
class QNAPMemorySensor(QNAPSensor):
|
||||
"""A QNAP sensor that monitors memory stats."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
free = float(self._api.data["system_stats"]["memory"]["free"]) / 1024
|
||||
if self.var_id == "memory_free":
|
||||
return round_nicely(free)
|
||||
|
||||
total = float(self._api.data["system_stats"]["memory"]["total"]) / 1024
|
||||
|
||||
used = total - free
|
||||
if self.var_id == "memory_used":
|
||||
return round_nicely(used)
|
||||
|
||||
if self.var_id == "memory_percent_used":
|
||||
return round(used / total * 100)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._api.data:
|
||||
data = self._api.data["system_stats"]["memory"]
|
||||
size = round_nicely(float(data["total"]) / 1024)
|
||||
return {
|
||||
ATTR_MEMORY_SIZE: "{} GB".format(size),
|
||||
}
|
||||
|
||||
|
||||
class QNAPNetworkSensor(QNAPSensor):
|
||||
"""A QNAP sensor that monitors network stats."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self.var_id == "network_link_status":
|
||||
nic = self._api.data["system_stats"]["nics"][self.monitor_device]
|
||||
return nic["link_status"]
|
||||
|
||||
data = self._api.data["bandwidth"][self.monitor_device]
|
||||
if self.var_id == "network_tx":
|
||||
return round_nicely(data["tx"] / 1024 / 1024)
|
||||
|
||||
if self.var_id == "network_rx":
|
||||
return round_nicely(data["rx"] / 1024 / 1024)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._api.data:
|
||||
data = self._api.data["system_stats"]["nics"][self.monitor_device]
|
||||
return {
|
||||
ATTR_IP: data["ip"],
|
||||
ATTR_MASK: data["mask"],
|
||||
ATTR_MAC: data["mac"],
|
||||
ATTR_MAX_SPEED: data["max_speed"],
|
||||
ATTR_PACKETS_TX: data["tx_packets"],
|
||||
ATTR_PACKETS_RX: data["rx_packets"],
|
||||
ATTR_PACKETS_ERR: data["err_packets"]
|
||||
}
|
||||
|
||||
|
||||
class QNAPHealthStatus(QNAPSensor):
|
||||
"""A QNAP sensor that monitors overall system health."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._api.data["system_health"]
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._api.data:
|
||||
data = self._api.data["system_stats"]
|
||||
days = int(data["uptime"]["days"])
|
||||
hours = int(data["uptime"]["hours"])
|
||||
minutes = int(data["uptime"]["minutes"])
|
||||
|
||||
return {
|
||||
ATTR_NAME: data["system"]["name"],
|
||||
ATTR_MODEL: data["system"]["model"],
|
||||
ATTR_SERIAL: data["system"]["serial_number"],
|
||||
ATTR_UPTIME: "{:0>2d}d {:0>2d}h {:0>2d}m".format(days,
|
||||
hours,
|
||||
minutes)
|
||||
}
|
||||
|
||||
|
||||
class QNAPDriveSensor(QNAPSensor):
|
||||
"""A QNAP sensor that monitors HDD/SSD drive stats."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
data = self._api.data["smart_drive_health"][self.monitor_device]
|
||||
|
||||
if self.var_id == "drive_smart_status":
|
||||
return data["health"]
|
||||
|
||||
if self.var_id == "drive_temp":
|
||||
return int(data["temp_c"])
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor, if any."""
|
||||
server_name = self._api.data["system_stats"]["system"]["name"]
|
||||
|
||||
return "{} {} (Drive {})".format(
|
||||
server_name,
|
||||
self.var_name,
|
||||
self.monitor_device
|
||||
)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._api.data:
|
||||
data = self._api.data["smart_drive_health"][self.monitor_device]
|
||||
return {
|
||||
ATTR_DRIVE: data["drive_number"],
|
||||
ATTR_MODEL: data["model"],
|
||||
ATTR_SERIAL: data["serial"],
|
||||
ATTR_TYPE: data["type"],
|
||||
}
|
||||
|
||||
|
||||
class QNAPVolumeSensor(QNAPSensor):
|
||||
"""A QNAP sensor that monitors storage volume stats."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
data = self._api.data["volumes"][self.monitor_device]
|
||||
|
||||
free_gb = int(data["free_size"]) / 1024 / 1024 / 1024
|
||||
if self.var_id == "volume_size_free":
|
||||
return round_nicely(free_gb)
|
||||
|
||||
total_gb = int(data["total_size"]) / 1024 / 1024 / 1024
|
||||
|
||||
used_gb = total_gb - free_gb
|
||||
if self.var_id == "volume_size_used":
|
||||
return round_nicely(used_gb)
|
||||
|
||||
if self.var_id == "volume_percentage_used":
|
||||
return round(used_gb / total_gb * 100)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._api.data:
|
||||
data = self._api.data["volumes"][self.monitor_device]
|
||||
total_gb = int(data["total_size"]) / 1024 / 1024 / 1024
|
||||
|
||||
return {
|
||||
ATTR_VOLUME_SIZE: "{} GB".format(round_nicely(total_gb)),
|
||||
}
|
@ -560,6 +560,9 @@ pywemo==0.4.11
|
||||
# homeassistant.components.zabbix
|
||||
pyzabbix==0.7.4
|
||||
|
||||
# homeassistant.components.sensor.qnap
|
||||
qnapstats==0.2.1
|
||||
|
||||
# homeassistant.components.climate.radiotherm
|
||||
radiotherm==1.2
|
||||
|
||||
|
Reference in New Issue
Block a user