"""Support for QNAP NAS Sensors.""" from __future__ import annotations import logging import voluptuous as vol from homeassistant import config_entries from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import ( ATTR_NAME, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, CONF_VERIFY_SSL, PERCENTAGE, UnitOfDataRate, UnitOfInformation, UnitOfTemperature, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( CONF_DRIVES, CONF_NICS, CONF_VOLUMES, DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, ) from .coordinator import QnapCoordinator _LOGGER = logging.getLogger(__name__) ATTR_DRIVE = "Drive" ATTR_IP = "IP Address" ATTR_MAC = "MAC Address" ATTR_MASK = "Mask" ATTR_MAX_SPEED = "Max Speed" ATTR_MEMORY_SIZE = "Memory Size" ATTR_MODEL = "Model" ATTR_PACKETS_TX = "Packets (TX)" ATTR_PACKETS_RX = "Packets (RX)" ATTR_PACKETS_ERR = "Packets (Err)" ATTR_SERIAL = "Serial #" ATTR_TYPE = "Type" ATTR_UPTIME = "Uptime" ATTR_VOLUME_SIZE = "Volume Size" _SYSTEM_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="status", name="Status", icon="mdi:checkbox-marked-circle-outline", ), SensorEntityDescription( key="system_temp", name="System Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, icon="mdi:thermometer", state_class=SensorStateClass.MEASUREMENT, ), ) _CPU_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="cpu_temp", name="CPU Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, icon="mdi:checkbox-marked-circle-outline", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="cpu_usage", name="CPU Usage", native_unit_of_measurement=PERCENTAGE, icon="mdi:chip", state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=0, ), ) _MEMORY_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="memory_free", name="Memory Available", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:memory", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="memory_used", name="Memory Used", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:memory", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="memory_percent_used", name="Memory Usage", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=0, ), ) _NETWORK_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="network_link_status", name="Network Link", icon="mdi:checkbox-marked-circle-outline", ), SensorEntityDescription( key="network_tx", name="Network Up", native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, icon="mdi:upload", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), SensorEntityDescription( key="network_rx", name="Network Down", native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, icon="mdi:download", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), ) _DRIVE_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="drive_smart_status", name="SMART Status", icon="mdi:checkbox-marked-circle-outline", entity_registry_enabled_default=False, ), SensorEntityDescription( key="drive_temp", name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), ) _VOLUME_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="volume_size_used", name="Used Space", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:chart-pie", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="volume_size_free", name="Free Space", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:chart-pie", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="volume_percentage_used", name="Volume Used", native_unit_of_measurement=PERCENTAGE, icon="mdi:chart-pie", state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=0, ), ) SENSOR_KEYS: list[str] = [ desc.key for desc in ( *_SYSTEM_MON_COND, *_CPU_MON_COND, *_MEMORY_MON_COND, *_NETWORK_MON_COND, *_DRIVE_MON_COND, *_VOLUME_MON_COND, ) ] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] ), vol.Optional(CONF_NICS): cv.ensure_list, vol.Optional(CONF_DRIVES): cv.ensure_list, vol.Optional(CONF_VOLUMES): cv.ensure_list, } ) async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the qnap sensor platform from yaml.""" async_create_issue( hass, HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.12.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", translation_placeholders={ "domain": DOMAIN, "integration_title": "QNAP", }, ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) ) async def async_setup_entry( hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up entry.""" coordinator = QnapCoordinator(hass, config_entry) await coordinator.async_refresh() if not coordinator.last_update_success: raise PlatformNotReady uid = config_entry.unique_id assert uid is not None sensors: list[QNAPSensor] = [] sensors.extend( [ QNAPSystemSensor(coordinator, description, uid) for description in _SYSTEM_MON_COND ] ) sensors.extend( [QNAPCPUSensor(coordinator, description, uid) for description in _CPU_MON_COND] ) sensors.extend( [ QNAPMemorySensor(coordinator, description, uid) for description in _MEMORY_MON_COND ] ) # Network sensors sensors.extend( [ QNAPNetworkSensor(coordinator, description, uid, nic) for nic in coordinator.data["system_stats"]["nics"] for description in _NETWORK_MON_COND ] ) # Drive sensors sensors.extend( [ QNAPDriveSensor(coordinator, description, uid, drive) for drive in coordinator.data["smart_drive_health"] for description in _DRIVE_MON_COND ] ) # Volume sensors sensors.extend( [ QNAPVolumeSensor(coordinator, description, uid, volume) for volume in coordinator.data["volumes"] for description in _VOLUME_MON_COND ] ) async_add_entities(sensors) class QNAPSensor(CoordinatorEntity[QnapCoordinator], SensorEntity): """Base class for a QNAP sensor.""" def __init__( self, coordinator: QnapCoordinator, description: SensorEntityDescription, unique_id: str, monitor_device: str | None = None, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description self.device_name = self.coordinator.data["system_stats"]["system"]["name"] self.monitor_device = monitor_device self._attr_unique_id = f"{unique_id}_{description.key}" if monitor_device: self._attr_unique_id = f"{self._attr_unique_id}_{monitor_device}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, name=self.device_name, model=self.coordinator.data["system_stats"]["system"]["model"], sw_version=self.coordinator.data["system_stats"]["firmware"]["version"], manufacturer="QNAP", ) @property def name(self): """Return the name of the sensor, if any.""" if self.monitor_device is not None: return f"{self.device_name} {self.entity_description.name} ({self.monitor_device})" return f"{self.device_name} {self.entity_description.name}" class QNAPCPUSensor(QNAPSensor): """A QNAP sensor that monitors CPU stats.""" @property def native_value(self): """Return the state of the sensor.""" if self.entity_description.key == "cpu_temp": return self.coordinator.data["system_stats"]["cpu"]["temp_c"] if self.entity_description.key == "cpu_usage": return self.coordinator.data["system_stats"]["cpu"]["usage_percent"] class QNAPMemorySensor(QNAPSensor): """A QNAP sensor that monitors memory stats.""" @property def native_value(self): """Return the state of the sensor.""" free = float(self.coordinator.data["system_stats"]["memory"]["free"]) if self.entity_description.key == "memory_free": return free total = float(self.coordinator.data["system_stats"]["memory"]["total"]) used = total - free if self.entity_description.key == "memory_used": return used if self.entity_description.key == "memory_percent_used": return used / total * 100 @property def extra_state_attributes(self): """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["system_stats"]["memory"] size = round(float(data["total"]) / 1024, 2) return {ATTR_MEMORY_SIZE: f"{size} {UnitOfInformation.GIBIBYTES}"} class QNAPNetworkSensor(QNAPSensor): """A QNAP sensor that monitors network stats.""" @property def native_value(self): """Return the state of the sensor.""" if self.entity_description.key == "network_link_status": nic = self.coordinator.data["system_stats"]["nics"][self.monitor_device] return nic["link_status"] data = self.coordinator.data["bandwidth"][self.monitor_device] if self.entity_description.key == "network_tx": return data["tx"] if self.entity_description.key == "network_rx": return data["rx"] @property def extra_state_attributes(self): """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["system_stats"]["nics"][self.monitor_device] return { ATTR_IP: data["ip"], ATTR_MASK: data["mask"], ATTR_MAC: data["mac"], ATTR_MAX_SPEED: data["max_speed"], ATTR_PACKETS_ERR: data["err_packets"], } class QNAPSystemSensor(QNAPSensor): """A QNAP sensor that monitors overall system health.""" @property def native_value(self): """Return the state of the sensor.""" if self.entity_description.key == "status": return self.coordinator.data["system_health"] if self.entity_description.key == "system_temp": return int(self.coordinator.data["system_stats"]["system"]["temp_c"]) @property def extra_state_attributes(self): """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["system_stats"] days = int(data["uptime"]["days"]) hours = int(data["uptime"]["hours"]) minutes = int(data["uptime"]["minutes"]) return { ATTR_NAME: data["system"]["name"], ATTR_MODEL: data["system"]["model"], ATTR_SERIAL: data["system"]["serial_number"], ATTR_UPTIME: f"{days:0>2d}d {hours:0>2d}h {minutes:0>2d}m", } class QNAPDriveSensor(QNAPSensor): """A QNAP sensor that monitors HDD/SSD drive stats.""" @property def native_value(self): """Return the state of the sensor.""" data = self.coordinator.data["smart_drive_health"][self.monitor_device] if self.entity_description.key == "drive_smart_status": return data["health"] if self.entity_description.key == "drive_temp": return int(data["temp_c"]) if data["temp_c"] is not None else 0 @property def name(self): """Return the name of the sensor, if any.""" server_name = self.coordinator.data["system_stats"]["system"]["name"] return ( f"{server_name} {self.entity_description.name} (Drive" f" {self.monitor_device})" ) @property def extra_state_attributes(self): """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["smart_drive_health"][self.monitor_device] return { ATTR_DRIVE: data["drive_number"], ATTR_MODEL: data["model"], ATTR_SERIAL: data["serial"], ATTR_TYPE: data["type"], } class QNAPVolumeSensor(QNAPSensor): """A QNAP sensor that monitors storage volume stats.""" @property def native_value(self): """Return the state of the sensor.""" data = self.coordinator.data["volumes"][self.monitor_device] free_gb = int(data["free_size"]) if self.entity_description.key == "volume_size_free": return free_gb total_gb = int(data["total_size"]) used_gb = total_gb - free_gb if self.entity_description.key == "volume_size_used": return used_gb if self.entity_description.key == "volume_percentage_used": return used_gb / total_gb * 100 @property def extra_state_attributes(self): """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["volumes"][self.monitor_device] total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 return { ATTR_VOLUME_SIZE: f"{round(total_gb, 1)} {UnitOfInformation.GIBIBYTES}" }