diff --git a/.coveragerc b/.coveragerc index 88e446a30ea..294b6b1b747 100644 --- a/.coveragerc +++ b/.coveragerc @@ -285,6 +285,7 @@ omit = homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_public_transport.py + homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/temper.py diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py new file mode 100644 index 00000000000..31201879207 --- /dev/null +++ b/homeassistant/components/sensor/synologydsm.py @@ -0,0 +1,252 @@ +""" +Support for Synology NAS Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.synologydsm/ +""" + +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_MONITORED_CONDITIONS, TEMP_CELSIUS, EVENT_HOMEASSISTANT_START) +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +import voluptuous as vol + +REQUIREMENTS = ['python-synology==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DISKS = 'disks' +CONF_VOLUMES = 'volumes' +DEFAULT_NAME = 'Synology DSM' +DEFAULT_PORT = 5000 + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) + +_UTILISATION_MON_COND = { + 'cpu_other_load': ['CPU Load (Other)', '%', 'mdi:chip'], + 'cpu_user_load': ['CPU Load (User)', '%', 'mdi:chip'], + 'cpu_system_load': ['CPU Load (System)', '%', 'mdi:chip'], + 'cpu_total_load': ['CPU Load (Total)', '%', 'mdi:chip'], + 'cpu_1min_load': ['CPU Load (1 min)', '%', 'mdi:chip'], + 'cpu_5min_load': ['CPU Load (5 min)', '%', 'mdi:chip'], + 'cpu_15min_load': ['CPU Load (15 min)', '%', 'mdi:chip'], + 'memory_real_usage': ['Memory Usage (Real)', '%', 'mdi:memory'], + 'memory_size': ['Memory Size', 'Mb', 'mdi:memory'], + 'memory_cached': ['Memory Cached', 'Mb', 'mdi:memory'], + 'memory_available_swap': ['Memory Available (Swap)', 'Mb', 'mdi:memory'], + 'memory_available_real': ['Memory Available (Real)', 'Mb', 'mdi:memory'], + 'memory_total_swap': ['Memory Total (Swap)', 'Mb', 'mdi:memory'], + 'memory_total_real': ['Memory Total (Real)', 'Mb', 'mdi:memory'], + 'network_up': ['Network Up', 'Kbps', 'mdi:upload'], + 'network_down': ['Network Down', 'Kbps', 'mdi:download'], +} +_STORAGE_VOL_MON_COND = { + 'volume_status': ['Status', None, 'mdi:checkbox-marked-circle-outline'], + 'volume_device_type': ['Type', None, 'mdi:harddisk'], + 'volume_size_total': ['Total Size', None, 'mdi:chart-pie'], + 'volume_size_used': ['Used Space', None, 'mdi:chart-pie'], + 'volume_percentage_used': ['Volume Used', '%', 'mdi:chart-pie'], + 'volume_disk_temp_avg': ['Average Disk Temp', None, 'mdi:thermometer'], + 'volume_disk_temp_max': ['Maximum Disk Temp', None, 'mdi:thermometer'], +} +_STORAGE_DSK_MON_COND = { + 'disk_name': ['Name', None, 'mdi:harddisk'], + 'disk_device': ['Device', None, 'mdi:dots-horizontal'], + 'disk_smart_status': ['Status (Smart)', None, + 'mdi:checkbox-marked-circle-outline'], + 'disk_status': ['Status', None, 'mdi:checkbox-marked-circle-outline'], + 'disk_exceed_bad_sector_thr': ['Exceeded Max Bad Sectors', None, + 'mdi:test-tube'], + 'disk_below_remain_life_thr': ['Below Min Remaining Life', None, + 'mdi:test-tube'], + 'disk_temp': ['Temperature', None, 'mdi:thermometer'], +} + +_MONITORED_CONDITIONS = list(_UTILISATION_MON_COND.keys()) + \ + list(_STORAGE_VOL_MON_COND.keys()) + \ + list(_STORAGE_DSK_MON_COND.keys()) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + 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_DISKS, default=None): cv.ensure_list, + vol.Optional(CONF_VOLUMES, default=None): cv.ensure_list, +}) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the Synology NAS Sensor.""" + # pylint: disable=too-many-locals + def run_setup(event): + """Wait until HASS is fully initialized before creating. + + Delay the setup until Home Assistant is fully initialized. + This allows any entities to be created already + """ + # Setup API + api = SynoApi(config.get(CONF_HOST), config.get(CONF_PORT), + config.get(CONF_USERNAME), config.get(CONF_PASSWORD), + hass.config.units.temperature_unit) + + sensors = [SynoNasUtilSensor(api, variable, + _UTILISATION_MON_COND[variable]) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _UTILISATION_MON_COND] + + # Handle all Volumes + volumes = config['volumes'] + if volumes is None: + volumes = api.storage().volumes + + for volume in volumes: + sensors += [SynoNasStorageSensor(api, variable, + _STORAGE_VOL_MON_COND[variable], + volume) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _STORAGE_VOL_MON_COND] + + # Handle all Disks + disks = config['disks'] + if disks is None: + disks = api.storage().disks + + for disk in disks: + sensors += [SynoNasStorageSensor(api, variable, + _STORAGE_DSK_MON_COND[variable], + disk) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _STORAGE_DSK_MON_COND] + + add_devices_callback(sensors) + + # Wait until start event is sent to load this component. + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) + + +class SynoApi(): + """Class to interface with API.""" + + # pylint: disable=too-many-arguments, bare-except + def __init__(self, host, port, username, password, temp_unit): + """Constructor of the API wrapper class.""" + from SynologyDSM import SynologyDSM + self.temp_unit = temp_unit + + try: + self._api = SynologyDSM(host, + port, + username, + password) + except: + _LOGGER.error("Error setting up Synology DSM") + + def utilisation(self): + """Return utilisation information from API.""" + if self._api is not None: + return self._api.utilisation + + def storage(self): + """Return storage information from API.""" + if self._api is not None: + return self._api.storage + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update function for updating api information.""" + self._api.update() + + +class SynoNasSensor(Entity): + """Representation of a Synology Nas Sensor.""" + + def __init__(self, api, variable, variableInfo, monitor_device=None): + """Initialize the sensor.""" + self.var_id = variable + self.var_name = variableInfo[0] + self.var_units = variableInfo[1] + self.var_icon = variableInfo[2] + self.monitor_device = monitor_device + self._api = api + + @property + def name(self): + """Return the name of the sensor, if any.""" + if self.monitor_device is not None: + return "{} ({})".format(self.var_name, self.monitor_device) + else: + return 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.""" + if self.var_id in ['volume_disk_temp_avg', 'volume_disk_temp_max', + 'disk_temp']: + return self._api.temp_unit + else: + return self.var_units + + def update(self): + """Get the latest data for the states.""" + if self._api is not None: + self._api.update() + + +class SynoNasUtilSensor(SynoNasSensor): + """Representation a Synology Utilisation Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + network_sensors = ['network_up', 'network_down'] + memory_sensors = ['memory_size', 'memory_cached', + 'memory_available_swap', 'memory_available_real', + 'memory_total_swap', 'memory_total_real'] + + if self.var_id in network_sensors or self.var_id in memory_sensors: + attr = getattr(self._api.utilisation(), self.var_id)(False) + + if self.var_id in network_sensors: + return round(attr / 1024.0, 1) + elif self.var_id in memory_sensors: + return round(attr / 1024.0 / 1024.0, 1) + else: + return getattr(self._api.utilisation(), self.var_id) + + +class SynoNasStorageSensor(SynoNasSensor): + """Representation a Synology Utilisation Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + temp_sensors = ['volume_disk_temp_avg', 'volume_disk_temp_max', + 'disk_temp'] + + if self.monitor_device is not None: + if self.var_id in temp_sensors: + attr = getattr(self._api.storage(), + self.var_id)(self.monitor_device) + + if self._api.temp_unit == TEMP_CELSIUS: + return attr + else: + return round(attr * 1.8 + 32.0, 1) + else: + return getattr(self._api.storage(), + self.var_id)(self.monitor_device) diff --git a/requirements_all.txt b/requirements_all.txt index 9ba9390c03c..121597b9be9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,6 +427,9 @@ python-nmap==0.6.1 # homeassistant.components.notify.pushover python-pushover==0.2 +# homeassistant.components.sensor.synologydsm +python-synology==0.1.0 + # homeassistant.components.notify.telegram python-telegram-bot==5.2.0