""" 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_VERIFY_SSL, CONF_TIMEOUT, 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.4'] _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 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, 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): """Set up the QNAP NAS sensor.""" api = QNAPStatsAPI(config) api.update() if not api.data: hass.components.persistent_notification.create( 'Error: Failed to set up QNAP sensor.
' 'Check the logs for additional information. ' 'You will need to restart hass after fixing.', title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) return False 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 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), verify_ssl=config.get(CONF_VERIFY_SSL), timeout=config.get(CONF_TIMEOUT), ) 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) 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'] 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 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']) @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)), }