Fix data sensors unit to use MiB or GiB as values are binary based (#184243)

This commit is contained in:
Sab44
2026-10-05 14:50:37 +02:00
committed by GitHub
parent c22c1cca20
commit e130747f6c
5 changed files with 166 additions and 7 deletions
@@ -6,7 +6,11 @@ from homeassistant.const import Platform, UnitOfDataRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN, THROUGHPUT_UNIQUE_ID_FRAGMENT
from .const import (
DOMAIN,
LEGACY_DATA_SIZE_UNIT_EQUIVALENTS,
THROUGHPUT_UNIQUE_ID_FRAGMENT,
)
from .coordinator import (
LibreHardwareMonitorConfigEntry,
LibreHardwareMonitorCoordinator,
@@ -92,6 +96,24 @@ async def async_migrate_entry(
unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
)
# Migrate binary Data units LHM labels MB and GB to MiB and GiB
for reg_entry in registry_entries:
if not (
data_size_unit := LEGACY_DATA_SIZE_UNIT_EQUIVALENTS.get(
reg_entry.unit_of_measurement
)
):
continue
_LOGGER.debug(
"Migrating entity %s unit from %s to %s",
reg_entry.entity_id,
reg_entry.unit_of_measurement,
data_size_unit,
)
entity_registry.async_update_entity(
reg_entry.entity_id, unit_of_measurement=data_size_unit
)
hass.config_entries.async_update_entry(
config_entry, data=config_entry.data, version=2, minor_version=2
)
@@ -1,5 +1,7 @@
"""Constants for the LibreHardwareMonitor integration."""
from homeassistant.const import UnitOfInformation
DOMAIN = "libre_hardware_monitor"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 8085
@@ -7,3 +9,9 @@ DEFAULT_SCAN_INTERVAL = 10
LEGACY_THROUGHPUT_UNIT = "KB/s"
THROUGHPUT_UNIQUE_ID_FRAGMENT = "throughput"
# LHM labels its data sizes MB and GB but calculates them with binary multiples
LEGACY_DATA_SIZE_UNIT_EQUIVALENTS: dict[str | None, str] = {
"MB": UnitOfInformation.MEBIBYTES,
"GB": UnitOfInformation.GIBIBYTES,
}
@@ -13,14 +13,14 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.const import UnitOfDataRate
from homeassistant.const import UnitOfDataRate, UnitOfInformation
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 LibreHardwareMonitorConfigEntry, LibreHardwareMonitorCoordinator
from .const import DOMAIN
from .const import DOMAIN, LEGACY_DATA_SIZE_UNIT_EQUIVALENTS
_LOGGER = logging.getLogger(__name__)
@@ -113,6 +113,9 @@ class LibreHardwareMonitorSensor(
elif device_class is SensorDeviceClass.VOLTAGE:
# Device class default rounds voltages to whole volts
self._attr_suggested_display_precision = 3
elif device_class is SensorDeviceClass.DATA_SIZE:
# LHM versions >= 0.9.7 report data sizes in raw bytes
self._attr_suggested_unit_of_measurement = UnitOfInformation.GIBIBYTES
self._set_state(sensor_data)
self._attr_unique_id: str = f"{entry_id}_{sensor_data.sensor_id}"
@@ -128,7 +131,12 @@ class LibreHardwareMonitorSensor(
def _set_state(self, sensor_data: LibreHardwareMonitorSensorData) -> None:
self._attr_native_value: str | None = sensor_data.value
self._attr_native_unit_of_measurement = sensor_data.unit
# This conversion is only needed for LHM versions < 0.9.7
self._attr_native_unit_of_measurement = (
LEGACY_DATA_SIZE_UNIT_EQUIVALENTS.get(sensor_data.unit, sensor_data.unit)
if sensor_data.type in (SensorType.DATA, SensorType.SMALL_DATA)
else sensor_data.unit
)
self._native_min_value = sensor_data.min
self._native_max_value = sensor_data.max
@@ -2,9 +2,12 @@
from dataclasses import replace
from datetime import timedelta
from types import MappingProxyType
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from librehardwaremonitor_api.model import LibreHardwareMonitorSensorData
from librehardwaremonitor_api.sensor_type import SensorType
import pytest
from homeassistant.components.libre_hardware_monitor.const import (
@@ -16,7 +19,11 @@ from homeassistant.components.libre_hardware_monitor.recorder import (
async_custom_equivalent_units,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfDataRate
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
UnitOfDataRate,
UnitOfInformation,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -172,6 +179,111 @@ async def test_migration_to_sensor_device_classes(
assert updated_config_entry.minor_version == 2
@pytest.mark.parametrize(
(
"sensor_type",
"lhm_value",
"lhm_unit",
"legacy_unit",
"expected_value",
"expected_unit",
),
[
pytest.param(
SensorType.SMALL_DATA,
"733.0",
"MB",
"MB",
733.0,
UnitOfInformation.MEBIBYTES,
id="small_data",
),
pytest.param(
SensorType.DATA,
"16.0",
"GB",
"GB",
16.0,
UnitOfInformation.GIBIBYTES,
id="data",
),
pytest.param(
SensorType.DATA,
"17179869184",
"B",
"GB",
16.0,
UnitOfInformation.GIBIBYTES,
id="data_reported_in_bytes",
),
],
)
@pytest.mark.usefixtures("recorder_mock")
async def test_migration_to_binary_data_size_units(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_lhm_client: AsyncMock,
sensor_type: SensorType,
lhm_value: str,
lhm_unit: str,
legacy_unit: str,
expected_value: float,
expected_unit: str,
) -> None:
"""Test that data sizes LHM labels MB and GB move to MiB and GiB."""
legacy_config_entry = MockConfigEntry(
domain=DOMAIN,
title="192.168.0.20:8085",
data=VALID_CONFIG,
entry_id="test_entry_id",
version=2,
minor_version=1,
)
legacy_config_entry.add_to_hass(hass)
sensor_id = "gpu-nvidia-0-data-0"
mock_lhm_client.get_data.return_value = replace(
mock_lhm_client.get_data.return_value,
sensor_data=MappingProxyType(
{
sensor_id: LibreHardwareMonitorSensorData(
name="GPU Memory Total",
value=lhm_value,
type=sensor_type,
min=lhm_value,
max=lhm_value,
unit=lhm_unit,
device_id="gpu-nvidia-0",
device_name="NVIDIA GeForce RTX 4080 SUPER",
device_type="NVIDIA",
sensor_id=sensor_id,
)
}
),
)
# Set up data size sensor with the unit it had before device classes
object_id = "nvidia_geforce_rtx_4080_gpu_memory_total"
entity_registry.async_get_or_create(
"sensor",
DOMAIN,
f"{legacy_config_entry.entry_id}_{sensor_id}",
suggested_object_id=object_id,
config_entry=legacy_config_entry,
unit_of_measurement=legacy_unit,
)
await init_integration(hass, legacy_config_entry)
entity_entry = entity_registry.async_get(f"sensor.{object_id}")
assert entity_entry.unit_of_measurement == expected_unit
# LHM always calculated binary sizes, so the value is relabelled, not converted
state = hass.states.get(f"sensor.{object_id}")
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == expected_unit
assert float(state.state) == expected_value
@pytest.mark.usefixtures("mock_deprecated_lhm_client")
async def test_deprecated_version_blocks_setup(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
@@ -125,16 +125,25 @@ async def test_sensors_are_created(
),
pytest.param(
SensorType.DATA,
# LHM labels binary gigabytes as GB
"GB",
SensorDeviceClass.DATA_SIZE,
UnitOfInformation.GIGABYTES,
UnitOfInformation.GIBIBYTES,
id="data",
),
pytest.param(
SensorType.DATA,
"B",
SensorDeviceClass.DATA_SIZE,
UnitOfInformation.GIBIBYTES,
id="data_bytes",
),
pytest.param(
SensorType.SMALL_DATA,
# LHM labels binary megabytes as MB
"MB",
SensorDeviceClass.DATA_SIZE,
UnitOfInformation.MEGABYTES,
UnitOfInformation.GIBIBYTES,
id="small_data",
),
pytest.param(