""" 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.3'] _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): """Setup the QNAP NAS sensor.""" api = QNAPStatsAPI(config) api.update() if not api.data: import homeassistant.loader as loader loader.get_component('persistent_notification').create( hass, '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) 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 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)), }