diff --git a/homeassistant/components/librenms/__init__.py b/homeassistant/components/librenms/__init__.py index 6ec24f2f3ad115..acd4e75d356bc6 100644 --- a/homeassistant/components/librenms/__init__.py +++ b/homeassistant/components/librenms/__init__.py @@ -5,7 +5,7 @@ from .coordinator import LibrenmsConfigEntry, LibrenmsDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: LibrenmsConfigEntry) -> bool: diff --git a/homeassistant/components/librenms/entity.py b/homeassistant/components/librenms/entity.py index 152d475df03aa9..906e611bad6013 100644 --- a/homeassistant/components/librenms/entity.py +++ b/homeassistant/components/librenms/entity.py @@ -1,10 +1,10 @@ -"""Base entity for the LibreNMS integration.""" +"""Base entities for the LibreNMS integration.""" from typing import override from aiolibrenms.devices.models import LibrenmsDeviceInfo -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -53,3 +53,25 @@ def available(self) -> bool: def _data(self) -> LibrenmsDeviceInfo: """Get DeviceInfo from coordinator.""" return self.coordinator.data.devices[self.device_id] + + +class LibrenmsSystemEntity(CoordinatorEntity[LibrenmsDataUpdateCoordinator]): + """Define LibreNMS base entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: LibrenmsDataUpdateCoordinator, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + manufacturer="LibreNMS", + sw_version=coordinator.data.system.local_ver, + entry_type=DeviceEntryType.SERVICE, + configuration_url=coordinator.configuration_url, + name="LibreNMS", + ) diff --git a/homeassistant/components/librenms/icons.json b/homeassistant/components/librenms/icons.json new file mode 100644 index 00000000000000..fcca916b7c10f4 --- /dev/null +++ b/homeassistant/components/librenms/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "database_version": { + "default": "mdi:database" + }, + "netsnmp_version": { + "default": "mdi:network-outline" + }, + "php_version": { + "default": "mdi:language-php" + }, + "python_version": { + "default": "mdi:language-python" + }, + "rrdtool_version": { + "default": "mdi:database-clock" + } + } + } +} diff --git a/homeassistant/components/librenms/sensor.py b/homeassistant/components/librenms/sensor.py new file mode 100644 index 00000000000000..09e3dd237d963d --- /dev/null +++ b/homeassistant/components/librenms/sensor.py @@ -0,0 +1,114 @@ +"""Sensor platform for the LibreNMS integration.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import override + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import ( + LibrenmsConfigEntry, + LibrenmsData, + LibrenmsDataUpdateCoordinator, +) +from .entity import LibrenmsSystemEntity + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class LibrenmsSystemSensorEntityDescription(SensorEntityDescription): + """Librenms system sensor entity description.""" + + value: Callable[[LibrenmsData], StateType] + is_suitable: Callable[[LibrenmsData], bool] = lambda _: True + + +SYSTEM_SENSOR_TYPES: tuple[LibrenmsSystemSensorEntityDescription, ...] = ( + LibrenmsSystemSensorEntityDescription( + key="device_count", + translation_key="device_count", + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: len(data.devices), + ), + LibrenmsSystemSensorEntityDescription( + key="database_version", + translation_key="database_version", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value=lambda data: data.system.database_ver, + ), + LibrenmsSystemSensorEntityDescription( + key="netsnmp_version", + translation_key="netsnmp_version", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value=lambda data: data.system.netsnmp_ver, + ), + LibrenmsSystemSensorEntityDescription( + key="php_version", + translation_key="php_version", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value=lambda data: data.system.php_ver, + ), + LibrenmsSystemSensorEntityDescription( + key="python_version", + translation_key="python_version", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value=lambda data: data.system.python_ver, + ), + LibrenmsSystemSensorEntityDescription( + key="rrdtool_version", + translation_key="rrdtool_version", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value=lambda data: data.system.rrdtool_ver, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LibrenmsConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Add LibreNMS server state sensors.""" + coordinator = entry.runtime_data + async_add_entities( + LibrenmsSystemSensorEntity(coordinator, description) + for description in SYSTEM_SENSOR_TYPES + if description.is_suitable(coordinator.data) + ) + + +class LibrenmsSystemSensorEntity(LibrenmsSystemEntity, SensorEntity): + """Define Librenms sensor entity.""" + + entity_description: LibrenmsSystemSensorEntityDescription + + def __init__( + self, + coordinator: LibrenmsDataUpdateCoordinator, + description: LibrenmsSystemSensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self.entity_description = description + + @property + @override + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.entity_description.value(self.coordinator.data) diff --git a/homeassistant/components/librenms/strings.json b/homeassistant/components/librenms/strings.json index 3544e152ec19aa..9ed38560461050 100644 --- a/homeassistant/components/librenms/strings.json +++ b/homeassistant/components/librenms/strings.json @@ -34,6 +34,16 @@ "status": { "name": "Status" } + }, + "sensor": { + "database_version": { "name": "Database version" }, + "device_count": { + "name": "Total device count" + }, + "netsnmp_version": { "name": "NetSNMP version" }, + "php_version": { "name": "PHP version" }, + "python_version": { "name": "Python version" }, + "rrdtool_version": { "name": "RRDTool version" } } }, "exceptions": { diff --git a/tests/components/librenms/snapshots/test_sensor.ambr b/tests/components/librenms/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..95e64f0e5ef762 --- /dev/null +++ b/tests/components/librenms/snapshots/test_sensor.ambr @@ -0,0 +1,336 @@ +# serializer version: 1 +# name: test_sensors.12 + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entry_id': , + 'config_subentry_id': , + 'configuration_url': 'https://librenms', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'librenms', + '01KXX1E2EMMSCDQ2K4A0C7JA9T', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'LibreNMS', + 'model': None, + 'model_id': None, + 'name': 'LibreNMS', + 'name_by_user': None, + 'serial_number': None, + 'sw_version': '26.6.1', + 'via_device_id': None, + }), + ]) +# --- +# name: test_sensors[sensor.librenms_database_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.librenms_database_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Database version', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Database version', + 'platform': 'librenms', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'database_version', + 'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_database_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.librenms_database_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'LibreNMS Database version', + }), + 'context': , + 'entity_id': 'sensor.librenms_database_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'MariaDB 10.5.29-MariaDB-ubu2004', + }) +# --- +# name: test_sensors[sensor.librenms_netsnmp_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.librenms_netsnmp_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'NetSNMP version', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NetSNMP version', + 'platform': 'librenms', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'netsnmp_version', + 'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_netsnmp_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.librenms_netsnmp_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'LibreNMS NetSNMP version', + }), + 'context': , + 'entity_id': 'sensor.librenms_netsnmp_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.9.5.2', + }) +# --- +# name: test_sensors[sensor.librenms_php_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.librenms_php_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'PHP version', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'PHP version', + 'platform': 'librenms', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'php_version', + 'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_php_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.librenms_php_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'LibreNMS PHP version', + }), + 'context': , + 'entity_id': 'sensor.librenms_php_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.4.21', + }) +# --- +# name: test_sensors[sensor.librenms_python_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.librenms_python_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Python version', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Python version', + 'platform': 'librenms', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'python_version', + 'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_python_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.librenms_python_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'LibreNMS Python version', + }), + 'context': , + 'entity_id': 'sensor.librenms_python_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.12.13', + }) +# --- +# name: test_sensors[sensor.librenms_rrdtool_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.librenms_rrdtool_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RRDTool version', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RRDTool version', + 'platform': 'librenms', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rrdtool_version', + 'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_rrdtool_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.librenms_rrdtool_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'LibreNMS RRDTool version', + }), + 'context': , + 'entity_id': 'sensor.librenms_rrdtool_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.9.0', + }) +# --- +# name: test_sensors[sensor.librenms_total_device_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.librenms_total_device_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total device count', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total device count', + 'platform': 'librenms', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'device_count', + 'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_device_count', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.librenms_total_device_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'LibreNMS Total device count', + : , + }), + 'context': , + 'entity_id': 'sensor.librenms_total_device_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- diff --git a/tests/components/librenms/test_sensor.py b/tests/components/librenms/test_sensor.py new file mode 100644 index 00000000000000..f153c7e7ba02dc --- /dev/null +++ b/tests/components/librenms/test_sensor.py @@ -0,0 +1,36 @@ +"""Test the LibreNMS sensor platform.""" + +from unittest.mock import Mock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + mock_librenms: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the LibreNMS sensor platform.""" + + with patch("homeassistant.components.librenms.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + devices = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + assert devices == snapshot