diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 866fe3befa9..27c9b8b6078 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -1,5 +1,7 @@ """The Nextcloud integration.""" +import logging + from nextcloudmonitor import ( NextcloudMonitor, NextcloudMonitorAuthorizationError, @@ -17,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -26,10 +28,25 @@ PLATFORMS = (Platform.SENSOR, Platform.BINARY_SENSOR) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Nextcloud integration.""" + # migrate old entity unique ids + entity_reg = er.async_get(hass) + entities: list[er.RegistryEntry] = er.async_entries_for_config_entry( + entity_reg, entry.entry_id + ) + for entity in entities: + old_uid_start = f"{entry.data[CONF_URL]}#nextcloud_" + new_uid_start = f"{entry.data[CONF_URL]}#" + if entity.unique_id.startswith(old_uid_start): + new_uid = entity.unique_id.replace(old_uid_start, new_uid_start) + _LOGGER.debug("migrate unique id '%s' to '%s'", entity.unique_id, new_uid) + entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_uid) + def _connect_nc(): return NextcloudMonitor( entry.data[CONF_URL], diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index 3cf3cc3ae2a..5281342dc14 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -1,8 +1,14 @@ """Summary binary data from Nextcoud.""" from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from typing import Final + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -10,12 +16,28 @@ from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator from .entity import NextcloudEntity -BINARY_SENSORS = ( - "nextcloud_system_enable_avatars", - "nextcloud_system_enable_previews", - "nextcloud_system_filelocking.enabled", - "nextcloud_system_debug", -) +BINARY_SENSORS: Final[dict[str, BinarySensorEntityDescription]] = { + "system_debug": BinarySensorEntityDescription( + key="system_debug", + translation_key="nextcloud_system_debug", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "system_enable_avatars": BinarySensorEntityDescription( + key="system_enable_avatars", + translation_key="nextcloud_system_enable_avatars", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "system_enable_previews": BinarySensorEntityDescription( + key="system_enable_previews", + translation_key="nextcloud_system_enable_previews", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "system_filelocking.enabled": BinarySensorEntityDescription( + key="system_filelocking.enabled", + translation_key="nextcloud_system_filelocking_enabled", + entity_category=EntityCategory.DIAGNOSTIC, + ), +} async def async_setup_entry( @@ -25,7 +47,7 @@ async def async_setup_entry( coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - NextcloudBinarySensor(coordinator, name, entry) + NextcloudBinarySensor(coordinator, name, entry, BINARY_SENSORS[name]) for name in coordinator.data if name in BINARY_SENSORS ] @@ -38,4 +60,5 @@ class NextcloudBinarySensor(NextcloudEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.coordinator.data.get(self.item) == "yes" + val = self.coordinator.data.get(self.item) + return val is True or val == "yes" diff --git a/homeassistant/components/nextcloud/coordinator.py b/homeassistant/components/nextcloud/coordinator.py index 73a07a77e23..c721168e848 100644 --- a/homeassistant/components/nextcloud/coordinator.py +++ b/homeassistant/components/nextcloud/coordinator.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DEFAULT_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ class NextcloudDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): leaf = True result.update(self._get_data_points(value, key_path, leaf)) else: - result[f"{DOMAIN}_{key_path}{key}"] = value + result[f"{key_path}{key}"] = value leaf = False return result diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index 17d59fe6b29..8f3ec55beec 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -1,8 +1,10 @@ """Base entity for the Nextcloud integration.""" +from urllib.parse import urlparse + from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import slugify from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -12,19 +14,22 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]): """Base Nextcloud entity.""" _attr_has_entity_name = True - _attr_icon = "mdi:cloud" def __init__( - self, coordinator: NextcloudDataUpdateCoordinator, item: str, entry: ConfigEntry + self, + coordinator: NextcloudDataUpdateCoordinator, + item: str, + entry: ConfigEntry, + desc: EntityDescription, ) -> None: """Initialize the Nextcloud sensor.""" super().__init__(coordinator) self.item = item - self._attr_translation_key = slugify(item) self._attr_unique_id = f"{coordinator.url}#{item}" self._attr_device_info = DeviceInfo( - name="Nextcloud", - identifiers={(DOMAIN, entry.entry_id)}, - sw_version=coordinator.data.get("nextcloud_system_version"), configuration_url=coordinator.url, + identifiers={(DOMAIN, entry.entry_id)}, + name=urlparse(coordinator.url).netloc, + sw_version=coordinator.data.get("system_version"), ) + self.entity_description = desc diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index a5df872e084..a91efee2284 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -1,8 +1,16 @@ """Summary data from Nextcoud.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from datetime import UTC, datetime +from typing import Final, cast + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -11,51 +19,235 @@ from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator from .entity import NextcloudEntity -SENSORS = ( - "nextcloud_system_version", - "nextcloud_system_theme", - "nextcloud_system_memcache.local", - "nextcloud_system_memcache.distributed", - "nextcloud_system_memcache.locking", - "nextcloud_system_freespace", - "nextcloud_system_cpuload", - "nextcloud_system_mem_total", - "nextcloud_system_mem_free", - "nextcloud_system_swap_total", - "nextcloud_system_swap_free", - "nextcloud_system_apps_num_installed", - "nextcloud_system_apps_num_updates_available", - "nextcloud_system_apps_app_updates_calendar", - "nextcloud_system_apps_app_updates_contacts", - "nextcloud_system_apps_app_updates_tasks", - "nextcloud_system_apps_app_updates_twofactor_totp", - "nextcloud_storage_num_users", - "nextcloud_storage_num_files", - "nextcloud_storage_num_storages", - "nextcloud_storage_num_storages_local", - "nextcloud_storage_num_storages_home", - "nextcloud_storage_num_storages_other", - "nextcloud_shares_num_shares", - "nextcloud_shares_num_shares_user", - "nextcloud_shares_num_shares_groups", - "nextcloud_shares_num_shares_link", - "nextcloud_shares_num_shares_mail", - "nextcloud_shares_num_shares_room", - "nextcloud_shares_num_shares_link_no_password", - "nextcloud_shares_num_fed_shares_sent", - "nextcloud_shares_num_fed_shares_received", - "nextcloud_shares_permissions_3_1", - "nextcloud_server_webserver", - "nextcloud_server_php_version", - "nextcloud_server_php_memory_limit", - "nextcloud_server_php_max_execution_time", - "nextcloud_server_php_upload_max_filesize", - "nextcloud_database_type", - "nextcloud_database_version", - "nextcloud_activeUsers_last5minutes", - "nextcloud_activeUsers_last1hour", - "nextcloud_activeUsers_last24hours", -) +UNIT_OF_LOAD: Final[str] = "load" + +SENSORS: Final[dict[str, SensorEntityDescription]] = { + "activeUsers_last1hour": SensorEntityDescription( + key="activeUsers_last1hour", + translation_key="nextcloud_activeusers_last1hour", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:account-multiple", + ), + "activeUsers_last24hours": SensorEntityDescription( + key="activeUsers_last24hours", + translation_key="nextcloud_activeusers_last24hours", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:account-multiple", + ), + "activeUsers_last5minutes": SensorEntityDescription( + key="activeUsers_last5minutes", + translation_key="nextcloud_activeusers_last5minutes", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:account-multiple", + ), + "database_type": SensorEntityDescription( + key="database_type", + translation_key="nextcloud_database_type", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:database", + ), + "database_version": SensorEntityDescription( + key="database_version", + translation_key="nextcloud_database_version", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:database", + ), + "server_php_max_execution_time": SensorEntityDescription( + key="server_php_max_execution_time", + translation_key="nextcloud_server_php_max_execution_time", + device_class=SensorDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfTime.SECONDS, + ), + "server_php_memory_limit": SensorEntityDescription( + key="server_php_memory_limit", + translation_key="nextcloud_server_php_memory_limit", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.CONFIG, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + "server_php_upload_max_filesize": SensorEntityDescription( + key="server_php_upload_max_filesize", + translation_key="nextcloud_server_php_upload_max_filesize", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.CONFIG, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + "server_php_version": SensorEntityDescription( + key="server_php_version", + translation_key="nextcloud_server_php_version", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:language-php", + ), + "server_webserver": SensorEntityDescription( + key="server_webserver", + translation_key="nextcloud_server_webserver", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_fed_shares_sent": SensorEntityDescription( + key="shares_num_fed_shares_sent", + translation_key="nextcloud_shares_num_fed_shares_sent", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_fed_shares_received": SensorEntityDescription( + key="shares_num_fed_shares_received", + translation_key="nextcloud_shares_num_fed_shares_received", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares": SensorEntityDescription( + key="shares_num_shares", + translation_key="nextcloud_shares_num_shares", + ), + "shares_num_shares_groups": SensorEntityDescription( + key="shares_num_shares_groups", + translation_key="nextcloud_shares_num_shares_groups", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_link": SensorEntityDescription( + key="shares_num_shares_link", + translation_key="nextcloud_shares_num_shares_link", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_link_no_password": SensorEntityDescription( + key="shares_num_shares_link_no_password", + translation_key="nextcloud_shares_num_shares_link_no_password", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_mail": SensorEntityDescription( + key="shares_num_shares_mail", + translation_key="nextcloud_shares_num_shares_mail", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_room": SensorEntityDescription( + key="shares_num_shares_room", + translation_key="nextcloud_shares_num_shares_room", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_user": SensorEntityDescription( + key="server_num_shares_user", + translation_key="nextcloud_shares_num_shares_user", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_files": SensorEntityDescription( + key="storage_num_files", + translation_key="nextcloud_storage_num_files", + ), + "storage_num_storages": SensorEntityDescription( + key="storage_num_storages", + translation_key="nextcloud_storage_num_storages", + ), + "storage_num_storages_home": SensorEntityDescription( + key="storage_num_storages_home", + translation_key="nextcloud_storage_num_storages_home", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_storages_local": SensorEntityDescription( + key="storage_num_storages_local", + translation_key="nextcloud_storage_num_storages_local", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_storages_other": SensorEntityDescription( + key="storage_num_storages_other", + translation_key="nextcloud_storage_num_storages_other", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_users": SensorEntityDescription( + key="storage_num_users", + translation_key="nextcloud_storage_num_users", + ), + "system_apps_num_installed": SensorEntityDescription( + key="system_apps_num_installed", + translation_key="nextcloud_system_apps_num_installed", + ), + "system_apps_num_updates_available": SensorEntityDescription( + key="system_apps_num_updates_available", + translation_key="nextcloud_system_apps_num_updates_available", + icon="mdi:update", + ), + "system_cpuload": SensorEntityDescription( + key="system_cpuload", + translation_key="nextcloud_system_cpuload", + icon="mdi:chip", + ), + "system_freespace": SensorEntityDescription( + key="system_freespace", + translation_key="nextcloud_system_freespace", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:harddisk", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_mem_free": SensorEntityDescription( + key="system_mem_free", + translation_key="nextcloud_system_mem_free", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_mem_total": SensorEntityDescription( + key="system_mem_total", + translation_key="nextcloud_system_mem_total", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_memcache.distributed": SensorEntityDescription( + key="system_memcache.distributed", + translation_key="nextcloud_system_memcache_distributed", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "system_memcache.local": SensorEntityDescription( + key="system_memcache.local", + translation_key="nextcloud_system_memcache_local", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "system_memcache.locking": SensorEntityDescription( + key="system_memcache.locking", + translation_key="nextcloud_system_memcache_locking", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "system_swap_total": SensorEntityDescription( + key="system_swap_total", + translation_key="nextcloud_system_swap_total", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_swap_free": SensorEntityDescription( + key="system_swap_free", + translation_key="nextcloud_system_swap_free", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_theme": SensorEntityDescription( + key="system_theme", + translation_key="nextcloud_system_theme", + ), + "system_version": SensorEntityDescription( + key="system_version", + translation_key="nextcloud_system_version", + ), +} async def async_setup_entry( @@ -65,7 +257,7 @@ async def async_setup_entry( coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - NextcloudSensor(coordinator, name, entry) + NextcloudSensor(coordinator, name, entry, SENSORS[name]) for name in coordinator.data if name in SENSORS ] @@ -76,6 +268,12 @@ class NextcloudSensor(NextcloudEntity, SensorEntity): """Represents a Nextcloud sensor.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state for this sensor.""" - return self.coordinator.data.get(self.item) + val = self.coordinator.data.get(self.item) + if ( + getattr(self.entity_description, "device_class", None) + == SensorDeviceClass.TIMESTAMP + ): + return datetime.fromtimestamp(cast(int, val), tz=UTC) + return val