From 9bcd878ef364f001357acb7e488a561da89372e1 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 5 May 2020 23:32:33 +0200 Subject: [PATCH 01/25] Add Synology DSM Security API --- .../components/synology_dsm/__init__.py | 12 +++-- .../components/synology_dsm/config_flow.py | 10 +++- .../components/synology_dsm/const.py | 7 +++ .../components/synology_dsm/sensor.py | 47 +++++++++++++++++++ .../components/synology_dsm/strings.json | 3 +- .../synology_dsm/translations/en.json | 3 +- .../synology_dsm/test_config_flow.py | 6 ++- 7 files changed, 81 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index b2ff2d2e8efb22..9c8f5651eb8f2a 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta from synology_dsm import SynologyDSM +from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.core.utilization import SynoCoreUtilization from synology_dsm.api.dsm.information import SynoDSMInformation from synology_dsm.api.storage.storage import SynoStorage @@ -24,8 +25,10 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( + CONF_SECURITY, CONF_VOLUMES, DEFAULT_SCAN_INTERVAL, + DEFAULT_SECURITY, DEFAULT_SSL, DOMAIN, SYNO_API, @@ -124,6 +127,7 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): self.dsm: SynologyDSM = None self.information: SynoDSMInformation = None self.utilisation: SynoCoreUtilization = None + self.security: SynoCoreSecurity = None self.storage: SynoStorage = None self._unsub_dispatcher = None @@ -145,11 +149,11 @@ async def async_setup(self): ) await self._hass.async_add_executor_job(self._fetch_device_configuration) - await self.update() + await self.async_update() self._unsub_dispatcher = async_track_time_interval( self._hass, - self.update, + self.async_update, timedelta( minutes=self._entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL @@ -161,13 +165,15 @@ def _fetch_device_configuration(self): """Fetch initial device config.""" self.information = self.dsm.information self.utilisation = self.dsm.utilisation + if self._entry.options.get(CONF_SECURITY, DEFAULT_SECURITY): + self.security = self.dsm.security self.storage = self.dsm.storage async def async_unload(self): """Stop interacting with the NAS and prepare for removal from hass.""" self._unsub_dispatcher() - async def update(self, now=None): + async def async_update(self, now=None): """Update function for updating API information.""" await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index a4d7d75e073692..23f4695ff85963 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -29,10 +29,12 @@ import homeassistant.helpers.config_validation as cv from .const import ( + CONF_SECURITY, CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, + DEFAULT_SECURITY, DEFAULT_SSL, ) from .const import DOMAIN # pylint: disable=unused-import @@ -250,7 +252,13 @@ async def async_step_init(self, user_input=None): default=self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), - ): cv.positive_int + ): cv.positive_int, + vol.Optional( + CONF_SECURITY, + default=self.config_entry.options.get( + CONF_SECURITY, DEFAULT_SECURITY + ), + ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index e0a166e908beec..ee0a29a2794232 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -14,11 +14,14 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" # Configuration +CONF_SECURITY = "security" CONF_VOLUMES = "volumes" + DEFAULT_SSL = True DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 # Options +DEFAULT_SECURITY = True DEFAULT_SCAN_INTERVAL = 15 # min UTILISATION_SENSORS = { @@ -58,5 +61,9 @@ "disk_temp": ["Temperature", None, "mdi:thermometer"], } +SECURITY_SENSORS = { + "status": ["Security status", None, "mdi:checkbox-marked-circle-outline"], +} + TEMP_SENSORS_KEYS = ["volume_disk_temp_avg", "volume_disk_temp_max", "disk_temp"] diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 81873cad4cde83..69d120ac633283 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -13,6 +13,7 @@ ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType @@ -21,6 +22,7 @@ BASE_NAME, CONF_VOLUMES, DOMAIN, + SECURITY_SENSORS, STORAGE_DISK_SENSORS, STORAGE_VOL_SENSORS, SYNO_API, @@ -43,6 +45,12 @@ async def async_setup_entry( for sensor_type in UTILISATION_SENSORS ] + if api.security: + sensors += [ + SynoNasSecuritySensor(api, sensor_type, SECURITY_SENSORS[sensor_type]) + for sensor_type in SECURITY_SENSORS + ] + # Handle all volumes if api.storage.volumes_ids: for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids): @@ -205,3 +213,42 @@ def device_info(self) -> Dict[str, any]: "sw_version": self._api.information.version_string, "via_device": (DOMAIN, self._api.information.serial), } + + +class SynoNasSecuritySensor(SynoNasSensor): + """Representation a Synology Security sensor.""" + + @property + def state(self): + """Return the state.""" + return getattr(self._api.security, self.sensor_type) + + async def options_updated(self): + """Config entry options are updated, remove entity if option is disabled.""" + await self.async_remove() + + async def async_remove(self): + """Clean up when removing entity. + + Remove entity if no entry in entity registry exist. + Remove entity registry entry if no entry in device registry exist. + Remove device registry entry if there is only one linked entity (this entity). + Remove entity registry entry if there are more than one entity linked to the device registry entry. + """ + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_entry = entity_registry.async_get(self.entity_id) + if not entity_entry: + await super().async_remove() + return + + device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get(entity_entry.device_id) + if not device_entry: + entity_registry.async_remove(self.entity_id) + return + + if len(async_entries_for_device(entity_registry, entity_entry.device_id)) == 1: + device_registry.async_remove_device(device_entry.id) + return + + entity_registry.async_remove(self.entity_id) diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 0024a7db612a6b..ca32e57d5fcf9b 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minutes between scans" + "scan_interval": "Minutes between scans", + "security": "Add security sensors" } } } diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 48a8118528a04a..3fb34a6acffe6d 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minutes between scans" + "scan_interval": "Minutes between scans", + "security": "Add security sensors" } } } diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index f592ad90a88044..773947cc320751 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -14,10 +14,12 @@ from homeassistant.components import ssdp from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( + CONF_SECURITY, CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, + DEFAULT_SECURITY, DEFAULT_SSL, DOMAIN, ) @@ -425,11 +427,13 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + assert config_entry.options[CONF_SECURITY] == DEFAULT_SECURITY # Manual result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2}, + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2, CONF_SECURITY: False}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 + assert not config_entry.options[CONF_SECURITY] From be37b0a45ff1c9e57a692fbd74b726b3ececc312 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 7 May 2020 12:18:40 +0200 Subject: [PATCH 02/25] Remove device registry cleaning because it's automatic --- homeassistant/components/synology_dsm/sensor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 69d120ac633283..0a106318b82f72 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -13,7 +13,6 @@ ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType @@ -232,7 +231,6 @@ async def async_remove(self): Remove entity if no entry in entity registry exist. Remove entity registry entry if no entry in device registry exist. - Remove device registry entry if there is only one linked entity (this entity). Remove entity registry entry if there are more than one entity linked to the device registry entry. """ entity_registry = await self.hass.helpers.entity_registry.async_get_registry() @@ -247,8 +245,4 @@ async def async_remove(self): entity_registry.async_remove(self.entity_id) return - if len(async_entries_for_device(entity_registry, entity_entry.device_id)) == 1: - device_registry.async_remove_device(device_entry.id) - return - entity_registry.async_remove(self.entity_id) From 4aaa4c5faf2c93fdb45d0de88299d6da4d95a733 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 7 May 2020 20:57:55 +0200 Subject: [PATCH 03/25] Remove useless options_updated --- homeassistant/components/synology_dsm/sensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 0a106318b82f72..3ca7564f68ae13 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -222,10 +222,6 @@ def state(self): """Return the state.""" return getattr(self._api.security, self.sensor_type) - async def options_updated(self): - """Config entry options are updated, remove entity if option is disabled.""" - await self.async_remove() - async def async_remove(self): """Clean up when removing entity. From d466de34b94ec4b96d74dd724bc7b303dabc4965 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Fri, 8 May 2020 14:27:30 +0200 Subject: [PATCH 04/25] Fix manual update --- homeassistant/components/synology_dsm/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 3ca7564f68ae13..8e7b37b6c454a6 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -143,7 +143,7 @@ def should_poll(self) -> bool: async def async_update(self): """Only used by the generic entity update service.""" - await self._api.update() + await self._api.async_update() async def async_added_to_hass(self): """Register state update callback.""" From 9bba84d5ffc891685d93988c1cecaac36d8ba5e9 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Fri, 8 May 2020 15:59:18 +0200 Subject: [PATCH 05/25] Refactor Synology DSM device - use device name instead of id - add device type to name - use disk manufacturer, model and firmware version Breaking: - unique_id now uses key instead of label - now binary sensor: - disk_exceed_bad_sector_thr - disk_below_remain_life_thr - removed sensor: - volume type (RAID, SHR ...) - disk name (Drive [X]) - disk device (/dev/sd[Y]) --- .../components/synology_dsm/__init__.py | 143 +++++++++++++++++- .../components/synology_dsm/binary_sensor.py | 55 +++++++ .../components/synology_dsm/const.py | 9 +- .../components/synology_dsm/sensor.py | 117 ++------------ 4 files changed, 211 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/synology_dsm/binary_sensor.py diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 9c8f5651eb8f2a..55def067214312 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,5 +1,7 @@ """The Synology DSM component.""" +import asyncio from datetime import timedelta +from typing import Dict from synology_dsm import SynologyDSM from synology_dsm.api.core.security import SynoCoreSecurity @@ -10,6 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_DISKS, CONF_HOST, CONF_MAC, @@ -20,18 +23,25 @@ CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType from .const import ( + BASE_NAME, CONF_SECURITY, CONF_VOLUMES, DEFAULT_SCAN_INTERVAL, DEFAULT_SECURITY, DEFAULT_SSL, DOMAIN, + PLATFORMS, SYNO_API, + TEMP_SENSORS_KEYS, UNDO_UPDATE_LISTENER, ) @@ -52,6 +62,8 @@ extra=vol.ALLOW_EXTRA, ) +ATTRIBUTION = "Data provided by Synology" + async def async_setup(hass, config): """Set up Synology DSM sensors from legacy config file.""" @@ -91,16 +103,24 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): entry, data={**entry.data, CONF_MAC: network.macs} ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) return True async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Synology DSM sensors.""" - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) if unload_ok: entry_data = hass.data[DOMAIN][entry.unique_id] @@ -177,3 +197,116 @@ async def async_update(self, now=None): """Update function for updating API information.""" await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) + + +class SynologyDSMEntity(Entity): + """Representation of a Synology NAS entry.""" + + def __init__( + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], + device_id: str = None, + ): + """Initialize the Synology DSM entity.""" + self._api = api + self.entity_type = entity_type + self._name = BASE_NAME + self._unit = entity_info[1] + self._icon = entity_info[2] + self._unique_id = f"{self._api.information.serial}_{entity_type}" + self._device_id = device_id + self._device_name = None + self._device_manufacturer = None + self._device_model = None + self._device_firmware = None + self._device_type = None + + if self._device_id: + if "volume" in entity_type: + volume = self._api.storage._get_volume(self._device_id) + # Volume does not have a name + self._device_name = volume["id"].replace("_", " ").capitalize() + self._device_manufacturer = "Synology" + self._device_model = self._api.information.model + self._device_firmware = self._api.information.version_string + self._device_type = ( + volume["device_type"] + .replace("_", " ") + .replace("raid", "RAID") + .replace("shr", "SHR") + ) + elif "disk" in entity_type: + disk = self._api.storage._get_disk(self._device_id) + self._device_name = disk["name"] + self._device_manufacturer = disk["vendor"] + self._device_model = disk["model"].strip() + self._device_firmware = disk["firm"] + self._device_type = disk["diskType"] + self._name += f" {self._device_name}" + self._unique_id += f"_{self._device_id}" + + self._name += f" {entity_info[0]}" + + self._unsub_dispatcher = None + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def unit_of_measurement(self) -> str: + """Return the unit the value is expressed in.""" + if self.entity_type in TEMP_SENSORS_KEYS: + return self._api.temp_unit + return self._unit + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._api.information.serial)}, + "name": "Synology NAS", + "manufacturer": "Synology", + "model": self._api.information.model, + "sw_version": self._api.information.version_string, + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + async def async_update(self): + """Only used by the generic entity update service.""" + if not self.enabled: + return + + await self._api.async_update() + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, self._api.signal_sensor_update, self.async_write_ha_state + ) + + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py new file mode 100644 index 00000000000000..054f00888bd037 --- /dev/null +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -0,0 +1,55 @@ +"""Support for Synology DSM binary sensors.""" +from typing import Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DISKS +from homeassistant.helpers.typing import HomeAssistantType + +from . import SynologyDSMEntity +from .const import DOMAIN, STORAGE_DISK_BINARY_SENSORS, SYNO_API + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Synology NAS Sensor.""" + + api = hass.data[DOMAIN][entry.unique_id][SYNO_API] + + entities = [] + + # Handle all disks + if api.storage.disks_ids: + for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): + entities += [ + SynoNasStorageSensor( + api, sensor_type, STORAGE_DISK_BINARY_SENSORS[sensor_type], disk + ) + for sensor_type in STORAGE_DISK_BINARY_SENSORS + ] + + async_add_entities(entities) + + +class SynoNasStorageSensor(SynologyDSMEntity): + """Representation a Synology Storage sensor.""" + + @property + def state(self): + """Return the state.""" + attr = getattr(self._api.storage, self.entity_type)(self._device_id) + if attr is None: + return None + return attr + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, + "name": f"Synology NAS ({self._device_name} {self._device_type})", + "manufacturer": self._device_manufacturer, + "model": self._device_model, + "sw_version": self._device_firmware, + "via_device": (DOMAIN, self._api.information.serial), + } diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index ee0a29a2794232..cf4a933b3c7874 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -7,6 +7,8 @@ ) DOMAIN = "synology_dsm" +PLATFORMS = ["binary_sensor", "sensor"] + BASE_NAME = "Synology" # Entry keys @@ -44,7 +46,6 @@ } STORAGE_VOL_SENSORS = { "volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], - "volume_device_type": ["Type", None, "mdi:harddisk"], "volume_size_total": ["Total Size", DATA_TERABYTES, "mdi:chart-pie"], "volume_size_used": ["Used Space", DATA_TERABYTES, "mdi:chart-pie"], "volume_percentage_used": ["Volume Used", UNIT_PERCENTAGE, "mdi:chart-pie"], @@ -52,13 +53,13 @@ "volume_disk_temp_max": ["Maximum Disk Temp", None, "mdi:thermometer"], } STORAGE_DISK_SENSORS = { - "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_temp": ["Temperature", None, "mdi:thermometer"], +} +STORAGE_DISK_BINARY_SENSORS = { "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"], } SECURITY_SENSORS = { diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 8e7b37b6c454a6..9e2dcf51ce65da 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -1,9 +1,8 @@ -"""Support for Synology DSM Sensors.""" +"""Support for Synology DSM sensors.""" from typing import Dict from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_DISKS, DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, @@ -11,14 +10,11 @@ PRECISION_TENTHS, TEMP_CELSIUS, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType -from . import SynoApi +from . import SynologyDSMEntity from .const import ( - BASE_NAME, CONF_VOLUMES, DOMAIN, SECURITY_SENSORS, @@ -29,8 +25,6 @@ UTILISATION_SENSORS, ) -ATTRIBUTION = "Data provided by Synology" - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -73,96 +67,13 @@ async def async_setup_entry( async_add_entities(sensors) -class SynoNasSensor(Entity): - """Representation of a Synology NAS sensor.""" - - def __init__( - self, - api: SynoApi, - sensor_type: str, - sensor_info: Dict[str, str], - monitored_device: str = None, - ): - """Initialize the sensor.""" - self._api = api - self.sensor_type = sensor_type - self._name = f"{BASE_NAME} {sensor_info[0]}" - self._unit = sensor_info[1] - self._icon = sensor_info[2] - self.monitored_device = monitored_device - self._unique_id = f"{self._api.information.serial}_{sensor_info[0]}" - - if self.monitored_device: - self._name += f" ({self.monitored_device})" - self._unique_id += f"_{self.monitored_device}" - - self._unsub_dispatcher = None - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - if self.sensor_type in TEMP_SENSORS_KEYS: - return self.hass.config.units.temperature_unit - return self._unit - - @property - def device_state_attributes(self) -> Dict[str, any]: - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def device_info(self) -> Dict[str, any]: - """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._api.information.serial)}, - "name": "Synology NAS", - "manufacturer": "Synology", - "model": self._api.information.model, - "sw_version": self._api.information.version_string, - } - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - async def async_update(self): - """Only used by the generic entity update service.""" - await self._api.async_update() - - async def async_added_to_hass(self): - """Register state update callback.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, self._api.signal_sensor_update, self.async_write_ha_state - ) - - async def async_will_remove_from_hass(self): - """Clean up after entity before removal.""" - self._unsub_dispatcher() - - -class SynoNasUtilSensor(SynoNasSensor): +class SynoNasUtilSensor(SynologyDSMEntity): """Representation a Synology Utilisation sensor.""" @property def state(self): """Return the state.""" - attr = getattr(self._api.utilisation, self.sensor_type) + attr = getattr(self._api.utilisation, self.entity_type) if callable(attr): attr = attr() if attr is None: @@ -179,13 +90,13 @@ def state(self): return attr -class SynoNasStorageSensor(SynoNasSensor): +class SynoNasStorageSensor(SynologyDSMEntity): """Representation a Synology Storage sensor.""" @property def state(self): """Return the state.""" - attr = getattr(self._api.storage, self.sensor_type)(self.monitored_device) + attr = getattr(self._api.storage, self.entity_type)(self._device_id) if attr is None: return None @@ -203,24 +114,22 @@ def state(self): def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "identifiers": { - (DOMAIN, self._api.information.serial, self.monitored_device) - }, - "name": f"Synology NAS ({self.monitored_device})", - "manufacturer": "Synology", - "model": self._api.information.model, - "sw_version": self._api.information.version_string, + "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, + "name": f"Synology NAS ({self._device_name} {self._device_type})", + "manufacturer": self._device_manufacturer, + "model": self._device_model, + "sw_version": self._device_firmware, "via_device": (DOMAIN, self._api.information.serial), } -class SynoNasSecuritySensor(SynoNasSensor): +class SynoNasSecuritySensor(SynologyDSMEntity): """Representation a Synology Security sensor.""" @property def state(self): """Return the state.""" - return getattr(self._api.security, self.sensor_type) + return getattr(self._api.security, self.entity_type) async def async_remove(self): """Clean up when removing entity. From 6a0f8f1646ae07850276a797a6682ffef75901a8 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sun, 10 May 2020 21:28:05 +0200 Subject: [PATCH 06/25] Security is binary + entity_registry_enabled_default + device_class --- .../components/synology_dsm/__init__.py | 44 +++- .../components/synology_dsm/binary_sensor.py | 34 ++- .../components/synology_dsm/const.py | 244 +++++++++++++++--- .../components/synology_dsm/sensor.py | 55 +--- 4 files changed, 289 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 55def067214312..d8876ed7e2d2a8 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -39,6 +39,11 @@ DEFAULT_SECURITY, DEFAULT_SSL, DOMAIN, + ENTITY_CLASS, + ENTITY_ENABLE, + ENTITY_ICON, + ENTITY_NAME, + ENTITY_UNIT, PLATFORMS, SYNO_API, TEMP_SENSORS_KEYS, @@ -213,8 +218,10 @@ def __init__( self._api = api self.entity_type = entity_type self._name = BASE_NAME - self._unit = entity_info[1] - self._icon = entity_info[2] + self._class = entity_info[ENTITY_CLASS] + self._enable_default = entity_info[ENTITY_ENABLE] + self._icon = entity_info[ENTITY_ICON] + self._unit = entity_info[ENTITY_UNIT] self._unique_id = f"{self._api.information.serial}_{entity_type}" self._device_id = device_id self._device_name = None @@ -247,7 +254,7 @@ def __init__( self._name += f" {self._device_name}" self._unique_id += f"_{self._device_id}" - self._name += f" {entity_info[0]}" + self._name += f" {entity_info[ENTITY_NAME]}" self._unsub_dispatcher = None @@ -273,6 +280,11 @@ def unit_of_measurement(self) -> str: return self._api.temp_unit return self._unit + @property + def device_class(self) -> str: + """Return the class of this device.""" + return self._class + @property def device_state_attributes(self) -> Dict[str, any]: """Return the state attributes.""" @@ -289,6 +301,11 @@ def device_info(self) -> Dict[str, any]: "sw_version": self._api.information.version_string, } + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enable_default + @property def should_poll(self) -> bool: """No polling needed.""" @@ -310,3 +327,24 @@ async def async_added_to_hass(self): async def async_will_remove_from_hass(self): """Clean up after entity before removal.""" self._unsub_dispatcher() + + async def async_remove(self): + """Clean up when removing entity. + + Remove entity if no entry in entity registry exist. + Remove entity registry entry if no entry in device registry exist. + Remove entity registry entry if there are more than one entity linked to the device registry entry. + """ + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_entry = entity_registry.async_get(self.entity_id) + if not entity_entry: + await super().async_remove() + return + + device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get(entity_entry.device_id) + if not device_entry: + entity_registry.async_remove(self.entity_id) + return + + entity_registry.async_remove(self.entity_id) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 054f00888bd037..30ae63243ac5f3 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,28 +1,41 @@ """Support for Synology DSM binary sensors.""" from typing import Dict +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS from homeassistant.helpers.typing import HomeAssistantType from . import SynologyDSMEntity -from .const import DOMAIN, STORAGE_DISK_BINARY_SENSORS, SYNO_API +from .const import ( + DOMAIN, + SECURITY_BINARY_SENSORS, + STORAGE_DISK_BINARY_SENSORS, + SYNO_API, +) async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: - """Set up the Synology NAS Sensor.""" + """Set up the Synology NAS binary sensor.""" api = hass.data[DOMAIN][entry.unique_id][SYNO_API] entities = [] + if api.security: + entities += [ + SynoDSMSecurityBinarySensor( + api, "status", SECURITY_BINARY_SENSORS["status"] + ) + ] + # Handle all disks if api.storage.disks_ids: for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): entities += [ - SynoNasStorageSensor( + SynoDSMStorageBinarySensor( api, sensor_type, STORAGE_DISK_BINARY_SENSORS[sensor_type], disk ) for sensor_type in STORAGE_DISK_BINARY_SENSORS @@ -31,11 +44,20 @@ async def async_setup_entry( async_add_entities(entities) -class SynoNasStorageSensor(SynologyDSMEntity): - """Representation a Synology Storage sensor.""" +class SynoDSMSecurityBinarySensor(SynologyDSMEntity, BinarySensorEntity): + """Representation a Synology Security binary sensor.""" + + @property + def is_on(self): + """Return the state.""" + return getattr(self._api.security, self.entity_type) != "safe" + + +class SynoDSMStorageBinarySensor(SynologyDSMEntity, BinarySensorEntity): + """Representation a Synology Storage binary sensor.""" @property - def state(self): + def is_on(self): """Return the state.""" attr = getattr(self._api.storage, self.entity_type)(self._device_id) if attr is None: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index cf4a933b3c7874..5910e804465e6a 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -26,44 +26,222 @@ DEFAULT_SECURITY = True DEFAULT_SCAN_INTERVAL = 15 # min + +ENTITY_NAME = "name" +ENTITY_UNIT = "unit" +ENTITY_ICON = "icon" +ENTITY_CLASS = "device_class" +ENTITY_ENABLE = "enable" + +# Binary sensors +STORAGE_DISK_BINARY_SENSORS = { + "disk_exceed_bad_sector_thr": { + ENTITY_NAME: "Exceeded Max Bad Sectors", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:test-tube", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "disk_below_remain_life_thr": { + ENTITY_NAME: "Below Min Remaining Life", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:test-tube", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, +} + +SECURITY_BINARY_SENSORS = { + "status": { + ENTITY_NAME: "Security status", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: "safety", + ENTITY_ENABLE: True, + }, +} + +# Sensors UTILISATION_SENSORS = { - "cpu_other_load": ["CPU Load (Other)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_user_load": ["CPU Load (User)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_system_load": ["CPU Load (System)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_total_load": ["CPU Load (Total)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_1min_load": ["CPU Load (1 min)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_5min_load": ["CPU Load (5 min)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_15min_load": ["CPU Load (15 min)", UNIT_PERCENTAGE, "mdi:chip"], - "memory_real_usage": ["Memory Usage (Real)", UNIT_PERCENTAGE, "mdi:memory"], - "memory_size": ["Memory Size", DATA_MEGABYTES, "mdi:memory"], - "memory_cached": ["Memory Cached", DATA_MEGABYTES, "mdi:memory"], - "memory_available_swap": ["Memory Available (Swap)", DATA_MEGABYTES, "mdi:memory"], - "memory_available_real": ["Memory Available (Real)", DATA_MEGABYTES, "mdi:memory"], - "memory_total_swap": ["Memory Total (Swap)", DATA_MEGABYTES, "mdi:memory"], - "memory_total_real": ["Memory Total (Real)", DATA_MEGABYTES, "mdi:memory"], - "network_up": ["Network Up", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:upload"], - "network_down": ["Network Down", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:download"], + "cpu_other_load": { + ENTITY_NAME: "CPU Load (Other)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + "cpu_user_load": { + ENTITY_NAME: "CPU Load (User)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "cpu_system_load": { + ENTITY_NAME: "CPU Load (System)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + "cpu_total_load": { + ENTITY_NAME: "CPU Load (Total)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "cpu_1min_load": { + ENTITY_NAME: "CPU Load (1 min)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + "cpu_5min_load": { + ENTITY_NAME: "CPU Load (5 min)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "cpu_15min_load": { + ENTITY_NAME: "CPU Load (15 min)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "memory_real_usage": { + ENTITY_NAME: "Memory Usage (Real)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "memory_size": { + ENTITY_NAME: "Memory Size", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + "memory_cached": { + ENTITY_NAME: "Memory Cached", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + "memory_available_swap": { + ENTITY_NAME: "Memory Available (Swap)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "memory_available_real": { + ENTITY_NAME: "Memory Available (Real)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "memory_total_swap": { + ENTITY_NAME: "Memory Total (Swap)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "memory_total_real": { + ENTITY_NAME: "Memory Total (Real)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "network_up": { + ENTITY_NAME: "Network Up", + ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, + ENTITY_ICON: "mdi:upload", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "network_down": { + ENTITY_NAME: "Network Down", + ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, + ENTITY_ICON: "mdi:download", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, } STORAGE_VOL_SENSORS = { - "volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], - "volume_size_total": ["Total Size", DATA_TERABYTES, "mdi:chart-pie"], - "volume_size_used": ["Used Space", DATA_TERABYTES, "mdi:chart-pie"], - "volume_percentage_used": ["Volume Used", UNIT_PERCENTAGE, "mdi:chart-pie"], - "volume_disk_temp_avg": ["Average Disk Temp", None, "mdi:thermometer"], - "volume_disk_temp_max": ["Maximum Disk Temp", None, "mdi:thermometer"], + "volume_status": { + ENTITY_NAME: "Status", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "volume_size_total": { + ENTITY_NAME: "Total Size", + ENTITY_UNIT: DATA_TERABYTES, + ENTITY_ICON: "mdi:chart-pie", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + "volume_size_used": { + ENTITY_NAME: "Used Space", + ENTITY_UNIT: DATA_TERABYTES, + ENTITY_ICON: "mdi:chart-pie", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "volume_percentage_used": { + ENTITY_NAME: "Volume Used", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chart-pie", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "volume_disk_temp_avg": { + ENTITY_NAME: "Average Disk Temp", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:thermometer", + ENTITY_CLASS: "temperature", + ENTITY_ENABLE: True, + }, + "volume_disk_temp_max": { + ENTITY_NAME: "Maximum Disk Temp", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:thermometer", + ENTITY_CLASS: "temperature", + ENTITY_ENABLE: False, + }, } STORAGE_DISK_SENSORS = { - "disk_smart_status": ["Status (Smart)", None, "mdi:checkbox-marked-circle-outline"], - "disk_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], - "disk_temp": ["Temperature", None, "mdi:thermometer"], -} -STORAGE_DISK_BINARY_SENSORS = { - "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"], -} - -SECURITY_SENSORS = { - "status": ["Security status", None, "mdi:checkbox-marked-circle-outline"], + "disk_smart_status": { + ENTITY_NAME: "Status (Smart)", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + "disk_status": { + ENTITY_NAME: "Status", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + "disk_temp": { + ENTITY_NAME: "Temperature", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:thermometer", + ENTITY_CLASS: "temperature", + ENTITY_ENABLE: True, + }, } diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 9e2dcf51ce65da..354978120516f2 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -17,7 +17,6 @@ from .const import ( CONF_VOLUMES, DOMAIN, - SECURITY_SENSORS, STORAGE_DISK_SENSORS, STORAGE_VOL_SENSORS, SYNO_API, @@ -33,22 +32,16 @@ async def async_setup_entry( api = hass.data[DOMAIN][entry.unique_id][SYNO_API] - sensors = [ - SynoNasUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) + entities = [ + SynoDSMUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) for sensor_type in UTILISATION_SENSORS ] - if api.security: - sensors += [ - SynoNasSecuritySensor(api, sensor_type, SECURITY_SENSORS[sensor_type]) - for sensor_type in SECURITY_SENSORS - ] - # Handle all volumes if api.storage.volumes_ids: for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids): - sensors += [ - SynoNasStorageSensor( + entities += [ + SynoDSMStorageSensor( api, sensor_type, STORAGE_VOL_SENSORS[sensor_type], volume ) for sensor_type in STORAGE_VOL_SENSORS @@ -57,17 +50,17 @@ async def async_setup_entry( # Handle all disks if api.storage.disks_ids: for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): - sensors += [ - SynoNasStorageSensor( + entities += [ + SynoDSMStorageSensor( api, sensor_type, STORAGE_DISK_SENSORS[sensor_type], disk ) for sensor_type in STORAGE_DISK_SENSORS ] - async_add_entities(sensors) + async_add_entities(entities) -class SynoNasUtilSensor(SynologyDSMEntity): +class SynoDSMUtilSensor(SynologyDSMEntity): """Representation a Synology Utilisation sensor.""" @property @@ -90,7 +83,7 @@ def state(self): return attr -class SynoNasStorageSensor(SynologyDSMEntity): +class SynoDSMStorageSensor(SynologyDSMEntity): """Representation a Synology Storage sensor.""" @property @@ -121,33 +114,3 @@ def device_info(self) -> Dict[str, any]: "sw_version": self._device_firmware, "via_device": (DOMAIN, self._api.information.serial), } - - -class SynoNasSecuritySensor(SynologyDSMEntity): - """Representation a Synology Security sensor.""" - - @property - def state(self): - """Return the state.""" - return getattr(self._api.security, self.entity_type) - - async def async_remove(self): - """Clean up when removing entity. - - Remove entity if no entry in entity registry exist. - Remove entity registry entry if no entry in device registry exist. - Remove entity registry entry if there are more than one entity linked to the device registry entry. - """ - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() - entity_entry = entity_registry.async_get(self.entity_id) - if not entity_entry: - await super().async_remove() - return - - device_registry = await self.hass.helpers.device_registry.async_get_registry() - device_entry = device_registry.async_get(entity_entry.device_id) - if not device_entry: - entity_registry.async_remove(self.entity_id) - return - - entity_registry.async_remove(self.entity_id) From 33cbcb5bfbb5d7057147815097989800da8025b6 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 11 May 2020 01:26:33 +0200 Subject: [PATCH 07/25] Do not fetch the API if all entries of it are disabled --- .../components/synology_dsm/__init__.py | 117 +++++++++++++----- .../components/synology_dsm/binary_sensor.py | 2 +- .../components/synology_dsm/const.py | 60 ++++----- 3 files changed, 122 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index d8876ed7e2d2a8..2dc692ea0e1e99 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -22,6 +22,7 @@ CONF_SSL, CONF_USERNAME, ) +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -33,10 +34,8 @@ from .const import ( BASE_NAME, - CONF_SECURITY, CONF_VOLUMES, DEFAULT_SCAN_INTERVAL, - DEFAULT_SECURITY, DEFAULT_SSL, DOMAIN, ENTITY_CLASS, @@ -149,11 +148,17 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): self._hass = hass self._entry = entry + # DSM APIs self.dsm: SynologyDSM = None self.information: SynoDSMInformation = None - self.utilisation: SynoCoreUtilization = None self.security: SynoCoreSecurity = None self.storage: SynoStorage = None + self.utilisation: SynoCoreUtilization = None + + # Should we fetch them + self._with_security = False + self._with_storage = False + self._with_utilisation = False self._unsub_dispatcher = None @@ -173,6 +178,8 @@ async def async_setup(self): device_token=self._entry.data.get("device_token"), ) + await self._should_fetch_api() + await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.async_update() @@ -186,13 +193,75 @@ async def async_setup(self): ), ) + async def _should_fetch_api(self): + + entity_reg = await self._hass.helpers.entity_registry.async_get_registry() + entity_entries = entity_registry.async_entries_for_config_entry( + entity_reg, self._entry.entry_id + ) + if not entity_entries: + self._with_security = True + self._with_storage = True + self._with_utilisation = True + return + + self._with_security = False + self._with_storage = False + self._with_utilisation = False + for entity_entry in entity_entries: + print("----------------------------------------------") + print(entity_entry) + # Pass disabled entries + if entity_entry.disabled_by: + continue + + # Check if we should fetch specific APIs + # for api_key in SECURITY_API_KEYS: + # if api_key in entity_entry.unique_id: + # print("#################################################") + # print(entity_entry) + # print("#################################################") + # self._with_security = True + + if "security" in entity_entry.unique_id: + self._with_security = True + + # for api_key in STORAGE_API_KEYS: + # if api_key in entity_entry.unique_id: + # self._with_storage = True + if "storage" in entity_entry.unique_id: + self._with_storage = True + + # for api_key in UTILISATION_API_KEYS: + # if api_key in entity_entry.unique_id: + # self._with_utilisation = True + if "utilisation" in entity_entry.unique_id: + self._with_utilisation = True + + if not self._with_security: + self.dsm._security = None # pylint: disable=protected-access + self.security = None + + if not self._with_storage: + self.dsm._storage = None # pylint: disable=protected-access + self.storage = None + + if not self._with_utilisation: + self.dsm._utilisation = None # pylint: disable=protected-access + self.utilisation = None + def _fetch_device_configuration(self): """Fetch initial device config.""" self.information = self.dsm.information - self.utilisation = self.dsm.utilisation - if self._entry.options.get(CONF_SECURITY, DEFAULT_SECURITY): + + if self._with_security: self.security = self.dsm.security - self.storage = self.dsm.storage + + if self._with_storage: + self.storage = self.dsm.storage + + if self._with_utilisation: + self.utilisation = self.dsm.utilisation async def async_unload(self): """Stop interacting with the NAS and prepare for removal from hass.""" @@ -200,6 +269,13 @@ async def async_unload(self): async def async_update(self, now=None): """Update function for updating API information.""" + print("secu ", self._with_security) + print("stor ", self._with_storage) + print("util ", self._with_utilisation) + await self._should_fetch_api() + print("secuAPI ", self.security) + print("storAPI ", self.storage) + print("utilAPI ", self.utilisation) await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) @@ -216,8 +292,9 @@ def __init__( ): """Initialize the Synology DSM entity.""" self._api = api - self.entity_type = entity_type + self.entity_type = entity_type.split(":")[-1] self._name = BASE_NAME + # self._api_key = entity_info[ENTITY_API] self._class = entity_info[ENTITY_CLASS] self._enable_default = entity_info[ENTITY_ENABLE] self._icon = entity_info[ENTITY_ICON] @@ -301,6 +378,11 @@ def device_info(self) -> Dict[str, any]: "sw_version": self._api.information.version_string, } + # @property + # def api_key(self) -> str: + # """Return the api key.""" + # return self._api_key + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" @@ -327,24 +409,3 @@ async def async_added_to_hass(self): async def async_will_remove_from_hass(self): """Clean up after entity before removal.""" self._unsub_dispatcher() - - async def async_remove(self): - """Clean up when removing entity. - - Remove entity if no entry in entity registry exist. - Remove entity registry entry if no entry in device registry exist. - Remove entity registry entry if there are more than one entity linked to the device registry entry. - """ - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() - entity_entry = entity_registry.async_get(self.entity_id) - if not entity_entry: - await super().async_remove() - return - - device_registry = await self.hass.helpers.device_registry.async_get_registry() - device_entry = device_registry.async_get(entity_entry.device_id) - if not device_entry: - entity_registry.async_remove(self.entity_id) - return - - entity_registry.async_remove(self.entity_id) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 30ae63243ac5f3..35ac93e4a340ad 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry( if api.security: entities += [ SynoDSMSecurityBinarySensor( - api, "status", SECURITY_BINARY_SENSORS["status"] + api, "security:status", SECURITY_BINARY_SENSORS["security:status"] ) ] diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 5910e804465e6a..c4cd16deaf4275 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -1,4 +1,8 @@ """Constants for Synology DSM.""" +# from synology_dsm.api.core.security import SynoCoreSecurity +# from synology_dsm.api.core.utilization import SynoCoreUtilization +# from synology_dsm.api.storage.storage import SynoStorage + from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, @@ -35,14 +39,14 @@ # Binary sensors STORAGE_DISK_BINARY_SENSORS = { - "disk_exceed_bad_sector_thr": { + "storage:disk_exceed_bad_sector_thr": { ENTITY_NAME: "Exceeded Max Bad Sectors", ENTITY_UNIT: None, ENTITY_ICON: "mdi:test-tube", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "disk_below_remain_life_thr": { + "storage:disk_below_remain_life_thr": { ENTITY_NAME: "Below Min Remaining Life", ENTITY_UNIT: None, ENTITY_ICON: "mdi:test-tube", @@ -52,7 +56,7 @@ } SECURITY_BINARY_SENSORS = { - "status": { + "security:status": { ENTITY_NAME: "Security status", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", @@ -63,112 +67,112 @@ # Sensors UTILISATION_SENSORS = { - "cpu_other_load": { + "utilisation:cpu_other_load": { ENTITY_NAME: "CPU Load (Other)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "cpu_user_load": { + "utilisation:cpu_user_load": { ENTITY_NAME: "CPU Load (User)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "cpu_system_load": { + "utilisation:cpu_system_load": { ENTITY_NAME: "CPU Load (System)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "cpu_total_load": { + "utilisation:cpu_total_load": { ENTITY_NAME: "CPU Load (Total)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "cpu_1min_load": { + "utilisation:cpu_1min_load": { ENTITY_NAME: "CPU Load (1 min)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "cpu_5min_load": { + "utilisation:cpu_5min_load": { ENTITY_NAME: "CPU Load (5 min)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "cpu_15min_load": { + "utilisation:cpu_15min_load": { ENTITY_NAME: "CPU Load (15 min)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "memory_real_usage": { + "utilisation:memory_real_usage": { ENTITY_NAME: "Memory Usage (Real)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "memory_size": { + "utilisation:memory_size": { ENTITY_NAME: "Memory Size", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "memory_cached": { + "utilisation:memory_cached": { ENTITY_NAME: "Memory Cached", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "memory_available_swap": { + "utilisation:memory_available_swap": { ENTITY_NAME: "Memory Available (Swap)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "memory_available_real": { + "utilisation:memory_available_real": { ENTITY_NAME: "Memory Available (Real)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "memory_total_swap": { + "utilisation:memory_total_swap": { ENTITY_NAME: "Memory Total (Swap)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "memory_total_real": { + "utilisation:memory_total_real": { ENTITY_NAME: "Memory Total (Real)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "network_up": { + "utilisation:network_up": { ENTITY_NAME: "Network Up", ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, ENTITY_ICON: "mdi:upload", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "network_down": { + "utilisation:network_down": { ENTITY_NAME: "Network Down", ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, ENTITY_ICON: "mdi:download", @@ -177,42 +181,42 @@ }, } STORAGE_VOL_SENSORS = { - "volume_status": { + "storage:volume_status": { ENTITY_NAME: "Status", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "volume_size_total": { + "storage:volume_size_total": { ENTITY_NAME: "Total Size", ENTITY_UNIT: DATA_TERABYTES, ENTITY_ICON: "mdi:chart-pie", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "volume_size_used": { + "storage:volume_size_used": { ENTITY_NAME: "Used Space", ENTITY_UNIT: DATA_TERABYTES, ENTITY_ICON: "mdi:chart-pie", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "volume_percentage_used": { + "storage:volume_percentage_used": { ENTITY_NAME: "Volume Used", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chart-pie", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "volume_disk_temp_avg": { + "storage:volume_disk_temp_avg": { ENTITY_NAME: "Average Disk Temp", ENTITY_UNIT: None, ENTITY_ICON: "mdi:thermometer", ENTITY_CLASS: "temperature", ENTITY_ENABLE: True, }, - "volume_disk_temp_max": { + "storage:volume_disk_temp_max": { ENTITY_NAME: "Maximum Disk Temp", ENTITY_UNIT: None, ENTITY_ICON: "mdi:thermometer", @@ -221,21 +225,21 @@ }, } STORAGE_DISK_SENSORS = { - "disk_smart_status": { + "storage:disk_smart_status": { ENTITY_NAME: "Status (Smart)", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "disk_status": { + "storage:disk_status": { ENTITY_NAME: "Status", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "disk_temp": { + "storage:disk_temp": { ENTITY_NAME: "Temperature", ENTITY_UNIT: None, ENTITY_ICON: "mdi:thermometer", From 3834e70b1879904084657e5ed932a870b9df96b2 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 01:08:45 +0200 Subject: [PATCH 08/25] Clean + use Synology API_KEY --- .../components/synology_dsm/__init__.py | 25 +++----- .../components/synology_dsm/binary_sensor.py | 3 +- .../components/synology_dsm/const.py | 63 ++++++++++--------- 3 files changed, 43 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 2dc692ea0e1e99..d697c626b098c4 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -199,12 +199,14 @@ async def _should_fetch_api(self): entity_entries = entity_registry.async_entries_for_config_entry( entity_reg, self._entry.entry_id ) + # Entities not added yet if not entity_entries: self._with_security = True self._with_storage = True self._with_utilisation = True return + # Determine which if at least one entity uses specific API self._with_security = False self._with_storage = False self._with_utilisation = False @@ -216,28 +218,19 @@ async def _should_fetch_api(self): continue # Check if we should fetch specific APIs - # for api_key in SECURITY_API_KEYS: - # if api_key in entity_entry.unique_id: - # print("#################################################") - # print(entity_entry) - # print("#################################################") - # self._with_security = True - - if "security" in entity_entry.unique_id: + if SynoCoreSecurity.API_KEY in entity_entry.unique_id: self._with_security = True + continue - # for api_key in STORAGE_API_KEYS: - # if api_key in entity_entry.unique_id: - # self._with_storage = True - if "storage" in entity_entry.unique_id: + if SynoStorage.API_KEY in entity_entry.unique_id: self._with_storage = True + continue - # for api_key in UTILISATION_API_KEYS: - # if api_key in entity_entry.unique_id: - # self._with_utilisation = True - if "utilisation" in entity_entry.unique_id: + if SynoCoreUtilization.API_KEY in entity_entry.unique_id: self._with_utilisation = True + continue + # Reset not used API if not self._with_security: self.dsm._security = None # pylint: disable=protected-access self.security = None diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 35ac93e4a340ad..92cd63960428cf 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -27,8 +27,9 @@ async def async_setup_entry( if api.security: entities += [ SynoDSMSecurityBinarySensor( - api, "security:status", SECURITY_BINARY_SENSORS["security:status"] + api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type] ) + for sensor_type in SECURITY_BINARY_SENSORS ] # Handle all disks diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index c4cd16deaf4275..7bd5df7399f80e 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -1,7 +1,8 @@ """Constants for Synology DSM.""" -# from synology_dsm.api.core.security import SynoCoreSecurity -# from synology_dsm.api.core.utilization import SynoCoreUtilization -# from synology_dsm.api.storage.storage import SynoStorage + +from synology_dsm.api.core.security import SynoCoreSecurity +from synology_dsm.api.core.utilization import SynoCoreUtilization +from synology_dsm.api.storage.storage import SynoStorage from homeassistant.const import ( DATA_MEGABYTES, @@ -39,14 +40,14 @@ # Binary sensors STORAGE_DISK_BINARY_SENSORS = { - "storage:disk_exceed_bad_sector_thr": { + f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": { ENTITY_NAME: "Exceeded Max Bad Sectors", ENTITY_UNIT: None, ENTITY_ICON: "mdi:test-tube", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "storage:disk_below_remain_life_thr": { + f"{SynoStorage.API_KEY}:disk_below_remain_life_thr": { ENTITY_NAME: "Below Min Remaining Life", ENTITY_UNIT: None, ENTITY_ICON: "mdi:test-tube", @@ -56,7 +57,7 @@ } SECURITY_BINARY_SENSORS = { - "security:status": { + f"{SynoCoreSecurity.API_KEY}:status": { ENTITY_NAME: "Security status", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", @@ -67,112 +68,112 @@ # Sensors UTILISATION_SENSORS = { - "utilisation:cpu_other_load": { + f"{SynoCoreUtilization.API_KEY}:cpu_other_load": { ENTITY_NAME: "CPU Load (Other)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "utilisation:cpu_user_load": { + f"{SynoCoreUtilization.API_KEY}:cpu_user_load": { ENTITY_NAME: "CPU Load (User)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:cpu_system_load": { + f"{SynoCoreUtilization.API_KEY}:cpu_system_load": { ENTITY_NAME: "CPU Load (System)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "utilisation:cpu_total_load": { + f"{SynoCoreUtilization.API_KEY}:cpu_total_load": { ENTITY_NAME: "CPU Load (Total)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:cpu_1min_load": { + f"{SynoCoreUtilization.API_KEY}:cpu_1min_load": { ENTITY_NAME: "CPU Load (1 min)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "utilisation:cpu_5min_load": { + f"{SynoCoreUtilization.API_KEY}:cpu_5min_load": { ENTITY_NAME: "CPU Load (5 min)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:cpu_15min_load": { + f"{SynoCoreUtilization.API_KEY}:cpu_15min_load": { ENTITY_NAME: "CPU Load (15 min)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:memory_real_usage": { + f"{SynoCoreUtilization.API_KEY}:memory_real_usage": { ENTITY_NAME: "Memory Usage (Real)", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:memory_size": { + f"{SynoCoreUtilization.API_KEY}:memory_size": { ENTITY_NAME: "Memory Size", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "utilisation:memory_cached": { + f"{SynoCoreUtilization.API_KEY}:memory_cached": { ENTITY_NAME: "Memory Cached", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "utilisation:memory_available_swap": { + f"{SynoCoreUtilization.API_KEY}:memory_available_swap": { ENTITY_NAME: "Memory Available (Swap)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:memory_available_real": { + f"{SynoCoreUtilization.API_KEY}:memory_available_real": { ENTITY_NAME: "Memory Available (Real)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:memory_total_swap": { + f"{SynoCoreUtilization.API_KEY}:memory_total_swap": { ENTITY_NAME: "Memory Total (Swap)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:memory_total_real": { + f"{SynoCoreUtilization.API_KEY}:memory_total_real": { ENTITY_NAME: "Memory Total (Real)", ENTITY_UNIT: DATA_MEGABYTES, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:network_up": { + f"{SynoCoreUtilization.API_KEY}:network_up": { ENTITY_NAME: "Network Up", ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, ENTITY_ICON: "mdi:upload", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "utilisation:network_down": { + f"{SynoCoreUtilization.API_KEY}:network_down": { ENTITY_NAME: "Network Down", ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, ENTITY_ICON: "mdi:download", @@ -181,42 +182,42 @@ }, } STORAGE_VOL_SENSORS = { - "storage:volume_status": { + f"{SynoStorage.API_KEY}:volume_status": { ENTITY_NAME: "Status", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "storage:volume_size_total": { + f"{SynoStorage.API_KEY}:volume_size_total": { ENTITY_NAME: "Total Size", ENTITY_UNIT: DATA_TERABYTES, ENTITY_ICON: "mdi:chart-pie", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "storage:volume_size_used": { + f"{SynoStorage.API_KEY}:volume_size_used": { ENTITY_NAME: "Used Space", ENTITY_UNIT: DATA_TERABYTES, ENTITY_ICON: "mdi:chart-pie", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "storage:volume_percentage_used": { + f"{SynoStorage.API_KEY}:volume_percentage_used": { ENTITY_NAME: "Volume Used", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:chart-pie", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "storage:volume_disk_temp_avg": { + f"{SynoStorage.API_KEY}:volume_disk_temp_avg": { ENTITY_NAME: "Average Disk Temp", ENTITY_UNIT: None, ENTITY_ICON: "mdi:thermometer", ENTITY_CLASS: "temperature", ENTITY_ENABLE: True, }, - "storage:volume_disk_temp_max": { + f"{SynoStorage.API_KEY}:volume_disk_temp_max": { ENTITY_NAME: "Maximum Disk Temp", ENTITY_UNIT: None, ENTITY_ICON: "mdi:thermometer", @@ -225,21 +226,21 @@ }, } STORAGE_DISK_SENSORS = { - "storage:disk_smart_status": { + f"{SynoStorage.API_KEY}:disk_smart_status": { ENTITY_NAME: "Status (Smart)", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, - "storage:disk_status": { + f"{SynoStorage.API_KEY}:disk_status": { ENTITY_NAME: "Status", ENTITY_UNIT: None, ENTITY_ICON: "mdi:checkbox-marked-circle-outline", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, - "storage:disk_temp": { + f"{SynoStorage.API_KEY}:disk_temp": { ENTITY_NAME: "Temperature", ENTITY_UNIT: None, ENTITY_ICON: "mdi:thermometer", From 5bec045ce77302e6f3d5b76d5bcca50a2bfe8269 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 01:22:05 +0200 Subject: [PATCH 09/25] Remove stale CONF_SECURITY --- homeassistant/components/synology_dsm/config_flow.py | 8 -------- homeassistant/components/synology_dsm/const.py | 2 -- tests/components/synology_dsm/test_config_flow.py | 6 +----- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 23f4695ff85963..01815920620366 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -29,12 +29,10 @@ import homeassistant.helpers.config_validation as cv from .const import ( - CONF_SECURITY, CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, - DEFAULT_SECURITY, DEFAULT_SSL, ) from .const import DOMAIN # pylint: disable=unused-import @@ -253,12 +251,6 @@ async def async_step_init(self, user_input=None): CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ): cv.positive_int, - vol.Optional( - CONF_SECURITY, - default=self.config_entry.options.get( - CONF_SECURITY, DEFAULT_SECURITY - ), - ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 7bd5df7399f80e..47c5ac5782d067 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -21,14 +21,12 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" # Configuration -CONF_SECURITY = "security" CONF_VOLUMES = "volumes" DEFAULT_SSL = True DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 # Options -DEFAULT_SECURITY = True DEFAULT_SCAN_INTERVAL = 15 # min diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 773947cc320751..f592ad90a88044 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -14,12 +14,10 @@ from homeassistant.components import ssdp from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( - CONF_SECURITY, CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, - DEFAULT_SECURITY, DEFAULT_SSL, DOMAIN, ) @@ -427,13 +425,11 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL - assert config_entry.options[CONF_SECURITY] == DEFAULT_SECURITY # Manual result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2, CONF_SECURITY: False}, + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 - assert not config_entry.options[CONF_SECURITY] From ff5121e679eaabfdfa9aa90c3363441bbadcfee6 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 01:26:05 +0200 Subject: [PATCH 10/25] remove logs --- homeassistant/components/synology_dsm/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index d697c626b098c4..c7bd074036ddcf 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -211,8 +211,6 @@ async def _should_fetch_api(self): self._with_storage = False self._with_utilisation = False for entity_entry in entity_entries: - print("----------------------------------------------") - print(entity_entry) # Pass disabled entries if entity_entry.disabled_by: continue @@ -262,13 +260,7 @@ async def async_unload(self): async def async_update(self, now=None): """Update function for updating API information.""" - print("secu ", self._with_security) - print("stor ", self._with_storage) - print("util ", self._with_utilisation) await self._should_fetch_api() - print("secuAPI ", self.security) - print("storAPI ", self.storage) - print("utilAPI ", self.utilisation) await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) From dd22a84eece6226e1ce1ebfff8df0b3d0a27a15a Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 01:56:37 +0200 Subject: [PATCH 11/25] Remove stale comments --- homeassistant/components/synology_dsm/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index c7bd074036ddcf..17209e30e7310c 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -279,7 +279,6 @@ def __init__( self._api = api self.entity_type = entity_type.split(":")[-1] self._name = BASE_NAME - # self._api_key = entity_info[ENTITY_API] self._class = entity_info[ENTITY_CLASS] self._enable_default = entity_info[ENTITY_ENABLE] self._icon = entity_info[ENTITY_ICON] @@ -363,11 +362,6 @@ def device_info(self) -> Dict[str, any]: "sw_version": self._api.information.version_string, } - # @property - # def api_key(self) -> str: - # """Return the api key.""" - # return self._api_key - @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" From 554ef4b88b4f3c7ce35d3e680e3c01ad6e9fd728 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 01:59:55 +0200 Subject: [PATCH 12/25] Remove security translates --- homeassistant/components/synology_dsm/strings.json | 3 +-- homeassistant/components/synology_dsm/translations/en.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index ca32e57d5fcf9b..0024a7db612a6b 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -44,8 +44,7 @@ "step": { "init": { "data": { - "scan_interval": "Minutes between scans", - "security": "Add security sensors" + "scan_interval": "Minutes between scans" } } } diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 3fb34a6acffe6d..48a8118528a04a 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -44,8 +44,7 @@ "step": { "init": { "data": { - "scan_interval": "Minutes between scans", - "security": "Add security sensors" + "scan_interval": "Minutes between scans" } } } From dba80512f49df40f76e647a89e7b6e2f17acf3b9 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 02:59:06 +0200 Subject: [PATCH 13/25] Add binary_sensor to coverage --- .coveragerc | 1 + homeassistant/components/synology_dsm/config_flow.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 5003e179b801c5..dca89bba12baea 100644 --- a/.coveragerc +++ b/.coveragerc @@ -757,6 +757,7 @@ omit = homeassistant/components/synology/camera.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py + homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_srm/device_tracker.py homeassistant/components/syslog/notify.py diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 01815920620366..a4d7d75e073692 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -250,7 +250,7 @@ async def async_step_init(self, user_input=None): default=self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), - ): cv.positive_int, + ): cv.positive_int } ) return self.async_show_form(step_id="init", data_schema=data_schema) From 678177476523be02d233a352c559ebb6c555441b Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 03:02:10 +0200 Subject: [PATCH 14/25] Review async_on_remove --- homeassistant/components/synology_dsm/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 17209e30e7310c..2175afa4bd8cdd 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -317,8 +317,6 @@ def __init__( self._name += f" {entity_info[ENTITY_NAME]}" - self._unsub_dispatcher = None - @property def unique_id(self) -> str: """Return a unique ID.""" @@ -381,10 +379,8 @@ async def async_update(self): async def async_added_to_hass(self): """Register state update callback.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, self._api.signal_sensor_update, self.async_write_ha_state + self.async_on_remove( + async_dispatcher_connect( + self.hass, self._api.signal_sensor_update, self.async_write_ha_state + ) ) - - async def async_will_remove_from_hass(self): - """Clean up after entity before removal.""" - self._unsub_dispatcher() From 9b862a3be1f3e7be444066db5b0fad669f0be334 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 23:13:48 +0200 Subject: [PATCH 15/25] Resolve rebase --- homeassistant/components/synology_dsm/__init__.py | 2 +- homeassistant/components/synology_dsm/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 2175afa4bd8cdd..cafd4b3dc3e41f 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -336,7 +336,7 @@ def icon(self) -> str: def unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" if self.entity_type in TEMP_SENSORS_KEYS: - return self._api.temp_unit + return self.hass.config.units.temperature_unit return self._unit @property diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 354978120516f2..e2b3cbf3fd93b0 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -98,7 +98,7 @@ def state(self): return round(attr / 1024.0 ** 4, 2) # Temperature - if self.sensor_type in TEMP_SENSORS_KEYS: + if self.entity_type in TEMP_SENSORS_KEYS: return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS) return attr From cef68ad10d98f5fdffc7bf7ffa4fe0f672366fed Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 13 May 2020 23:32:05 +0200 Subject: [PATCH 16/25] Add comment to _should_fetch_api --- homeassistant/components/synology_dsm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index cafd4b3dc3e41f..276ce568dba600 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -194,7 +194,7 @@ async def async_setup(self): ) async def _should_fetch_api(self): - + """Determine if we should fetch each API, if one entity needs it.""" entity_reg = await self._hass.helpers.entity_registry.async_get_registry() entity_entries = entity_registry.async_entries_for_config_entry( entity_reg, self._entry.entry_id From 3ade18a5ef99771fbcdac807a05c3eb6d76fa886 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Fri, 15 May 2020 01:02:03 +0200 Subject: [PATCH 17/25] Use API reset --- homeassistant/components/synology_dsm/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 276ce568dba600..acd963bbc85c3f 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -230,15 +230,15 @@ async def _should_fetch_api(self): # Reset not used API if not self._with_security: - self.dsm._security = None # pylint: disable=protected-access + self.dsm.reset(self.security) self.security = None if not self._with_storage: - self.dsm._storage = None # pylint: disable=protected-access + self.dsm.reset(self.storage) self.storage = None if not self._with_utilisation: - self.dsm._utilisation = None # pylint: disable=protected-access + self.dsm.reset(self.utilisation) self.utilisation = None def _fetch_device_configuration(self): From af715eb9d5fb2d29c9df1c098015aaaf4619050f Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 20 May 2020 19:50:46 +0200 Subject: [PATCH 18/25] entity unique_id migration --- .../components/synology_dsm/__init__.py | 68 +++++++++++++++++++ .../components/synology_dsm/binary_sensor.py | 24 ++++--- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index acd963bbc85c3f..a228805dc35938 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,6 +1,7 @@ """The Synology DSM component.""" import asyncio from datetime import timedelta +import logging from typing import Dict from synology_dsm import SynologyDSM @@ -22,6 +23,7 @@ CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import callback from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -44,9 +46,13 @@ ENTITY_NAME, ENTITY_UNIT, PLATFORMS, + STORAGE_DISK_BINARY_SENSORS, + STORAGE_DISK_SENSORS, + STORAGE_VOL_SENSORS, SYNO_API, TEMP_SENSORS_KEYS, UNDO_UPDATE_LISTENER, + UTILISATION_SENSORS, ) CONFIG_SCHEMA = vol.Schema( @@ -69,6 +75,9 @@ ATTRIBUTION = "Data provided by Synology" +_LOGGER = logging.getLogger(__name__) + + async def async_setup(hass, config): """Set up Synology DSM sensors from legacy config file.""" @@ -90,6 +99,65 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Synology DSM sensors.""" api = SynoApi(hass, entry) + # Migrate old unique_id + @callback + def _async_migrator(entity_entry: entity_registry.RegistryEntry): + """Migrate away from ID using label.""" + # Reject if new unique_id + if "SYNO." in entity_entry.unique_id: + return None + + entries = { + **STORAGE_DISK_BINARY_SENSORS, + **STORAGE_DISK_SENSORS, + **STORAGE_VOL_SENSORS, + **UTILISATION_SENSORS, + } + infos = entity_entry.unique_id.split("_") + serial = infos.pop(0) + label = infos.pop(0) + device_id = "_".join(infos) + + # Removed entity + if ( + "Type" in entity_entry.unique_id + or "Device" in entity_entry.unique_id + or "Name" in entity_entry.unique_id + ): + return None + + entity_type = None + for entity_key, entity_attrs in entries.items(): + if ( + device_id + and entity_attrs[ENTITY_NAME] == "Status" + and "Status" in entity_entry.unique_id + and "(Smart)" not in entity_entry.unique_id + ): + if "sd" in device_id and "disk" in entity_key: + entity_type = entity_key + continue + if "volume" in device_id and "volume" in entity_key: + entity_type = entity_key + continue + + if entity_attrs[ENTITY_NAME] == label: + entity_type = entity_key + + new_unique_id = "_".join([serial, entity_type]) + if device_id: + new_unique_id += f"_{device_id}" + + _LOGGER.info( + "Migrating unique_id from [%s] to [%s]", + entity_entry.unique_id, + new_unique_id, + ) + return {"new_unique_id": new_unique_id} + + await entity_registry.async_migrate_entries(hass, entry.entry_id, _async_migrator) + + # Continue setup await api.async_setup() undo_listener = entry.add_update_listener(_async_update_listener) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 92cd63960428cf..e9b5e43ddb77ab 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -22,15 +22,12 @@ async def async_setup_entry( api = hass.data[DOMAIN][entry.unique_id][SYNO_API] - entities = [] - - if api.security: - entities += [ - SynoDSMSecurityBinarySensor( - api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type] - ) - for sensor_type in SECURITY_BINARY_SENSORS - ] + entities = [ + SynoDSMSecurityBinarySensor( + api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type] + ) + for sensor_type in SECURITY_BINARY_SENSORS + ] # Handle all disks if api.storage.disks_ids: @@ -49,16 +46,21 @@ class SynoDSMSecurityBinarySensor(SynologyDSMEntity, BinarySensorEntity): """Representation a Synology Security binary sensor.""" @property - def is_on(self): + def is_on(self) -> bool: """Return the state.""" return getattr(self._api.security, self.entity_type) != "safe" + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._api.security + class SynoDSMStorageBinarySensor(SynologyDSMEntity, BinarySensorEntity): """Representation a Synology Storage binary sensor.""" @property - def is_on(self): + def is_on(self) -> bool: """Return the state.""" attr = getattr(self._api.storage, self.entity_type)(self._device_id) if attr is None: From c00a81444b8878905185f03d0b30d58b3b695e52 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 20 May 2020 23:32:56 +0200 Subject: [PATCH 19/25] SynologyDSMDeviceEntity for disk and volume + add available --- .../components/synology_dsm/__init__.py | 100 +++++++++++------- .../components/synology_dsm/binary_sensor.py | 18 +--- .../components/synology_dsm/sensor.py | 23 ++-- 3 files changed, 71 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a228805dc35938..272ae3fdd4af8a 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -337,53 +337,17 @@ class SynologyDSMEntity(Entity): """Representation of a Synology NAS entry.""" def __init__( - self, - api: SynoApi, - entity_type: str, - entity_info: Dict[str, str], - device_id: str = None, + self, api: SynoApi, entity_type: str, entity_info: Dict[str, str], ): """Initialize the Synology DSM entity.""" self._api = api self.entity_type = entity_type.split(":")[-1] - self._name = BASE_NAME + self._name = f"{BASE_NAME} {entity_info[ENTITY_NAME]}" self._class = entity_info[ENTITY_CLASS] self._enable_default = entity_info[ENTITY_ENABLE] self._icon = entity_info[ENTITY_ICON] self._unit = entity_info[ENTITY_UNIT] self._unique_id = f"{self._api.information.serial}_{entity_type}" - self._device_id = device_id - self._device_name = None - self._device_manufacturer = None - self._device_model = None - self._device_firmware = None - self._device_type = None - - if self._device_id: - if "volume" in entity_type: - volume = self._api.storage._get_volume(self._device_id) - # Volume does not have a name - self._device_name = volume["id"].replace("_", " ").capitalize() - self._device_manufacturer = "Synology" - self._device_model = self._api.information.model - self._device_firmware = self._api.information.version_string - self._device_type = ( - volume["device_type"] - .replace("_", " ") - .replace("raid", "RAID") - .replace("shr", "SHR") - ) - elif "disk" in entity_type: - disk = self._api.storage._get_disk(self._device_id) - self._device_name = disk["name"] - self._device_manufacturer = disk["vendor"] - self._device_model = disk["model"].strip() - self._device_firmware = disk["firm"] - self._device_type = disk["diskType"] - self._name += f" {self._device_name}" - self._unique_id += f"_{self._device_id}" - - self._name += f" {entity_info[ENTITY_NAME]}" @property def unique_id(self) -> str: @@ -452,3 +416,63 @@ async def async_added_to_hass(self): self.hass, self._api.signal_sensor_update, self.async_write_ha_state ) ) + + +class SynologyDSMDeviceEntity(SynologyDSMEntity): + """Representation of a Synology NAS disk or volume entry.""" + + def __init__( + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], + device_id: str = None, + ): + """Initialize the Synology DSM disk or volume entity.""" + super().__init__(api, entity_type, entity_info) + self._device_id = device_id + self._device_name = None + self._device_manufacturer = None + self._device_model = None + self._device_firmware = None + self._device_type = None + + if "volume" in entity_type: + volume = self._api.storage._get_volume(self._device_id) + # Volume does not have a name + self._device_name = volume["id"].replace("_", " ").capitalize() + self._device_manufacturer = "Synology" + self._device_model = self._api.information.model + self._device_firmware = self._api.information.version_string + self._device_type = ( + volume["device_type"] + .replace("_", " ") + .replace("raid", "RAID") + .replace("shr", "SHR") + ) + elif "disk" in entity_type: + disk = self._api.storage._get_disk(self._device_id) + self._device_name = disk["name"] + self._device_manufacturer = disk["vendor"] + self._device_model = disk["model"].strip() + self._device_firmware = disk["firm"] + self._device_type = disk["diskType"] + self._name = f"{BASE_NAME} {self._device_name} {entity_info[ENTITY_NAME]}" + self._unique_id += f"_{self._device_id}" + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._api.storage + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, + "name": f"Synology NAS ({self._device_name} {self._device_type})", + "manufacturer": self._device_manufacturer, + "model": self._device_model, + "sw_version": self._device_firmware, + "via_device": (DOMAIN, self._api.information.serial), + } diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index e9b5e43ddb77ab..d81135b3e551db 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,12 +1,10 @@ """Support for Synology DSM binary sensors.""" -from typing import Dict - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS from homeassistant.helpers.typing import HomeAssistantType -from . import SynologyDSMEntity +from . import SynologyDSMDeviceEntity, SynologyDSMEntity from .const import ( DOMAIN, SECURITY_BINARY_SENSORS, @@ -56,7 +54,7 @@ def available(self) -> bool: return self._api.security -class SynoDSMStorageBinarySensor(SynologyDSMEntity, BinarySensorEntity): +class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): """Representation a Synology Storage binary sensor.""" @property @@ -66,15 +64,3 @@ def is_on(self) -> bool: if attr is None: return None return attr - - @property - def device_info(self) -> Dict[str, any]: - """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, - "name": f"Synology NAS ({self._device_name} {self._device_type})", - "manufacturer": self._device_manufacturer, - "model": self._device_model, - "sw_version": self._device_firmware, - "via_device": (DOMAIN, self._api.information.serial), - } diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index e2b3cbf3fd93b0..9fac9398d04989 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -1,6 +1,4 @@ """Support for Synology DSM sensors.""" -from typing import Dict - from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DISKS, @@ -13,7 +11,7 @@ from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType -from . import SynologyDSMEntity +from . import SynologyDSMDeviceEntity, SynologyDSMEntity from .const import ( CONF_VOLUMES, DOMAIN, @@ -82,8 +80,13 @@ def state(self): return attr + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._api.utilisation + -class SynoDSMStorageSensor(SynologyDSMEntity): +class SynoDSMStorageSensor(SynologyDSMDeviceEntity): """Representation a Synology Storage sensor.""" @property @@ -102,15 +105,3 @@ def state(self): return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS) return attr - - @property - def device_info(self) -> Dict[str, any]: - """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, - "name": f"Synology NAS ({self._device_name} {self._device_type})", - "manufacturer": self._device_manufacturer, - "model": self._device_model, - "sw_version": self._device_firmware, - "via_device": (DOMAIN, self._api.information.serial), - } From f639d25568dee1f5f56a44fe46c044ef8287df33 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 27 May 2020 17:21:51 +0200 Subject: [PATCH 20/25] Review: minor changes --- homeassistant/components/synology_dsm/__init__.py | 8 ++++---- homeassistant/components/synology_dsm/binary_sensor.py | 2 +- homeassistant/components/synology_dsm/sensor.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 272ae3fdd4af8a..2205481d6a74bc 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -246,7 +246,7 @@ async def async_setup(self): device_token=self._entry.data.get("device_token"), ) - await self._should_fetch_api() + await self._async_should_fetch_api() await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.async_update() @@ -261,7 +261,7 @@ async def async_setup(self): ), ) - async def _should_fetch_api(self): + async def _async_should_fetch_api(self): """Determine if we should fetch each API, if one entity needs it.""" entity_reg = await self._hass.helpers.entity_registry.async_get_registry() entity_entries = entity_registry.async_entries_for_config_entry( @@ -328,7 +328,7 @@ async def async_unload(self): async def async_update(self, now=None): """Update function for updating API information.""" - await self._should_fetch_api() + await self._async_should_fetch_api() await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) @@ -463,7 +463,7 @@ def __init__( @property def available(self) -> bool: """Return True if entity is available.""" - return self._api.storage + return bool(self._api.storage) @property def device_info(self) -> Dict[str, any]: diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index d81135b3e551db..3dfc21b8a7b0ff 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -51,7 +51,7 @@ def is_on(self) -> bool: @property def available(self) -> bool: """Return True if entity is available.""" - return self._api.security + return bool(self._api.security) class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 9fac9398d04989..22171fdf2f59ef 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -83,7 +83,7 @@ def state(self): @property def available(self) -> bool: """Return True if entity is available.""" - return self._api.utilisation + return bool(self._api.utilisation) class SynoDSMStorageSensor(SynologyDSMDeviceEntity): From 6e0c771cf850e067d04a0c2274d9411be7ecfad5 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 1 Jun 2020 11:26:33 +0200 Subject: [PATCH 21/25] Review: subscribe to fetching, unsubscribe on async_on_remove --- .../components/synology_dsm/__init__.py | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 2205481d6a74bc..a17a636ccc5a34 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -224,9 +224,10 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): self.utilisation: SynoCoreUtilization = None # Should we fetch them - self._with_security = False - self._with_storage = False - self._with_utilisation = False + self.fetching_entities = set() + self._with_security = True + self._with_storage = True + self._with_utilisation = True self._unsub_dispatcher = None @@ -263,36 +264,26 @@ async def async_setup(self): async def _async_should_fetch_api(self): """Determine if we should fetch each API, if one entity needs it.""" - entity_reg = await self._hass.helpers.entity_registry.async_get_registry() - entity_entries = entity_registry.async_entries_for_config_entry( - entity_reg, self._entry.entry_id - ) - # Entities not added yet - if not entity_entries: - self._with_security = True - self._with_storage = True - self._with_utilisation = True + # Entities not added yet, fetch all + if not self.fetching_entities: return # Determine which if at least one entity uses specific API self._with_security = False self._with_storage = False self._with_utilisation = False - for entity_entry in entity_entries: - # Pass disabled entries - if entity_entry.disabled_by: - continue + for entity_unique_id in self.fetching_entities: # Check if we should fetch specific APIs - if SynoCoreSecurity.API_KEY in entity_entry.unique_id: + if SynoCoreSecurity.API_KEY in entity_unique_id: self._with_security = True continue - if SynoStorage.API_KEY in entity_entry.unique_id: + if SynoStorage.API_KEY in entity_unique_id: self._with_storage = True continue - if SynoCoreUtilization.API_KEY in entity_entry.unique_id: + if SynoCoreUtilization.API_KEY in entity_unique_id: self._with_utilisation = True continue @@ -417,6 +408,16 @@ async def async_added_to_hass(self): ) ) + # subscribe from API fetches + # HA reload the config when enable entities, it will subscribe disabled entities at this moment + self._api.fetching_entities.add(self._unique_id) + # unsubscribe with disable + self.async_on_remove(self._remove_fetch) + + @callback + def _remove_fetch(self): + self._api.fetching_entities.remove(self._unique_id) + class SynologyDSMDeviceEntity(SynologyDSMEntity): """Representation of a Synology NAS disk or volume entry.""" From e209c22191d0873adc3ebaf3140858a454de1962 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 2 Jun 2020 01:52:28 +0200 Subject: [PATCH 22/25] Review : Use dict to avoid loop --- .../components/synology_dsm/__init__.py | 33 +++++++------------ .../components/synology_dsm/const.py | 2 ++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a17a636ccc5a34..06ac71656330d2 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -224,7 +224,7 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): self.utilisation: SynoCoreUtilization = None # Should we fetch them - self.fetching_entities = set() + self.fetching_entities = {} self._with_security = True self._with_storage = True self._with_utilisation = True @@ -268,24 +268,12 @@ async def _async_should_fetch_api(self): if not self.fetching_entities: return - # Determine which if at least one entity uses specific API - self._with_security = False - self._with_storage = False - self._with_utilisation = False - - for entity_unique_id in self.fetching_entities: - # Check if we should fetch specific APIs - if SynoCoreSecurity.API_KEY in entity_unique_id: - self._with_security = True - continue - - if SynoStorage.API_KEY in entity_unique_id: - self._with_storage = True - continue - - if SynoCoreUtilization.API_KEY in entity_unique_id: - self._with_utilisation = True - continue + # Determine if we should fetch an API + self._with_security = bool(self.fetching_entities.get(SynoCoreSecurity.API_KEY)) + self._with_storage = bool(self.fetching_entities.get(SynoStorage.API_KEY)) + self._with_utilisation = bool( + self.fetching_entities.get(SynoCoreUtilization.API_KEY) + ) # Reset not used API if not self._with_security: @@ -332,6 +320,7 @@ def __init__( ): """Initialize the Synology DSM entity.""" self._api = api + self._api_key = entity_type.split(":")[0] self.entity_type = entity_type.split(":")[-1] self._name = f"{BASE_NAME} {entity_info[ENTITY_NAME]}" self._class = entity_info[ENTITY_CLASS] @@ -410,13 +399,15 @@ async def async_added_to_hass(self): # subscribe from API fetches # HA reload the config when enable entities, it will subscribe disabled entities at this moment - self._api.fetching_entities.add(self._unique_id) + if self._api_key not in self._api.fetching_entities: + self._api.fetching_entities[self._api_key] = set() + self._api.fetching_entities[self._api_key].add(self.unique_id) # unsubscribe with disable self.async_on_remove(self._remove_fetch) @callback def _remove_fetch(self): - self._api.fetching_entities.remove(self._unique_id) + self._api.fetching_entities[self._api_key].remove(self.unique_id) class SynologyDSMDeviceEntity(SynologyDSMEntity): diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 47c5ac5782d067..c525bec22298ca 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -36,6 +36,8 @@ ENTITY_CLASS = "device_class" ENTITY_ENABLE = "enable" +# Entity keys should start with the API_KEY to fetch + # Binary sensors STORAGE_DISK_BINARY_SENSORS = { f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": { From e385f3135df2e5a36a11421fafa0a46aa9d0f366 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 2 Jun 2020 02:14:59 +0200 Subject: [PATCH 23/25] Review: Use async_on_remove to subscribe and unsubscribe with callback --- .../components/synology_dsm/__init__.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 06ac71656330d2..f0ed7b9a5b47c1 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -224,7 +224,7 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): self.utilisation: SynoCoreUtilization = None # Should we fetch them - self.fetching_entities = {} + self._fetching_entities = {} self._with_security = True self._with_storage = True self._with_utilisation = True @@ -262,17 +262,33 @@ async def async_setup(self): ), ) + @callback + def subscribe(self, api_key, unique_id): + """Subscribe an entity from API fetches.""" + if api_key not in self._fetching_entities: + self._fetching_entities[api_key] = set() + self._fetching_entities[api_key].add(unique_id) + + @callback + def unsubscribe() -> None: + """Unsubscribe an entity from API fetches (when disable).""" + self._fetching_entities[api_key].remove(unique_id) + + return unsubscribe + async def _async_should_fetch_api(self): """Determine if we should fetch each API, if one entity needs it.""" # Entities not added yet, fetch all - if not self.fetching_entities: + if not self._fetching_entities: return # Determine if we should fetch an API - self._with_security = bool(self.fetching_entities.get(SynoCoreSecurity.API_KEY)) - self._with_storage = bool(self.fetching_entities.get(SynoStorage.API_KEY)) + self._with_security = bool( + self._fetching_entities.get(SynoCoreSecurity.API_KEY) + ) + self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY)) self._with_utilisation = bool( - self.fetching_entities.get(SynoCoreUtilization.API_KEY) + self._fetching_entities.get(SynoCoreUtilization.API_KEY) ) # Reset not used API @@ -397,17 +413,7 @@ async def async_added_to_hass(self): ) ) - # subscribe from API fetches - # HA reload the config when enable entities, it will subscribe disabled entities at this moment - if self._api_key not in self._api.fetching_entities: - self._api.fetching_entities[self._api_key] = set() - self._api.fetching_entities[self._api_key].add(self.unique_id) - # unsubscribe with disable - self.async_on_remove(self._remove_fetch) - - @callback - def _remove_fetch(self): - self._api.fetching_entities[self._api_key].remove(self.unique_id) + self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id)) class SynologyDSMDeviceEntity(SynologyDSMEntity): From c4bf893bb610ee2159bdad99dfca073ae0d34fd6 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 2 Jun 2020 08:41:07 +0200 Subject: [PATCH 24/25] Review: callback _async_setup_api_requests --- homeassistant/components/synology_dsm/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index f0ed7b9a5b47c1..bf01d084191b08 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -247,7 +247,7 @@ async def async_setup(self): device_token=self._entry.data.get("device_token"), ) - await self._async_should_fetch_api() + self._async_setup_api_requests() await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.async_update() @@ -276,7 +276,8 @@ def unsubscribe() -> None: return unsubscribe - async def _async_should_fetch_api(self): + @callback + def _async_setup_api_requests(self): """Determine if we should fetch each API, if one entity needs it.""" # Entities not added yet, fetch all if not self._fetching_entities: @@ -323,7 +324,7 @@ async def async_unload(self): async def async_update(self, now=None): """Update function for updating API information.""" - await self._async_should_fetch_api() + self._async_setup_api_requests() await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) From fad4d5ba4b12ee1ec87fc9ed41dfe5e9f0408363 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 2 Jun 2020 17:01:41 +0200 Subject: [PATCH 25/25] Add caret to separate device name and type --- homeassistant/components/synology_dsm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index bf01d084191b08..d4dbecf8f1cdd4 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -469,7 +469,7 @@ def device_info(self) -> Dict[str, any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, - "name": f"Synology NAS ({self._device_name} {self._device_type})", + "name": f"Synology NAS ({self._device_name} - {self._device_type})", "manufacturer": self._device_manufacturer, "model": self._device_model, "sw_version": self._device_firmware,