diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml new file mode 100644 index 0000000..13fda23 --- /dev/null +++ b/.github/workflows/hassfest.yml @@ -0,0 +1,23 @@ +name: Validate with hassfest + +on: + + push: + + pull_request: + + schedule: + + - cron: '0 0 * * *' + +jobs: + + validate: + + runs-on: "ubuntu-latest" + + steps: + + - uses: "actions/checkout@v3" + + - uses: "home-assistant/actions/hassfest@master" diff --git a/README.md b/README.md index b8cd215..045eb7b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ -# QNAP NAP Integration +# QNAP Integration Re-write with Config-Flow -This is a complete re-write of the Core QNAP integration, adding config_flow and unique_ID. Using this custom componant for development and testing before creating a PR and adding to Core. +This is a complete re-write of the Core QNAP integration, adding config_flow and unique_ID. Using this custom componant will override the built-in QNAP integration. +This integration has been submitted and accepted as a PR to Home Assistant Core. I will leave this custom_integration open, active, and in sync with the core version so I can continue to develop on it. Here are some upcoming features (that are not submitted to core). +* Folder entites +* Firmware update entity +* auto-discovery of device -## Looking for contributers, testers, and ideas... +## Installation + +Due to this repository overriding an existing core integration, it will need to be added as a custom repository. Goto HACS - Custom Integrations - three dots at the top - Custom Repositories. +Paste: https://github.com/disforw/qnap + + +NOTE: All folder and drive entities will be disabled by default. You will need to go to the entities you want and enable them manually. diff --git a/custom_components/qnap/__init__.py b/custom_components/qnap/__init__.py index eded8c8..14dab8c 100644 --- a/custom_components/qnap/__init__.py +++ b/custom_components/qnap/__init__.py @@ -1,97 +1,34 @@ """The qnap component.""" -from datetime import timedelta -import logging +from __future__ import annotations -from qnapstats import QNAPStats +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady -from homeassistant import config_entries -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_SSL, - CONF_USERNAME, - CONF_VERIFY_SSL, -) -from homeassistant.helpers import config_per_platform -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from .const import DOMAIN +from .coordinator import QnapCoordinator -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS +PLATFORMS: list[Platform] = [ + Platform.SENSOR, +] -UPDATE_INTERVAL = timedelta(minutes=1) -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass, config): - """Set up the qnap environment.""" - hass.data.setdefault(DOMAIN, {}) - - # Import configuration from sensor platform - config_platform = config_per_platform(config, "sensor") - for p_type, p_config in config_platform: - if p_type != DOMAIN: - continue - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=p_config, - ) - ) - - return True - - -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set the config entry up.""" - host = config_entry.data[CONF_HOST] - protocol = "https" if config_entry.data.get(CONF_SSL) else "http" - api = QNAPStats( - host=f"{protocol}://{host}", - port=config_entry.data.get(CONF_PORT, DEFAULT_PORT), - username=config_entry.data[CONF_USERNAME], - password=config_entry.data[CONF_PASSWORD], - verify_ssl=config_entry.data.get(CONF_VERIFY_SSL), - timeout=DEFAULT_TIMEOUT, - ) - - async def async_update_data(): - datas = {} - datas["system_stats"] = await hass.async_add_executor_job(api.get_system_stats) - datas["system_health"] = await hass.async_add_executor_job(api.get_system_health) - datas["smart_drive_health"] = await hass.async_add_executor_job(api.get_smart_disk_health) - datas["volumes"] = await hass.async_add_executor_job(api.get_volumes) - datas["bandwidth"] = await hass.async_add_executor_job(api.get_bandwidth) - return datas - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="sensor", - update_method=async_update_data, - update_interval=UPDATE_INTERVAL, - ) - + hass.data.setdefault(DOMAIN, {}) + coordinator = QnapCoordinator(hass, config_entry) # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][config_entry.entry_id] = coordinator - - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) - ) - + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( + if unload_ok := await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS - ) - if unload_ok: + ): hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/custom_components/qnap/config_flow.py b/custom_components/qnap/config_flow.py index 26e6375..6103327 100644 --- a/custom_components/qnap/config_flow.py +++ b/custom_components/qnap/config_flow.py @@ -1,12 +1,14 @@ """Config flow to configure qnap component.""" +from __future__ import annotations + import logging +from typing import Any from qnapstats import QNAPStats from requests.exceptions import ConnectTimeout import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.persistent_notification import create as notify_create from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -15,17 +17,24 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN +from .const import ( + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_TIMEOUT, + DEFAULT_VERIFY_SSL, + DOMAIN, +) DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, } ) @@ -38,27 +47,28 @@ class QnapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): - """Initialize.""" - self.is_imported = False - async def async_step_import(self, import_info): """Set the config entry up from yaml.""" - self.is_imported = True return await self.async_step_user(import_info) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: + user_input.setdefault(CONF_SSL, DEFAULT_SSL) + user_input.setdefault(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + user_input.setdefault(CONF_PORT, DEFAULT_PORT) host = user_input[CONF_HOST] - protocol = "https" if user_input.get(CONF_SSL, False) else "http" + protocol = "https" if user_input[CONF_SSL] else "http" api = QNAPStats( host=f"{protocol}://{host}", - port=user_input.get(CONF_PORT, DEFAULT_PORT), + port=user_input[CONF_PORT], username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], - verify_ssl=user_input.get(CONF_VERIFY_SSL, True), + verify_ssl=user_input[CONF_VERIFY_SSL], timeout=DEFAULT_TIMEOUT, ) try: @@ -75,11 +85,6 @@ async def async_step_user(self, user_input=None): await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() title = stats["system"]["name"].capitalize() - if self.is_imported: - _LOGGER.warning( - "The import of the QNAP configuration was successful. \ - Please remove the platform from the YAML configuration file" - ) return self.async_create_entry(title=title, data=user_input) return self.async_show_form( diff --git a/custom_components/qnap/const.py b/custom_components/qnap/const.py index 366ffba..babba8e 100644 --- a/custom_components/qnap/const.py +++ b/custom_components/qnap/const.py @@ -1,210 +1,12 @@ """The Qnap constants.""" -from __future__ import annotations -from dataclasses import dataclass - -from homeassistant.components.sensor import ( - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - TEMP_CELSIUS, - SensorEntityDescription, -) -from homeassistant.const import ( - DATA_GIBIBYTES, - DATA_RATE_MEBIBYTES_PER_SECOND, - PERCENTAGE, -) - -ATTR_DRIVE = "Drive" -ATTR_ENABLED = "Sensor Enabled" -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" - -PLATFORMS = ["sensor"] DEFAULT_NAME = "QNAP" DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 5 -DOMAIN = "qnap" +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True +DOMAIN = "qnap" NOTIFICATION_ID = "qnap_notification" NOTIFICATION_TITLE = "QNAP Sensor Setup" VOLUME_NAME = "volume" - - -@dataclass -class QNapSensorEntityDescription(SensorEntityDescription): - """Represents an Flow Sensor.""" - - stype: str | None = None - - -SENSOR_TYPES: tuple[QNapSensorEntityDescription, ...] = ( - QNapSensorEntityDescription( - stype="basic", - key="status", - name="Health", - icon="mdi:checkbox-marked-circle-outline", - entity_registry_enabled_default=True, - ), - QNapSensorEntityDescription( - stype="basic", - key="system_temp", - name="System Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - icon="mdi:thermometer", - entity_registry_enabled_default=True, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="cpu", - key="cpu_temp", - name="CPU Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - icon="mdi:checkbox-marked-circle-outline", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="cpu", - key="cpu_usage", - name="CPU Usage", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:chip", - entity_registry_enabled_default=True, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="memory", - key="memory_free", - name="Memory Available", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:memory", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="memory", - key="memory_used", - name="Memory Used", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:memory", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="memory", - key="memory_percent_used", - name="Memory Usage", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - entity_registry_enabled_default=True, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="network", - key="network_link_status", - name="Network Link", - icon="mdi:checkbox-marked-circle-outline", - entity_registry_enabled_default=True, - ), - QNapSensorEntityDescription( - stype="network", - key="network_tx", - name="Network Up", - native_unit_of_measurement=DATA_RATE_MEBIBYTES_PER_SECOND, - icon="mdi:upload", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="network", - key="network_rx", - name="Network Down", - native_unit_of_measurement=DATA_RATE_MEBIBYTES_PER_SECOND, - icon="mdi:download", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="drive", - key="drive_smart_status", - name="SMART Status", - icon="mdi:checkbox-marked-circle-outline", - entity_registry_enabled_default=False, - ), - QNapSensorEntityDescription( - stype="drive", - key="drive_temp", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="folder", - key="folder_size_used", - name="Used Space", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:chart-pie", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="folder", - key="folder_percentage_used", - name="Folder Used", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:chart-pie", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="volume", - key="volume_size_used", - name="Used Space", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:chart-pie", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="volume", - key="volume_size_free", - name="Free Space", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:chart-pie", - entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - ), - QNapSensorEntityDescription( - stype="volume", - key="volume_percentage_used", - name="Volume Used", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:chart-pie", - entity_registry_enabled_default=True, - state_class=STATE_CLASS_MEASUREMENT, - ), -) - -BAS_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "basic"] -CPU_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "cpu"] -MEM_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "memory"] -NET_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "network"] -DRI_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "drive"] -FOL_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "folder"] -VOL_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "volume"] diff --git a/custom_components/qnap/coordinator.py b/custom_components/qnap/coordinator.py new file mode 100644 index 0000000..8738ed6 --- /dev/null +++ b/custom_components/qnap/coordinator.py @@ -0,0 +1,59 @@ +"""Data coordinator for the qnap integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +from qnapstats import QNAPStats + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_TIMEOUT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +UPDATE_INTERVAL = timedelta(minutes=1) + +_LOGGER = logging.getLogger(__name__) + + +class QnapCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]): + """Custom coordinator for the qnap integration.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize the qnap coordinator.""" + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + + protocol = "https" if config_entry.data[CONF_SSL] else "http" + self._api = QNAPStats( + f"{protocol}://{config_entry.data.get(CONF_HOST)}", + config_entry.data.get(CONF_PORT), + config_entry.data.get(CONF_USERNAME), + config_entry.data.get(CONF_PASSWORD), + verify_ssl=config_entry.data.get(CONF_VERIFY_SSL), + timeout=config_entry.data.get(CONF_TIMEOUT), + ) + + def _sync_update(self) -> dict[str, dict[str, Any]]: + """Get the latest data from the Qnap API.""" + return { + "system_stats": self._api.get_system_stats(), + "system_health": self._api.get_system_health(), + "smart_drive_health": self._api.get_smart_disk_health(), + "volumes": self._api.get_volumes(), + "bandwidth": self._api.get_bandwidth(), + } + + async def _async_update_data(self) -> dict[str, dict[str, Any]]: + """Get the latest data from the Qnap API.""" + return await self.hass.async_add_executor_job(self._sync_update) diff --git a/custom_components/qnap/manifest.json b/custom_components/qnap/manifest.json index c17f98d..24fabc2 100644 --- a/custom_components/qnap/manifest.json +++ b/custom_components/qnap/manifest.json @@ -1,12 +1,12 @@ { "domain": "qnap", "name": "QNAP", + "codeowners": ["@disforw"], "config_flow": true, - "version": "10.0", - "dependencies": ["persistent_notification"], "documentation": "https://www.home-assistant.io/integrations/qnap", - "requirements": ["qnapstats==0.4.0"], - "codeowners": ["@colinodell", "@disforw"], + "integration_type": "device", "iot_class": "local_polling", - "integration_type": "device" + "issue_tracker": "https://github.com/disforw/inverse_switch/issues", + "requirements": ["qnapstats==0.4.0"], + "version": "10.0" } diff --git a/custom_components/qnap/sensor.bak b/custom_components/qnap/sensor.bak new file mode 100644 index 0000000..86dfb27 --- /dev/null +++ b/custom_components/qnap/sensor.bak @@ -0,0 +1,519 @@ +"""Support for QNAP NAS Sensors.""" +"""This backup includes the folder sensors""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from dataclasses import dataclass +from homeassistant.components.sensor import ( + SensorEntity, + SensorDeviceClass, + SensorStateClass, + SensorEntityDescription, +) +from homeassistant.const import ( + ATTR_NAME, + PERCENTAGE, + UnitOfTemperature, + UnitOfInformation, + UnitOfDataRate, +) + +from .const import ( + ATTR_DRIVE, + ATTR_IP, + ATTR_MAC, + ATTR_MASK, + ATTR_MAX_SPEED, + ATTR_MEMORY_SIZE, + ATTR_MODEL, + ATTR_PACKETS_ERR, + ATTR_PACKETS_RX, + ATTR_PACKETS_TX, + ATTR_SERIAL, + ATTR_TYPE, + ATTR_UPTIME, + ATTR_VOLUME_SIZE, + DEFAULT_NAME, + DOMAIN, + VOLUME_NAME, +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback +) -> None: + """Set up entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + uid = config_entry.unique_id + sensors: list[QNAPSensor] = [] + + sensors.extend( + [QNAPSystemSensor(coordinator, description, uid) for description in BAS_SENSOR] + ) + + sensors.extend( + [QNAPCPUSensor(coordinator, description, uid) for description in CPU_SENSOR] + ) + + sensors.extend( + [QNAPMemorySensor(coordinator, description, uid) for description in MEM_SENSOR] + ) + + # Network sensors + sensors.extend( + [ + QNAPNetworkSensor(coordinator, description, uid, nic) + for nic in coordinator.data["system_stats"]["nics"].keys() + for description in NET_SENSOR + ] + ) + + # Drive sensors + sensors.extend( + [ + QNAPDriveSensor(coordinator, description, uid, drive) + for drive in coordinator.data["smart_drive_health"].keys() + for description in DRI_SENSOR + ] + ) + + # Volume sensors + sensors.extend( + [ + QNAPVolumeSensor(coordinator, description, uid, volume) + for volume in coordinator.data["volumes"].keys() + for description in VOL_SENSOR + ] + ) + + # Folders sensors + sensors.extend( + [ + QNAPFolderSensor(coordinator, description, uid, volume, folder["sharename"]) + for volume in coordinator.data["volumes"].keys() + for folder in coordinator.data["volumes"][volume].get("folders", []) + for description in FOL_SENSOR + ] + ) + async_add_entities(sensors) + + +def round_nicely(number): + """Round a number based on its size (so it looks nice).""" + if number < 10: + return round(number, 2) + if number < 100: + return round(number, 1) + + return round(number) + + +@dataclass +class QNapSensorEntityDescription(SensorEntityDescription): + """Represents an Flow Sensor.""" + + stype: str | None = None + + +SENSOR_TYPES: tuple[QNapSensorEntityDescription, ...] = ( + QNapSensorEntityDescription( + stype="basic", + key="status", + name="Health", + icon="mdi:checkbox-marked-circle-outline", + ), + QNapSensorEntityDescription( + stype="basic", + key="system_temp", + name="System Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + icon="mdi:thermometer", + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="cpu", + 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, + ), + QNapSensorEntityDescription( + stype="cpu", + key="cpu_usage", + name="CPU Usage", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chip", + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="memory", + key="memory_free", + name="Memory Available", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:memory", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="memory", + key="memory_used", + name="Memory Used", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:memory", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="memory", + key="memory_percent_used", + name="Memory Usage", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="network", + key="network_link_status", + name="Network Link", + icon="mdi:checkbox-marked-circle-outline", + ), + QNapSensorEntityDescription( + stype="network", + key="network_tx", + name="Network Up", + native_unit_of_measurement=UnitOfDataRate.MEBIBYTES_PER_SECOND, + icon="mdi:upload", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="network", + key="network_rx", + name="Network Down", + native_unit_of_measurement=UnitOfDataRate.MEBIBYTES_PER_SECOND, + icon="mdi:download", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="drive", + key="drive_smart_status", + name="SMART Status", + icon="mdi:checkbox-marked-circle-outline", + entity_registry_enabled_default=False, + ), + QNapSensorEntityDescription( + stype="drive", + key="drive_temp", + name="Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + icon="mdi:thermometer", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="folder", + key="folder_size_used", + name="Used Space", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:chart-pie", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="folder", + key="folder_percentage_used", + name="Folder Used", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-pie", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="volume", + key="volume_size_used", + name="Used Space", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:chart-pie", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="volume", + key="volume_size_free", + name="Free Space", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:chart-pie", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + QNapSensorEntityDescription( + stype="volume", + key="volume_percentage_used", + name="Volume Used", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-pie", + state_class=SensorStateClass.MEASUREMENT, + ), +) +BAS_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "basic"] +CPU_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "cpu"] +MEM_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "memory"] +NET_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "network"] +DRI_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "drive"] +FOL_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "folder"] +VOL_SENSOR = [desc for desc in SENSOR_TYPES if desc.stype == "volume"] + +class QNAPSensor(CoordinatorEntity, SensorEntity): + """Base class for a QNAP sensor.""" + + def __init__( + self, coordinator, description, uid, monitor_device=None, monitor_subdevice=None + ) -> None: + """Initialize the sensor.""" + self.coordinator = coordinator + self.entity_description = description + self.uid = uid + self.device_name = self.coordinator.data["system_stats"]["system"]["name"] + self.monitor_device = monitor_device + self.monitor_subdevice = monitor_subdevice + + @property + def unique_id(self): + """Return unique_id.""" + return f"{self.uid}_{self.name}" + + @property + def coordinator_context(self): + """helpers update_coordinator""" + return None + + @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.monitor_device} - {self.entity_description.name}" + return f"{self.device_name} {self.entity_description.name}" + + @property + def device_info(self): + """Return device information.""" + return { + "identifiers": {(DOMAIN, self.uid)}, + "name": self.device_name, + "model": self.coordinator.data["system_stats"]["system"]["model"], + "sw_version": self.coordinator.data["system_stats"]["firmware"]["version"], + "manufacturer": DEFAULT_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"]) / 1024 + if self.entity_description.key == "memory_free": + return round_nicely(free) + + total = float(self.coordinator.data["system_stats"]["memory"]["total"]) / 1024 + + used = total - free + if self.entity_description.key == "memory_used": + return round_nicely(used) + + if self.entity_description.key == "memory_percent_used": + return round(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_nicely(float(data["total"]) / 1024) + return {ATTR_MEMORY_SIZE: f"{size} {UnitOfInformation.GIBIBYTES}"} + + +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 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 round_nicely(data["tx"] / 1024 / 1024) + + if self.entity_description.key == "network_rx": + return round_nicely(data["rx"] / 1024 / 1024) + + @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_TX: data["tx_packets"], + ATTR_PACKETS_RX: data["rx_packets"], + ATTR_PACKETS_ERR: data["err_packets"], + } + + +class QNAPDriveSensor(QNAPSensor): + """A QNAP sensor that monitors HDD/SSD drive stats.""" + + @property + def name(self): + """Return the name of the sensor, if any.""" + return f"{self.device_name} Drive {self.monitor_device} - {self.entity_description.name}" + + @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 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"]) / 1024 / 1024 / 1024 + if self.entity_description.key == "volume_size_free": + return round_nicely(free_gb) + + total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 + + used_gb = total_gb - free_gb + if self.entity_description.key == "volume_size_used": + return round_nicely(used_gb) + + if self.entity_description.key == "volume_percentage_used": + return round(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_nicely(total_gb)} {UnitOfInformation.GIBIBYTES}"} + + +class QNAPFolderSensor(QNAPSensor): + """A QNAP sensor that monitors storage folder stats.""" + + @property + def name(self): + """Return the name of the sensor, if any.""" + return f"{self.device_name} Folder {self.monitor_subdevice} - {self.entity_description.name}" + + @property + def native_value(self): + """Return the state of the sensor.""" + for folder in self.coordinator.data["volumes"][self.monitor_device]["folders"]: + if folder["sharename"] == self.monitor_subdevice: + vol = self.coordinator.data["volumes"][self.monitor_device] + used_gb = int(folder["used_size"]) / 1024 / 1024 / 1024 + total_gb = int(vol["total_size"]) / 1024 / 1024 / 1024 + + if self.entity_description.key == "folder_size_used": + return round_nicely(used_gb) + + if self.entity_description.key == "folder_percentage_used": + return round(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 + volume_name = self.monitor_device + + return { + ATTR_VOLUME_SIZE: f"{round_nicely(total_gb)} {UnitOfInformation.GIBIBYTES}", + VOLUME_NAME: volume_name, + } diff --git a/custom_components/qnap/sensor.py b/custom_components/qnap/sensor.py index 9e590e6..1bbd029 100644 --- a/custom_components/qnap/sensor.py +++ b/custom_components/qnap/sensor.py @@ -1,71 +1,211 @@ - """Support for QNAP NAS Sensors.""" +from __future__ import annotations + import logging -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ATTR_NAME, DATA_GIBIBYTES -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant import config_entries +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_NAME, + PERCENTAGE, + UnitOfDataRate, + UnitOfInformation, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from .const import ( - ATTR_DRIVE, - ATTR_IP, - ATTR_MAC, - ATTR_MASK, - ATTR_MAX_SPEED, - ATTR_MEMORY_SIZE, - ATTR_MODEL, - ATTR_PACKETS_ERR, - ATTR_PACKETS_RX, - ATTR_PACKETS_TX, - ATTR_SERIAL, - ATTR_TYPE, - ATTR_UPTIME, - ATTR_VOLUME_SIZE, - BAS_SENSOR, - CPU_SENSOR, - DEFAULT_NAME, - DOMAIN, - DRI_SENSOR, - FOL_SENSOR, - MEM_SENSOR, - NET_SENSOR, - VOL_SENSOR, - VOLUME_NAME, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import QnapCoordinator _LOGGER = logging.getLogger(__name__) +ATTR_DRIVE = "Drive" +ATTR_ENABLED = "Sensor Enabled" +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" + + +_SYS_SENSORS: 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_SENSORS: 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, + ), +) +_MEM_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="memory_free", + name="Memory Available", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:memory", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="memory_used", + name="Memory Used", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:memory", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="memory_percent_used", + name="Memory Usage", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), +) +_NET_SENSORS: 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.MEBIBYTES_PER_SECOND, + icon="mdi:upload", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="network_rx", + name="Network Down", + native_unit_of_measurement=UnitOfDataRate.MEBIBYTES_PER_SECOND, + icon="mdi:download", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), +) +_DRI_SENSORS: 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, + icon="mdi:thermometer", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), +) +_VOL_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="volume_size_used", + name="Used Space", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:chart-pie", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="volume_size_free", + name="Free Space", + native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + icon="mdi:chart-pie", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="volume_percentage_used", + name="Volume Used", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-pie", + state_class=SensorStateClass.MEASUREMENT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigType, - async_add_entities: AddEntitiesCallback + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = QnapCoordinator(hass, config_entry) + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise PlatformNotReady uid = config_entry.unique_id sensors: list[QNAPSensor] = [] sensors.extend( - [QNAPSystemSensor(coordinator, description, uid) for description in BAS_SENSOR] + [ + QNAPSystemSensor(coordinator, description, uid) + for description in _SYS_SENSORS + ] ) sensors.extend( - [QNAPCPUSensor(coordinator, description, uid) for description in CPU_SENSOR] + [QNAPCPUSensor(coordinator, description, uid) for description in _CPU_SENSORS] ) sensors.extend( - [QNAPMemorySensor(coordinator, description, uid) for description in MEM_SENSOR] + [ + QNAPMemorySensor(coordinator, description, uid) + for description in _MEM_SENSORS + ] ) # Network sensors sensors.extend( [ QNAPNetworkSensor(coordinator, description, uid, nic) - for nic in coordinator.data["system_stats"]["nics"].keys() - for description in NET_SENSOR + for nic in coordinator.data["system_stats"]["nics"] + for description in _NET_SENSORS ] ) @@ -73,8 +213,8 @@ async def async_setup_entry( sensors.extend( [ QNAPDriveSensor(coordinator, description, uid, drive) - for drive in coordinator.data["smart_drive_health"].keys() - for description in DRI_SENSOR + for drive in coordinator.data["smart_drive_health"] + for description in _DRI_SENSORS ] ) @@ -82,18 +222,8 @@ async def async_setup_entry( sensors.extend( [ QNAPVolumeSensor(coordinator, description, uid, volume) - for volume in coordinator.data["volumes"].keys() - for description in VOL_SENSOR - ] - ) - - # Folders sensors - sensors.extend( - [ - QNAPFolderSensor(coordinator, description, uid, volume, folder["sharename"]) - for volume in coordinator.data["volumes"].keys() - for folder in coordinator.data["volumes"][volume].get("folders", []) - for description in FOL_SENSOR + for volume in coordinator.data["volumes"] + for description in _VOL_SENSORS ] ) async_add_entities(sensors) @@ -109,47 +239,42 @@ def round_nicely(number): return round(number) -class QNAPSensor(CoordinatorEntity, SensorEntity): +class QNAPSensor(CoordinatorEntity[QnapCoordinator], SensorEntity): """Base class for a QNAP sensor.""" def __init__( - self, coordinator, description, uid, monitor_device=None, monitor_subdevice=None - ): + self, + coordinator: QnapCoordinator, + description, + uid, + monitor_device: str | None = None, + monitor_subdevice: str | None = None, + ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self.coordinator = coordinator self.entity_description = description self.uid = uid self.device_name = self.coordinator.data["system_stats"]["system"]["name"] self.monitor_device = monitor_device self.monitor_subdevice = monitor_subdevice + self.coordinator_context=None + self._attr_unique_id = f"{self.uid}_{self.device_name}_{self.name}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.uid)}, + name=self.device_name, + model=self.coordinator.data["system_stats"]["system"]["model"], + sw_version=self.coordinator.data["system_stats"]["firmware"]["version"], + manufacturer=DEFAULT_NAME, + ) - @property - def unique_id(self): - """Return unique_id.""" - return f"{self.uid}_{self.name}" - - @property - def coordinator_context(self): - return None - @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.monitor_device} - {self.entity_description.name}" + return f"{self.device_name} {self.entity_description.name} ({self.monitor_device})" return f"{self.device_name} {self.entity_description.name}" - @property - def device_info(self): - """Return device information.""" - return { - "identifiers": {(DOMAIN, self.uid)}, - "name": self.device_name, - "model": self.coordinator.data["system_stats"]["system"]["model"], - "sw_version": self.coordinator.data["system_stats"]["firmware"]["version"], - "manufacturer": DEFAULT_NAME, - } - class QNAPCPUSensor(QNAPSensor): """A QNAP sensor that monitors CPU stats.""" @@ -188,36 +313,7 @@ def extra_state_attributes(self): if self.coordinator.data: data = self.coordinator.data["system_stats"]["memory"] size = round_nicely(float(data["total"]) / 1024) - return {ATTR_MEMORY_SIZE: f"{size} {DATA_GIBIBYTES}"} - - -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", - } + return {ATTR_MEMORY_SIZE: f"{size} {UnitOfInformation.GIBIBYTES}"} class QNAPNetworkSensor(QNAPSensor): @@ -253,6 +349,35 @@ def extra_state_attributes(self): } +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.""" @@ -313,41 +438,6 @@ def extra_state_attributes(self): data = self.coordinator.data["volumes"][self.monitor_device] total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 - return {ATTR_VOLUME_SIZE: f"{round_nicely(total_gb)} {DATA_GIBIBYTES}"} - - -class QNAPFolderSensor(QNAPSensor): - """A QNAP sensor that monitors storage folder stats.""" - - @property - def name(self): - """Return the name of the sensor, if any.""" - return f"{self.device_name} Folder {self.monitor_subdevice} - {self.entity_description.name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - for folder in self.coordinator.data["volumes"][self.monitor_device]["folders"]: - if folder["sharename"] == self.monitor_subdevice: - vol = self.coordinator.data["volumes"][self.monitor_device] - used_gb = int(folder["used_size"]) / 1024 / 1024 / 1024 - total_gb = int(vol["total_size"]) / 1024 / 1024 / 1024 - - if self.entity_description.key == "folder_size_used": - return round_nicely(used_gb) - - if self.entity_description.key == "folder_percentage_used": - return round(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 - volume_name = self.monitor_device - return { - ATTR_VOLUME_SIZE: f"{round_nicely(total_gb)} {DATA_GIBIBYTES}", - VOLUME_NAME: volume_name, + ATTR_VOLUME_SIZE: f"{round_nicely(total_gb)} {UnitOfInformation.GIBIBYTES}" } diff --git a/hacs.json b/hacs.json index 73993c7..21c444f 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,5 @@ { "name": "Qnap", - "domains": ["sensor"], "homeassistant": "2021.5", "render_readme": true, - "iot_class": "Local Push" }