Refactor Synology entries to allow not fetching the API when it's disabled + add security binary sensor (#35565)

- add Synology DSM Security binary sensor (enabled by default)
- use device name instead of id in names
- add device type to name
- show disk manufacturer, model and firmware version in devices
- some entries are disabled by default (`entity_registry_enabled_default`)
- binary sensor + sensor uses `device_class` when possible
- do not fetch a concerned API if all entries of it are disabled
- entity unique_id now uses key instead of label
- entity entity_id changes for disk and volume: example from `sensor.synology_status_sda` to `sensor.synology_drive_1_status`, or from `sensor.synology_average_disk_temp_volume_1` to `sensor.synology_volume_1_average_disk_temp`
- 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])
This commit is contained in:
Quentame 2020-06-02 18:22:51 +02:00 committed by GitHub
parent 5d780ded29
commit 26cbca101a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 622 additions and 160 deletions

View File

@ -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

View File

@ -1,7 +1,11 @@
"""The Synology DSM component."""
import asyncio
from datetime import timedelta
import logging
from typing import Dict
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
@ -9,6 +13,7 @@ import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_DISKS,
CONF_HOST,
CONF_MAC,
@ -18,18 +23,36 @@ from homeassistant.const import (
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 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_VOLUMES,
DEFAULT_SCAN_INTERVAL,
DEFAULT_SSL,
DOMAIN,
ENTITY_CLASS,
ENTITY_ENABLE,
ENTITY_ICON,
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(
@ -49,6 +72,11 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
ATTRIBUTION = "Data provided by Synology"
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up Synology DSM sensors from legacy config file."""
@ -71,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)
@ -88,16 +175,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]
@ -121,10 +216,18 @@ class SynoApi:
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._fetching_entities = {}
self._with_security = True
self._with_storage = True
self._with_utilisation = True
self._unsub_dispatcher = None
@ -144,12 +247,14 @@ class SynoApi:
device_token=self._entry.data.get("device_token"),
)
self._async_setup_api_requests()
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
@ -157,17 +262,216 @@ class SynoApi:
),
)
@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
@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:
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_utilisation = bool(
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
)
# Reset not used API
if not self._with_security:
self.dsm.reset(self.security)
self.security = None
if not self._with_storage:
self.dsm.reset(self.storage)
self.storage = None
if not self._with_utilisation:
self.dsm.reset(self.utilisation)
self.utilisation = None
def _fetch_device_configuration(self):
"""Fetch initial device config."""
self.information = self.dsm.information
self.utilisation = self.dsm.utilisation
self.storage = self.dsm.storage
if self._with_security:
self.security = self.dsm.security
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."""
self._unsub_dispatcher()
async def update(self, now=None):
async def async_update(self, now=None):
"""Update function for updating API information."""
self._async_setup_api_requests()
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],
):
"""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]
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}"
@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.hass.config.units.temperature_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."""
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 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."""
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.async_on_remove(
async_dispatcher_connect(
self.hass, self._api.signal_sensor_update, self.async_write_ha_state
)
)
self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id))
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 bool(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),
}

View File

@ -0,0 +1,66 @@
"""Support for Synology DSM binary sensors."""
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 SynologyDSMDeviceEntity, SynologyDSMEntity
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 binary sensor."""
api = hass.data[DOMAIN][entry.unique_id][SYNO_API]
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:
for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids):
entities += [
SynoDSMStorageBinarySensor(
api, sensor_type, STORAGE_DISK_BINARY_SENSORS[sensor_type], disk
)
for sensor_type in STORAGE_DISK_BINARY_SENSORS
]
async_add_entities(entities)
class SynoDSMSecurityBinarySensor(SynologyDSMEntity, BinarySensorEntity):
"""Representation a Synology Security binary sensor."""
@property
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 bool(self._api.security)
class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity):
"""Representation a Synology Storage binary sensor."""
@property
def is_on(self) -> bool:
"""Return the state."""
attr = getattr(self._api.storage, self.entity_type)(self._device_id)
if attr is None:
return None
return attr

View File

@ -1,4 +1,9 @@
"""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,
@ -7,6 +12,8 @@ from homeassistant.const import (
)
DOMAIN = "synology_dsm"
PLATFORMS = ["binary_sensor", "sensor"]
BASE_NAME = "Synology"
# Entry keys
@ -15,47 +22,231 @@ UNDO_UPDATE_LISTENER = "undo_update_listener"
# Configuration
CONF_VOLUMES = "volumes"
DEFAULT_SSL = True
DEFAULT_PORT = 5000
DEFAULT_PORT_SSL = 5001
# Options
DEFAULT_SCAN_INTERVAL = 15 # min
ENTITY_NAME = "name"
ENTITY_UNIT = "unit"
ENTITY_ICON = "icon"
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": {
ENTITY_NAME: "Exceeded Max Bad Sectors",
ENTITY_UNIT: None,
ENTITY_ICON: "mdi:test-tube",
ENTITY_CLASS: None,
ENTITY_ENABLE: True,
},
f"{SynoStorage.API_KEY}: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 = {
f"{SynoCoreSecurity.API_KEY}: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"],
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,
},
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,
},
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,
},
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,
},
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,
},
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,
},
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,
},
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,
},
f"{SynoCoreUtilization.API_KEY}:memory_size": {
ENTITY_NAME: "Memory Size",
ENTITY_UNIT: DATA_MEGABYTES,
ENTITY_ICON: "mdi:memory",
ENTITY_CLASS: None,
ENTITY_ENABLE: False,
},
f"{SynoCoreUtilization.API_KEY}:memory_cached": {
ENTITY_NAME: "Memory Cached",
ENTITY_UNIT: DATA_MEGABYTES,
ENTITY_ICON: "mdi:memory",
ENTITY_CLASS: None,
ENTITY_ENABLE: False,
},
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,
},
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,
},
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,
},
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,
},
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,
},
f"{SynoCoreUtilization.API_KEY}: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_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"],
"volume_disk_temp_avg": ["Average Disk Temp", None, "mdi:thermometer"],
"volume_disk_temp_max": ["Maximum Disk Temp", None, "mdi:thermometer"],
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,
},
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,
},
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,
},
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,
},
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,
},
f"{SynoStorage.API_KEY}: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_name": ["Name", None, "mdi:harddisk"],
"disk_device": ["Device", None, "mdi:dots-horizontal"],
"disk_smart_status": ["Status (Smart)", None, "mdi:checkbox-marked-circle-outline"],
"disk_status": ["Status", None, "mdi:checkbox-marked-circle-outline"],
"disk_exceed_bad_sector_thr": ["Exceeded Max Bad Sectors", None, "mdi:test-tube"],
"disk_below_remain_life_thr": ["Below Min Remaining Life", None, "mdi:test-tube"],
"disk_temp": ["Temperature", None, "mdi:thermometer"],
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,
},
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,
},
f"{SynoStorage.API_KEY}:disk_temp": {
ENTITY_NAME: "Temperature",
ENTITY_UNIT: None,
ENTITY_ICON: "mdi:thermometer",
ENTITY_CLASS: "temperature",
ENTITY_ENABLE: True,
},
}

View File

@ -1,9 +1,6 @@
"""Support for Synology DSM Sensors."""
from typing import Dict
"""Support for Synology DSM sensors."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_DISKS,
DATA_MEGABYTES,
DATA_RATE_KILOBYTES_PER_SECOND,
@ -11,14 +8,11 @@ from homeassistant.const import (
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 SynologyDSMDeviceEntity, SynologyDSMEntity
from .const import (
BASE_NAME,
CONF_VOLUMES,
DOMAIN,
STORAGE_DISK_SENSORS,
@ -28,8 +22,6 @@ from .const import (
UTILISATION_SENSORS,
)
ATTRIBUTION = "Data provided by Synology"
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
@ -38,16 +30,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
]
# 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
@ -56,106 +48,23 @@ 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 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.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 SynoDSMUtilSensor(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:
@ -171,14 +80,19 @@ class SynoNasUtilSensor(SynoNasSensor):
return attr
@property
def available(self) -> bool:
"""Return True if entity is available."""
return bool(self._api.utilisation)
class SynoNasStorageSensor(SynoNasSensor):
class SynoDSMStorageSensor(SynologyDSMDeviceEntity):
"""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
@ -187,21 +101,7 @@ class SynoNasStorageSensor(SynoNasSensor):
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
@property
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,
"via_device": (DOMAIN, self._api.information.serial),
}