mirror of https://github.com/home-assistant/core
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:
parent
5d780ded29
commit
26cbca101a
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue