diff --git a/.strict-typing b/.strict-typing index b3e41747239..452a6f647a7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -307,6 +307,7 @@ homeassistant.components.ld2410_ble.* homeassistant.components.led_ble.* homeassistant.components.lektrico.* homeassistant.components.letpot.* +homeassistant.components.libre_hardware_monitor.* homeassistant.components.lidarr.* homeassistant.components.lifx.* homeassistant.components.light.* diff --git a/CODEOWNERS b/CODEOWNERS index 2bdb56f6383..855555c199e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -860,6 +860,8 @@ build.json @home-assistant/supervisor /tests/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lg_thinq/ @LG-ThinQ-Integration /tests/components/lg_thinq/ @LG-ThinQ-Integration +/homeassistant/components/libre_hardware_monitor/ @Sab44 +/tests/components/libre_hardware_monitor/ @Sab44 /homeassistant/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob /homeassistant/components/lifx/ @Djelibeybi diff --git a/homeassistant/components/libre_hardware_monitor/__init__.py b/homeassistant/components/libre_hardware_monitor/__init__.py new file mode 100644 index 00000000000..4f39d51e963 --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/__init__.py @@ -0,0 +1,34 @@ +"""The LibreHardwareMonitor integration.""" + +from __future__ import annotations + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import ( + LibreHardwareMonitorConfigEntry, + LibreHardwareMonitorCoordinator, +) + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: LibreHardwareMonitorConfigEntry +) -> bool: + """Set up LibreHardwareMonitor from a config entry.""" + + lhm_coordinator = LibreHardwareMonitorCoordinator(hass, config_entry) + await lhm_coordinator.async_config_entry_first_refresh() + + config_entry.runtime_data = lhm_coordinator + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry( + hass: HomeAssistant, config_entry: LibreHardwareMonitorConfigEntry +) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/libre_hardware_monitor/config_flow.py b/homeassistant/components/libre_hardware_monitor/config_flow.py new file mode 100644 index 00000000000..f24c801254c --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for LibreHardwareMonitor.""" + +from __future__ import annotations + +import logging +from typing import Any + +from librehardwaremonitor_api import ( + LibreHardwareMonitorClient, + LibreHardwareMonitorConnectionError, + LibreHardwareMonitorNoDevicesError, +) +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DEFAULT_HOST, DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + + +class LibreHardwareMonitorConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for LibreHardwareMonitor.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + self._async_abort_entries_match(user_input) + + api = LibreHardwareMonitorClient( + user_input[CONF_HOST], user_input[CONF_PORT] + ) + + try: + _ = (await api.get_data()).main_device_ids_and_names.values() + except LibreHardwareMonitorConnectionError as exception: + _LOGGER.error(exception) + errors["base"] = "cannot_connect" + except LibreHardwareMonitorNoDevicesError: + errors["base"] = "no_devices" + else: + return self.async_create_entry( + title=f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}", + data=user_input, + ) + + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/libre_hardware_monitor/const.py b/homeassistant/components/libre_hardware_monitor/const.py new file mode 100644 index 00000000000..88380a6cf9d --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/const.py @@ -0,0 +1,6 @@ +"""Constants for the LibreHardwareMonitor integration.""" + +DOMAIN = "libre_hardware_monitor" +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 8085 +DEFAULT_SCAN_INTERVAL = 10 diff --git a/homeassistant/components/libre_hardware_monitor/coordinator.py b/homeassistant/components/libre_hardware_monitor/coordinator.py new file mode 100644 index 00000000000..6e87fd70301 --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/coordinator.py @@ -0,0 +1,130 @@ +"""Coordinator for LibreHardwareMonitor integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from types import MappingProxyType + +from librehardwaremonitor_api import ( + LibreHardwareMonitorClient, + LibreHardwareMonitorConnectionError, + LibreHardwareMonitorNoDevicesError, +) +from librehardwaremonitor_api.model import ( + DeviceId, + DeviceName, + LibreHardwareMonitorData, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +type LibreHardwareMonitorConfigEntry = ConfigEntry[LibreHardwareMonitorCoordinator] + + +class LibreHardwareMonitorCoordinator(DataUpdateCoordinator[LibreHardwareMonitorData]): + """Class to manage fetching LibreHardwareMonitor data.""" + + config_entry: LibreHardwareMonitorConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: LibreHardwareMonitorConfigEntry + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + config_entry=config_entry, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + self._api = LibreHardwareMonitorClient(host, port) + device_entries: list[DeviceEntry] = dr.async_entries_for_config_entry( + registry=dr.async_get(self.hass), config_entry_id=config_entry.entry_id + ) + self._previous_devices: MappingProxyType[DeviceId, DeviceName] = ( + MappingProxyType( + { + DeviceId(next(iter(device.identifiers))[1]): DeviceName(device.name) + for device in device_entries + if device.identifiers and device.name + } + ) + ) + + async def _async_update_data(self) -> LibreHardwareMonitorData: + try: + lhm_data = await self._api.get_data() + except LibreHardwareMonitorConnectionError as err: + raise UpdateFailed( + "LibreHardwareMonitor connection failed, will retry" + ) from err + except LibreHardwareMonitorNoDevicesError as err: + raise UpdateFailed("No sensor data available, will retry") from err + + await self._async_handle_changes_in_devices(lhm_data.main_device_ids_and_names) + + return lhm_data + + async def _async_refresh( + self, + log_failures: bool = True, + raise_on_auth_failed: bool = False, + scheduled: bool = False, + raise_on_entry_error: bool = False, + ) -> None: + # we don't expect the computer to be online 24/7 so we don't want to log a connection loss as an error + await super()._async_refresh( + False, raise_on_auth_failed, scheduled, raise_on_entry_error + ) + + async def _async_handle_changes_in_devices( + self, detected_devices: MappingProxyType[DeviceId, DeviceName] + ) -> None: + """Handle device changes by deleting devices from / adding devices to Home Assistant.""" + previous_device_ids = set(self._previous_devices.keys()) + detected_device_ids = set(detected_devices.keys()) + + if previous_device_ids == detected_device_ids: + return + + if self.data is None: + # initial update during integration startup + self._previous_devices = detected_devices # type: ignore[unreachable] + return + + if orphaned_devices := previous_device_ids - detected_device_ids: + _LOGGER.warning( + "Device(s) no longer available, will be removed: %s", + [self._previous_devices[device_id] for device_id in orphaned_devices], + ) + device_registry = dr.async_get(self.hass) + for device_id in orphaned_devices: + if device := device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ): + device_registry.async_update_device( + device_id=device.id, + remove_config_entry_id=self.config_entry.entry_id, + ) + + if new_devices := detected_device_ids - previous_device_ids: + _LOGGER.warning( + "New Device(s) detected, reload integration to add them to Home Assistant: %s", + [detected_devices[DeviceId(device_id)] for device_id in new_devices], + ) + + self._previous_devices = detected_devices diff --git a/homeassistant/components/libre_hardware_monitor/manifest.json b/homeassistant/components/libre_hardware_monitor/manifest.json new file mode 100644 index 00000000000..66623db1f2d --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "libre_hardware_monitor", + "name": "Libre Hardware Monitor", + "codeowners": ["@Sab44"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor", + "iot_class": "local_polling", + "quality_scale": "silver", + "requirements": ["librehardwaremonitor-api==1.3.1"] +} diff --git a/homeassistant/components/libre_hardware_monitor/quality_scale.yaml b/homeassistant/components/libre_hardware_monitor/quality_scale.yaml new file mode 100644 index 00000000000..cdb13882038 --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/quality_scale.yaml @@ -0,0 +1,81 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + No custom actions are defined. + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: | + No custom actions are defined. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + No explicit event subscriptions. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: + status: exempt + comment: | + Device is expected to be offline most of the time, but needs to connect quickly once available. + unique-config-entry: done + # Silver + action-exceptions: + status: exempt + comment: | + No custom actions are defined. + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: + status: exempt + comment: | + Device is expected to be temporarily unavailable. + parallel-updates: done + reauthentication-flow: + status: exempt + comment: | + This integration does not require authentication. + test-coverage: done + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: done + docs-use-cases: todo + dynamic-devices: done + entity-category: todo + entity-device-class: todo + entity-disabled-by-default: todo + entity-translations: todo + exception-translations: todo + icon-translations: todo + reconfiguration-flow: todo + repair-issues: + status: exempt + comment: | + This integration doesn't have any cases where raising an issue is needed. + stale-devices: done + # Platinum + async-dependency: done + inject-websession: todo + strict-typing: done diff --git a/homeassistant/components/libre_hardware_monitor/sensor.py b/homeassistant/components/libre_hardware_monitor/sensor.py new file mode 100644 index 00000000000..cb7d94ae73e --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/sensor.py @@ -0,0 +1,95 @@ +"""Support for LibreHardwareMonitor Sensor Platform.""" + +from __future__ import annotations + +from librehardwaremonitor_api.model import LibreHardwareMonitorSensorData + +from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import LibreHardwareMonitorCoordinator +from .const import DOMAIN +from .coordinator import LibreHardwareMonitorConfigEntry + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + +STATE_MIN_VALUE = "min_value" +STATE_MAX_VALUE = "max_value" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: LibreHardwareMonitorConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the LibreHardwareMonitor platform.""" + lhm_coordinator = config_entry.runtime_data + + async_add_entities( + LibreHardwareMonitorSensor(lhm_coordinator, sensor_data) + for sensor_data in lhm_coordinator.data.sensor_data.values() + ) + + +class LibreHardwareMonitorSensor( + CoordinatorEntity[LibreHardwareMonitorCoordinator], SensorEntity +): + """Sensor to display information from LibreHardwareMonitor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True + + def __init__( + self, + coordinator: LibreHardwareMonitorCoordinator, + sensor_data: LibreHardwareMonitorSensorData, + ) -> None: + """Initialize an LibreHardwareMonitor sensor.""" + super().__init__(coordinator) + + self._attr_name: str = sensor_data.name + self.value: str | None = sensor_data.value + self._attr_extra_state_attributes: dict[str, str] = { + STATE_MIN_VALUE: self._format_number_value(sensor_data.min), + STATE_MAX_VALUE: self._format_number_value(sensor_data.max), + } + self._attr_native_unit_of_measurement = sensor_data.unit + self._attr_unique_id: str = f"lhm-{sensor_data.sensor_id}" + + self._sensor_id: str = sensor_data.sensor_id + + # Hardware device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, sensor_data.device_id)}, + name=sensor_data.device_name, + model=sensor_data.device_type, + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if sensor_data := self.coordinator.data.sensor_data.get(self._sensor_id): + self.value = sensor_data.value + self._attr_extra_state_attributes = { + STATE_MIN_VALUE: self._format_number_value(sensor_data.min), + STATE_MAX_VALUE: self._format_number_value(sensor_data.max), + } + else: + self.value = None + + super()._handle_coordinator_update() + + @property + def native_value(self) -> str | None: + """Return the formatted sensor value or None if no value is available.""" + if self.value is not None and self.value != "-": + return self._format_number_value(self.value) + return None + + @staticmethod + def _format_number_value(number_str: str) -> str: + return number_str.replace(",", ".") diff --git a/homeassistant/components/libre_hardware_monitor/strings.json b/homeassistant/components/libre_hardware_monitor/strings.json new file mode 100644 index 00000000000..6a40a8dbb7a --- /dev/null +++ b/homeassistant/components/libre_hardware_monitor/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_devices": "[%key:common::config_flow::abort::no_devices_found%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "The IP address or hostname of the system running Libre Hardware Monitor.", + "port": "The port of your Libre Hardware Monitor web server. By default 8085." + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 96ef5fd4c93..e8788502664 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -348,6 +348,7 @@ FLOWS = { "lg_netcast", "lg_soundbar", "lg_thinq", + "libre_hardware_monitor", "lidarr", "lifx", "linkplay", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6ab648c3f73..722c55dcf8c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3484,6 +3484,12 @@ } } }, + "libre_hardware_monitor": { + "name": "Libre Hardware Monitor", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "lidarr": { "name": "Lidarr", "integration_type": "service", diff --git a/mypy.ini b/mypy.ini index ad9196c80c5..db883045f85 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2826,6 +2826,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.libre_hardware_monitor.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lidarr.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 17731e065bd..62a69b9d53c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1351,6 +1351,9 @@ libpyfoscamcgi==0.0.7 # homeassistant.components.vivotek libpyvivotek==0.4.0 +# homeassistant.components.libre_hardware_monitor +librehardwaremonitor-api==1.3.1 + # homeassistant.components.mikrotik librouteros==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce6e163a396..498f4afe792 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1167,6 +1167,9 @@ letpot==0.6.2 # homeassistant.components.foscam libpyfoscamcgi==0.0.7 +# homeassistant.components.libre_hardware_monitor +librehardwaremonitor-api==1.3.1 + # homeassistant.components.mikrotik librouteros==3.2.0 diff --git a/tests/components/libre_hardware_monitor/__init__.py b/tests/components/libre_hardware_monitor/__init__.py new file mode 100644 index 00000000000..5038f95219f --- /dev/null +++ b/tests/components/libre_hardware_monitor/__init__.py @@ -0,0 +1,15 @@ +"""Tests for the LibreHardwareMonitor integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Mock integration setup.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/libre_hardware_monitor/conftest.py b/tests/components/libre_hardware_monitor/conftest.py new file mode 100644 index 00000000000..cff9e4acf3a --- /dev/null +++ b/tests/components/libre_hardware_monitor/conftest.py @@ -0,0 +1,57 @@ +"""Common fixtures for the LibreHardwareMonitor tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from librehardwaremonitor_api.parser import LibreHardwareMonitorParser +import pytest + +from homeassistant.components.libre_hardware_monitor.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry, load_json_object_fixture + +VALID_CONFIG = {CONF_HOST: "192.168.0.20", CONF_PORT: 8085} + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.libre_hardware_monitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Config entry fixture.""" + return MockConfigEntry( + domain=DOMAIN, + title="192.168.0.20:8085", + data=VALID_CONFIG, + ) + + +@pytest.fixture +def mock_lhm_client() -> Generator[AsyncMock]: + """Mock a LibreHardwareMonitor client.""" + with ( + patch( + "homeassistant.components.libre_hardware_monitor.config_flow.LibreHardwareMonitorClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.libre_hardware_monitor.coordinator.LibreHardwareMonitorClient", + new=mock_client, + ), + ): + client = mock_client.return_value + test_data_json = load_json_object_fixture( + "libre_hardware_monitor.json", "libre_hardware_monitor" + ) + test_data = LibreHardwareMonitorParser().parse_data(test_data_json) + client.get_data.return_value = test_data + + yield client diff --git a/tests/components/libre_hardware_monitor/fixtures/libre_hardware_monitor.json b/tests/components/libre_hardware_monitor/fixtures/libre_hardware_monitor.json new file mode 100644 index 00000000000..0e4c6309ba3 --- /dev/null +++ b/tests/components/libre_hardware_monitor/fixtures/libre_hardware_monitor.json @@ -0,0 +1,465 @@ +{ + "id": 0, + "Text": "Sensor", + "Min": "Min", + "Value": "Value", + "Max": "Max", + "ImageURL": "", + "Children": [ + { + "id": 1, + "Text": "GAMING", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/computer.png", + "Children": [ + { + "id": 2, + "Text": "MSI MAG B650M MORTAR WIFI (MS-7D76)", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/mainboard.png", + "Children": [ + { + "id": 3, + "Text": "Nuvoton NCT6687D", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/chip.png", + "Children": [ + { + "id": 4, + "Text": "Voltages", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/voltage.png", + "Children": [ + { + "id": 5, + "Text": "+12V", + "Min": "12,048 V", + "Value": "12,072 V", + "Max": "12,096 V", + "SensorId": "/lpc/nct6687d/0/voltage/0", + "Type": "Voltage", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 6, + "Text": "+5V", + "Min": "5,020 V", + "Value": "5,030 V", + "Max": "5,050 V", + "SensorId": "/lpc/nct6687d/0/voltage/1", + "Type": "Voltage", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 7, + "Text": "Vcore", + "Min": "1,310 V", + "Value": "1,312 V", + "Max": "1,318 V", + "SensorId": "/lpc/nct6687d/0/voltage/2", + "Type": "Voltage", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 8, + "Text": "Temperatures", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/temperature.png", + "Children": [ + { + "id": 9, + "Text": "CPU", + "Min": "39,0 °C", + "Value": "55,0 °C", + "Max": "68,0 °C", + "SensorId": "/lpc/nct6687d/0/temperature/0", + "Type": "Temperature", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 10, + "Text": "System", + "Min": "32,5 °C", + "Value": "45,5 °C", + "Max": "46,5 °C", + "SensorId": "/lpc/nct6687d/0/temperature/1", + "Type": "Temperature", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 11, + "Text": "Fans", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/fan.png", + "Children": [ + { + "id": 12, + "Text": "CPU Fan", + "Min": "0 RPM", + "Value": "0 RPM", + "Max": "0 RPM", + "SensorId": "/lpc/nct6687d/0/fan/0", + "Type": "Fan", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 13, + "Text": "Pump Fan", + "Min": "0 RPM", + "Value": "0 RPM", + "Max": "0 RPM", + "SensorId": "/lpc/nct6687d/0/fan/1", + "Type": "Fan", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 14, + "Text": "System Fan #1", + "Min": "-", + "Value": "-", + "Max": "-", + "SensorId": "/lpc/nct6687d/0/fan/2", + "Type": "Fan", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + } + ] + } + ] + }, + { + "id": 15, + "Text": "AMD Ryzen 7 7800X3D", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/cpu.png", + "Children": [ + { + "id": 16, + "Text": "Voltages", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/voltage.png", + "Children": [ + { + "id": 17, + "Text": "VDDCR", + "Min": "0,452 V", + "Value": "1,083 V", + "Max": "1,173 V", + "SensorId": "/amdcpu/0/voltage/2", + "Type": "Voltage", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 18, + "Text": "VDDCR SoC", + "Min": "1,305 V", + "Value": "1,305 V", + "Max": "1,306 V", + "SensorId": "/amdcpu/0/voltage/3", + "Type": "Voltage", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 19, + "Text": "Powers", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/power.png", + "Children": [ + { + "id": 20, + "Text": "Package", + "Min": "25,1 W", + "Value": "39,6 W", + "Max": "70,1 W", + "SensorId": "/amdcpu/0/power/0", + "Type": "Power", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 21, + "Text": "Temperatures", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/temperature.png", + "Children": [ + { + "id": 22, + "Text": "Core (Tctl/Tdie)", + "Min": "39,4 °C", + "Value": "55,5 °C", + "Max": "69,1 °C", + "SensorId": "/amdcpu/0/temperature/2", + "Type": "Temperature", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 23, + "Text": "Package", + "Min": "38,4 °C", + "Value": "52,8 °C", + "Max": "74,0 °C", + "SensorId": "/amdcpu/0/temperature/3", + "Type": "Temperature", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 24, + "Text": "Load", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/load.png", + "Children": [ + { + "id": 25, + "Text": "CPU Total", + "Min": "0,0 %", + "Value": "9,1 %", + "Max": "55,8 %", + "SensorId": "/amdcpu/0/load/0", + "Type": "Load", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + } + ] + }, + { + "id": 26, + "Text": "NVIDIA GeForce RTX 4080 SUPER", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/nvidia.png", + "Children": [ + { + "id": 27, + "Text": "Powers", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/power.png", + "Children": [ + { + "id": 28, + "Text": "GPU Package", + "Min": "4,1 W", + "Value": "59,6 W", + "Max": "66,6 W", + "SensorId": "/gpu-nvidia/0/power/0", + "Type": "Power", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 29, + "Text": "Clocks", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/clock.png", + "Children": [ + { + "id": 30, + "Text": "GPU Core", + "Min": "210,0 MHz", + "Value": "2805,0 MHz", + "Max": "2805,0 MHz", + "SensorId": "/gpu-nvidia/0/clock/0", + "Type": "Clock", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 31, + "Text": "GPU Memory", + "Min": "405,0 MHz", + "Value": "11252,0 MHz", + "Max": "11502,0 MHz", + "SensorId": "/gpu-nvidia/0/clock/4", + "Type": "Clock", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 32, + "Text": "Temperatures", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/temperature.png", + "Children": [ + { + "id": 33, + "Text": "GPU Core", + "Min": "25,0 °C", + "Value": "36,0 °C", + "Max": "37,0 °C", + "SensorId": "/gpu-nvidia/0/temperature/0", + "Type": "Temperature", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 34, + "Text": "GPU Hot Spot", + "Min": "32,5 °C", + "Value": "43,0 °C", + "Max": "43,3 °C", + "SensorId": "/gpu-nvidia/0/temperature/2", + "Type": "Temperature", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 35, + "Text": "Load", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/load.png", + "Children": [ + { + "id": 36, + "Text": "GPU Core", + "Min": "0,0 %", + "Value": "5,0 %", + "Max": "19,0 %", + "SensorId": "/gpu-nvidia/0/load/0", + "Type": "Load", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 37, + "Text": "GPU Memory Controller", + "Min": "0,0 %", + "Value": "0,0 %", + "Max": "49,0 %", + "SensorId": "/gpu-nvidia/0/load/1", + "Type": "Load", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 38, + "Text": "GPU Video Engine", + "Min": "0,0 %", + "Value": "97,0 %", + "Max": "99,0 %", + "SensorId": "/gpu-nvidia/0/load/2", + "Type": "Load", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 39, + "Text": "Fans", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/fan.png", + "Children": [ + { + "id": 40, + "Text": "GPU Fan 1", + "Min": "0 RPM", + "Value": "0 RPM", + "Max": "0 RPM", + "SensorId": "/gpu-nvidia/0/fan/1", + "Type": "Fan", + "ImageURL": "images/transparent.png", + "Children": [] + }, + { + "id": 41, + "Text": "GPU Fan 2", + "Min": "0 RPM", + "Value": "0 RPM", + "Max": "0 RPM", + "SensorId": "/gpu-nvidia/0/fan/2", + "Type": "Fan", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + }, + { + "id": 42, + "Text": "Throughput", + "Min": "", + "Value": "", + "Max": "", + "ImageURL": "images_icon/throughput.png", + "Children": [ + { + "id": 43, + "Text": "GPU PCIe Tx", + "Min": "0,0 KB/s", + "Value": "166,1 MB/s", + "Max": "2422,8 MB/s", + "SensorId": "/gpu-nvidia/0/throughput/1", + "Type": "Throughput", + "ImageURL": "images/transparent.png", + "Children": [] + } + ] + } + ] + } + ] + } + ] +} diff --git a/tests/components/libre_hardware_monitor/snapshots/test_sensor.ambr b/tests/components/libre_hardware_monitor/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..9e26d4d49f7 --- /dev/null +++ b/tests/components/libre_hardware_monitor/snapshots/test_sensor.ambr @@ -0,0 +1,2106 @@ +# serializer version: 1 +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_core_tctl_tdie_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_core_tctl_tdie_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Core (Tctl/Tdie) Temperature', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-amdcpu-0-temperature-2', + 'unit_of_measurement': '°C', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_core_tctl_tdie_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Core (Tctl/Tdie) Temperature', + 'max_value': '69.1', + 'min_value': '39.4', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_core_tctl_tdie_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.5', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_cpu_total_load-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_cpu_total_load', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CPU Total Load', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-amdcpu-0-load-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_cpu_total_load-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D CPU Total Load', + 'max_value': '55.8', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_cpu_total_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.1', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_package_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Package Power', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-amdcpu-0-power-0', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_package_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Package Power', + 'max_value': '70.1', + 'min_value': '25.1', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '39.6', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_package_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Package Temperature', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-amdcpu-0-temperature-3', + 'unit_of_measurement': '°C', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_package_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Package Temperature', + 'max_value': '74.0', + 'min_value': '38.4', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52.8', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_vddcr_soc_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_soc_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VDDCR SoC Voltage', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-amdcpu-0-voltage-3', + 'unit_of_measurement': 'V', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_vddcr_soc_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D VDDCR SoC Voltage', + 'max_value': '1.306', + 'min_value': '1.305', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_soc_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.305', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_vddcr_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VDDCR Voltage', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-amdcpu-0-voltage-2', + 'unit_of_measurement': 'V', + }) +# --- +# name: test_sensors_are_created[sensor.amd_ryzen_7_7800x3d_vddcr_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D VDDCR Voltage', + 'max_value': '1.173', + 'min_value': '0.452', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.083', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': '+12V Voltage', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-voltage-0', + 'unit_of_measurement': 'V', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) +12V Voltage', + 'max_value': '12.096', + 'min_value': '12.048', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.072', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_5v_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_5v_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': '+5V Voltage', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-voltage-1', + 'unit_of_measurement': 'V', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_5v_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) +5V Voltage', + 'max_value': '5.050', + 'min_value': '5.020', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_5v_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.030', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CPU Fan Fan', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-fan-0', + 'unit_of_measurement': 'RPM', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CPU Temperature', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-temperature-0', + 'unit_of_measurement': '°C', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Temperature', + 'max_value': '68.0', + 'min_value': '39.0', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.0', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pump Fan Fan', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-fan-1', + 'unit_of_measurement': 'RPM', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'System Fan #1 Fan', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-fan-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan', + 'max_value': '-', + 'min_value': '-', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'System Temperature', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-temperature-1', + 'unit_of_measurement': '°C', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Temperature', + 'max_value': '46.5', + 'min_value': '32.5', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.5', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_vcore_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_vcore_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vcore Voltage', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-lpc-nct6687d-0-voltage-2', + 'unit_of_measurement': 'V', + }) +# --- +# name: test_sensors_are_created[sensor.msi_mag_b650m_mortar_wifi_ms_7d76_vcore_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) Vcore Voltage', + 'max_value': '1.318', + 'min_value': '1.310', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_vcore_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.312', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_core_clock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_clock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Core Clock', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-clock-0', + 'unit_of_measurement': 'MHz', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_core_clock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Clock', + 'max_value': '2805.0', + 'min_value': '210.0', + 'state_class': , + 'unit_of_measurement': 'MHz', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_clock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2805.0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_core_load-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_load', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Core Load', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-load-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_core_load-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Load', + 'max_value': '19.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_core_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Core Temperature', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-temperature-0', + 'unit_of_measurement': '°C', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_core_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Temperature', + 'max_value': '37.0', + 'min_value': '25.0', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_fan_1_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_1_fan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Fan 1 Fan', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-fan-1', + 'unit_of_measurement': 'RPM', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_fan_1_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_1_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_fan_2_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_2_fan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Fan 2 Fan', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-fan-2', + 'unit_of_measurement': 'RPM', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_fan_2_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_2_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_hot_spot_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_hot_spot_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Hot Spot Temperature', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-temperature-2', + 'unit_of_measurement': '°C', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_hot_spot_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Hot Spot Temperature', + 'max_value': '43.3', + 'min_value': '32.5', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_hot_spot_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '43.0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_memory_clock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_clock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Memory Clock', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-clock-4', + 'unit_of_measurement': 'MHz', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_memory_clock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Memory Clock', + 'max_value': '11502.0', + 'min_value': '405.0', + 'state_class': , + 'unit_of_measurement': 'MHz', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_clock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11252.0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_memory_controller_load-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_controller_load', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Memory Controller Load', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-load-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_memory_controller_load-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Memory Controller Load', + 'max_value': '49.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_controller_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_package_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_package_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Package Power', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-power-0', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_package_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Package Power', + 'max_value': '66.6', + 'min_value': '4.1', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_package_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '59.6', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_pcie_tx_throughput-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_pcie_tx_throughput', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU PCIe Tx Throughput', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-throughput-1', + 'unit_of_measurement': 'MB/s', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_pcie_tx_throughput-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU PCIe Tx Throughput', + 'max_value': '2422.8', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': 'MB/s', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_pcie_tx_throughput', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '166.1', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_video_engine_load-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_video_engine_load', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'GPU Video Engine Load', + 'platform': 'libre_hardware_monitor', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'lhm-gpu-nvidia-0-load-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors_are_created[sensor.nvidia_geforce_rtx_4080_super_gpu_video_engine_load-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Video Engine Load', + 'max_value': '99.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_video_engine_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97.0', + }) +# --- +# name: test_sensors_go_unavailable_in_case_of_error_and_recover_after_successful_retry[LibreHardwareMonitorConnectionError][valid_sensor_data] + list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) +12V Voltage', + 'max_value': '12.096', + 'min_value': '12.048', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.072', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) +5V Voltage', + 'max_value': '5.050', + 'min_value': '5.020', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_5v_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.030', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) Vcore Voltage', + 'max_value': '1.318', + 'min_value': '1.310', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_vcore_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.312', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Temperature', + 'max_value': '68.0', + 'min_value': '39.0', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Temperature', + 'max_value': '46.5', + 'min_value': '32.5', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.5', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan', + 'max_value': '-', + 'min_value': '-', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D VDDCR Voltage', + 'max_value': '1.173', + 'min_value': '0.452', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.083', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D VDDCR SoC Voltage', + 'max_value': '1.306', + 'min_value': '1.305', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_soc_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.305', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Package Power', + 'max_value': '70.1', + 'min_value': '25.1', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '39.6', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Core (Tctl/Tdie) Temperature', + 'max_value': '69.1', + 'min_value': '39.4', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_core_tctl_tdie_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.5', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Package Temperature', + 'max_value': '74.0', + 'min_value': '38.4', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52.8', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D CPU Total Load', + 'max_value': '55.8', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_cpu_total_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.1', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Package Power', + 'max_value': '66.6', + 'min_value': '4.1', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_package_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '59.6', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Clock', + 'max_value': '2805.0', + 'min_value': '210.0', + 'state_class': , + 'unit_of_measurement': 'MHz', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_clock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2805.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Memory Clock', + 'max_value': '11502.0', + 'min_value': '405.0', + 'state_class': , + 'unit_of_measurement': 'MHz', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_clock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11252.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Temperature', + 'max_value': '37.0', + 'min_value': '25.0', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Hot Spot Temperature', + 'max_value': '43.3', + 'min_value': '32.5', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_hot_spot_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '43.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Load', + 'max_value': '19.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Memory Controller Load', + 'max_value': '49.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_controller_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Video Engine Load', + 'max_value': '99.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_video_engine_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_1_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_2_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU PCIe Tx Throughput', + 'max_value': '2422.8', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': 'MB/s', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_pcie_tx_throughput', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '166.1', + }), + ]) +# --- +# name: test_sensors_go_unavailable_in_case_of_error_and_recover_after_successful_retry[LibreHardwareMonitorNoDevicesError][valid_sensor_data] + list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) +12V Voltage', + 'max_value': '12.096', + 'min_value': '12.048', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.072', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) +5V Voltage', + 'max_value': '5.050', + 'min_value': '5.020', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_5v_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.030', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) Vcore Voltage', + 'max_value': '1.318', + 'min_value': '1.310', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_vcore_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.312', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Temperature', + 'max_value': '68.0', + 'min_value': '39.0', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Temperature', + 'max_value': '46.5', + 'min_value': '32.5', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.5', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan', + 'max_value': '-', + 'min_value': '-', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D VDDCR Voltage', + 'max_value': '1.173', + 'min_value': '0.452', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.083', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D VDDCR SoC Voltage', + 'max_value': '1.306', + 'min_value': '1.305', + 'state_class': , + 'unit_of_measurement': 'V', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_vddcr_soc_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.305', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Package Power', + 'max_value': '70.1', + 'min_value': '25.1', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '39.6', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Core (Tctl/Tdie) Temperature', + 'max_value': '69.1', + 'min_value': '39.4', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_core_tctl_tdie_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.5', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D Package Temperature', + 'max_value': '74.0', + 'min_value': '38.4', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_package_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52.8', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AMD Ryzen 7 7800X3D CPU Total Load', + 'max_value': '55.8', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.amd_ryzen_7_7800x3d_cpu_total_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.1', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Package Power', + 'max_value': '66.6', + 'min_value': '4.1', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_package_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '59.6', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Clock', + 'max_value': '2805.0', + 'min_value': '210.0', + 'state_class': , + 'unit_of_measurement': 'MHz', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_clock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2805.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Memory Clock', + 'max_value': '11502.0', + 'min_value': '405.0', + 'state_class': , + 'unit_of_measurement': 'MHz', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_clock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11252.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Temperature', + 'max_value': '37.0', + 'min_value': '25.0', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Hot Spot Temperature', + 'max_value': '43.3', + 'min_value': '32.5', + 'state_class': , + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_hot_spot_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '43.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Core Load', + 'max_value': '19.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_core_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Memory Controller Load', + 'max_value': '49.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_memory_controller_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Video Engine Load', + 'max_value': '99.0', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_video_engine_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_1_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Fan', + 'max_value': '0', + 'min_value': '0', + 'state_class': , + 'unit_of_measurement': 'RPM', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_fan_2_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVIDIA GeForce RTX 4080 SUPER GPU PCIe Tx Throughput', + 'max_value': '2422.8', + 'min_value': '0.0', + 'state_class': , + 'unit_of_measurement': 'MB/s', + }), + 'context': , + 'entity_id': 'sensor.nvidia_geforce_rtx_4080_super_gpu_pcie_tx_throughput', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '166.1', + }), + ]) +# --- diff --git a/tests/components/libre_hardware_monitor/test_config_flow.py b/tests/components/libre_hardware_monitor/test_config_flow.py new file mode 100644 index 00000000000..9fcab5daeba --- /dev/null +++ b/tests/components/libre_hardware_monitor/test_config_flow.py @@ -0,0 +1,114 @@ +"""Test the LibreHardwareMonitor config flow.""" + +from unittest.mock import AsyncMock + +from librehardwaremonitor_api import ( + LibreHardwareMonitorConnectionError, + LibreHardwareMonitorNoDevicesError, +) + +from homeassistant.components.libre_hardware_monitor.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import VALID_CONFIG + +from tests.common import MockConfigEntry + + +async def test_create_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that a complete config entry is created.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=VALID_CONFIG + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id is None + + mock_config_entry = result["result"] + assert ( + mock_config_entry.title + == f"{VALID_CONFIG[CONF_HOST]}:{VALID_CONFIG[CONF_PORT]}" + ) + assert mock_config_entry.data == VALID_CONFIG + + assert mock_setup_entry.call_count == 1 + + +async def test_errors_and_flow_recovery( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_lhm_client: AsyncMock +) -> None: + """Test that errors are shown as expected.""" + mock_lhm_client.get_data.side_effect = LibreHardwareMonitorConnectionError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=VALID_CONFIG + ) + + assert result["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + mock_lhm_client.get_data.side_effect = LibreHardwareMonitorNoDevicesError() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=VALID_CONFIG + ) + + assert result["errors"] == {"base": "no_devices"} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + mock_lhm_client.get_data.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=VALID_CONFIG + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + assert mock_setup_entry.call_count == 1 + + +async def test_lhm_server_already_exists( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test we only allow a single entry per server.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=VALID_CONFIG + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_setup_entry.call_count == 0 diff --git a/tests/components/libre_hardware_monitor/test_sensor.py b/tests/components/libre_hardware_monitor/test_sensor.py new file mode 100644 index 00000000000..0ce8f5e1c8f --- /dev/null +++ b/tests/components/libre_hardware_monitor/test_sensor.py @@ -0,0 +1,212 @@ +"""Test the LibreHardwareMonitor sensor.""" + +from dataclasses import replace +from datetime import timedelta +import logging +from types import MappingProxyType +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from librehardwaremonitor_api import ( + LibreHardwareMonitorConnectionError, + LibreHardwareMonitorNoDevicesError, +) +from librehardwaremonitor_api.model import ( + DeviceId, + DeviceName, + LibreHardwareMonitorData, +) +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.libre_hardware_monitor.const import ( + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_sensors_are_created( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test sensors are created.""" + await init_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "error", [LibreHardwareMonitorConnectionError, LibreHardwareMonitorNoDevicesError] +) +async def test_sensors_go_unavailable_in_case_of_error_and_recover_after_successful_retry( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + error: type[Exception], +) -> None: + """Test sensors go unavailable.""" + await init_integration(hass, mock_config_entry) + + initial_states = hass.states.async_all() + assert initial_states == snapshot(name="valid_sensor_data") + + mock_lhm_client.get_data.side_effect = error + + freezer.tick(timedelta(DEFAULT_SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + unavailable_states = hass.states.async_all() + assert all(state.state == STATE_UNAVAILABLE for state in unavailable_states) + + mock_lhm_client.get_data.side_effect = None + + freezer.tick(timedelta(DEFAULT_SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + recovered_states = hass.states.async_all() + assert all(state.state != STATE_UNAVAILABLE for state in recovered_states) + + +async def test_sensors_are_updated( + hass: HomeAssistant, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensors are updated.""" + await init_integration(hass, mock_config_entry) + + entity_id = "sensor.amd_ryzen_7_7800x3d_package_temperature" + + state = hass.states.get(entity_id) + + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "52.8" + + updated_data = dict(mock_lhm_client.get_data.return_value.sensor_data) + updated_data["amdcpu-0-temperature-3"] = replace( + updated_data["amdcpu-0-temperature-3"], value="42,1" + ) + mock_lhm_client.get_data.return_value = replace( + mock_lhm_client.get_data.return_value, + sensor_data=MappingProxyType(updated_data), + ) + + freezer.tick(timedelta(DEFAULT_SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "42.1" + + +async def test_sensor_state_is_unknown_when_no_sensor_data_is_provided( + hass: HomeAssistant, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor state is unknown when sensor data is missing.""" + await init_integration(hass, mock_config_entry) + + entity_id = "sensor.amd_ryzen_7_7800x3d_package_temperature" + + state = hass.states.get(entity_id) + + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "52.8" + + updated_data = dict(mock_lhm_client.get_data.return_value.sensor_data) + del updated_data["amdcpu-0-temperature-3"] + mock_lhm_client.get_data.return_value = replace( + mock_lhm_client.get_data.return_value, + sensor_data=MappingProxyType(updated_data), + ) + + freezer.tick(timedelta(DEFAULT_SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == STATE_UNKNOWN + + +async def test_orphaned_devices_are_removed( + hass: HomeAssistant, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that devices in HA that do not receive updates are removed.""" + await init_integration(hass, mock_config_entry) + + mock_lhm_client.get_data.return_value = LibreHardwareMonitorData( + main_device_ids_and_names=MappingProxyType( + { + DeviceId("amdcpu-0"): DeviceName("AMD Ryzen 7 7800X3D"), + DeviceId("gpu-nvidia-0"): DeviceName("NVIDIA GeForce RTX 4080 SUPER"), + } + ), + sensor_data=mock_lhm_client.get_data.return_value.sensor_data, + ) + + device_registry = dr.async_get(hass) + orphaned_device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, "lpc-nct6687d-0")}, + ) + + with patch.object( + device_registry, + "async_remove_device", + wraps=device_registry.async_update_device, + ) as mock_remove: + freezer.tick(timedelta(DEFAULT_SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_remove.assert_called_once_with(orphaned_device.id) + + +async def test_integration_does_not_log_new_devices_on_first_refresh( + hass: HomeAssistant, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that initial data update does not cause warning about new devices.""" + mock_lhm_client.get_data.return_value = LibreHardwareMonitorData( + main_device_ids_and_names=MappingProxyType( + { + **mock_lhm_client.get_data.return_value.main_device_ids_and_names, + DeviceId("generic-memory"): DeviceName("Generic Memory"), + } + ), + sensor_data=mock_lhm_client.get_data.return_value.sensor_data, + ) + + with caplog.at_level(logging.WARNING): + await init_integration(hass, mock_config_entry) + assert len(caplog.records) == 0