core/homeassistant/components/sensor/qnap.py

406 lines
13 KiB
Python

"""
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
import voluptuous as vol
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, ATTR_NAME,
CONF_VERIFY_SSL, CONF_TIMEOUT, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS)
from homeassistant.util import Throttle
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['qnapstats==0.2.7']
_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_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
DEFAULT_TIMEOUT = 5
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
NOTIFICATION_ID = 'qnap_notification'
NOTIFICATION_TITLE = 'QNAP Sensor Setup'
_SYSTEM_MON_COND = {
'status': ['Status', None, 'mdi:checkbox-marked-circle-outline'],
'system_temp': ['System Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
}
_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(_SYSTEM_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_VERIFY_SSL, default=True): cv.boolean,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
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): cv.ensure_list,
vol.Optional(CONF_DRIVES): cv.ensure_list,
vol.Optional(CONF_VOLUMES): cv.ensure_list,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the QNAP NAS sensor."""
api = QNAPStatsAPI(config)
api.update()
# QNAP is not available
if not api.data:
raise PlatformNotReady
sensors = []
# Basic sensors
for variable in config[CONF_MONITORED_CONDITIONS]:
if variable in _SYSTEM_MON_COND:
sensors.append(QNAPSystemSensor(
api, variable, _SYSTEM_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
for nic in config.get(CONF_NICS, api.data["system_stats"]["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
for drive in config.get(CONF_DRIVES, api.data["smart_drive_health"]):
sensors += [QNAPDriveSensor(api, variable,
_DRIVE_MON_COND[variable], drive)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in _DRIVE_MON_COND]
# Volume sensors
for volume in config.get(CONF_VOLUMES, api.data["volumes"]):
sensors += [QNAPVolumeSensor(api, variable,
_VOLUME_MON_COND[variable], volume)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in _VOLUME_MON_COND]
add_entities(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:
"""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(
'{}://{}'.format(protocol, config.get(CONF_HOST)),
config.get(CONF_PORT),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
verify_ssl=config.get(CONF_VERIFY_SSL),
timeout=config.get(CONF_TIMEOUT),
)
self.data = {}
@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: # noqa: E722 pylint: disable=bare-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)
return "{} {}".format(server_name, self.var_name)
@property
def icon(self):
"""Return the 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']
if 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 QNAPSystemSensor(QNAPSensor):
"""A QNAP sensor that monitors overall system health."""
@property
def state(self):
"""Return the state of the sensor."""
if self.var_id == 'status':
return self._api.data['system_health']
if self.var_id == 'system_temp':
return int(self._api.data['system_stats']['system']['temp_c'])
@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']) if data['temp_c'] is not None else 0
@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)),
}