1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-19 18:38:58 +00:00

Fix unique IDs and migrate v1 entries (#155319)

This commit is contained in:
Sab44
2025-10-28 10:07:37 +01:00
committed by GitHub
parent c9d68ddd5c
commit 28bee6d1aa
8 changed files with 193 additions and 37 deletions

View File

@@ -2,9 +2,13 @@
from __future__ import annotations
import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN
from .coordinator import (
LibreHardwareMonitorConfigEntry,
LibreHardwareMonitorCoordinator,
@@ -12,6 +16,61 @@ from .coordinator import (
PLATFORMS = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: LibreHardwareMonitorConfigEntry
) -> bool:
"""Migrate non-unique entity and device ids."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
# Migrate entity identifiers
entity_registry = er.async_get(hass)
registry_entries = er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
for reg_entry in registry_entries:
new_entity_id = f"{config_entry.entry_id}_{reg_entry.unique_id[4:]}"
_LOGGER.debug(
"Migrating entity %s unique id from %s to %s",
reg_entry.entity_id,
reg_entry.unique_id,
new_entity_id,
)
entity_registry.async_update_entity(
reg_entry.entity_id, new_unique_id=new_entity_id
)
# Migrate device identifiers
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
registry=device_registry, config_entry_id=config_entry.entry_id
)
for device in device_entries:
old_device_id = next(iter(device.identifiers))[1]
new_device_id = f"{config_entry.entry_id}_{old_device_id}"
_LOGGER.debug(
"Migrating device %s unique id from %s to %s",
device.name,
old_device_id,
new_device_id,
)
device_registry.async_update_device(
device_id=device.id,
new_identifiers={(DOMAIN, new_device_id)},
)
hass.config_entries.async_update_entry(
config_entry, data=config_entry.data, version=2
)
_LOGGER.debug("Migration to version 2 successful")
return True
return True
async def async_setup_entry(
hass: HomeAssistant, config_entry: LibreHardwareMonitorConfigEntry

View File

@@ -30,7 +30,7 @@ CONFIG_SCHEMA = vol.Schema(
class LibreHardwareMonitorConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for LibreHardwareMonitor."""
VERSION = 1
VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -114,7 +114,7 @@ class LibreHardwareMonitorCoordinator(DataUpdateCoordinator[LibreHardwareMonitor
device_registry = dr.async_get(self.hass)
for device_id in orphaned_devices:
if device := device_registry.async_get_device(
identifiers={(DOMAIN, device_id)}
identifiers={(DOMAIN, f"{self.config_entry.entry_id}_{device_id}")}
):
device_registry.async_update_device(
device_id=device.id,

View File

@@ -10,11 +10,9 @@ 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 . import LibreHardwareMonitorConfigEntry, 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"
@@ -30,7 +28,7 @@ async def async_setup_entry(
lhm_coordinator = config_entry.runtime_data
async_add_entities(
LibreHardwareMonitorSensor(lhm_coordinator, sensor_data)
LibreHardwareMonitorSensor(lhm_coordinator, config_entry.entry_id, sensor_data)
for sensor_data in lhm_coordinator.data.sensor_data.values()
)
@@ -46,6 +44,7 @@ class LibreHardwareMonitorSensor(
def __init__(
self,
coordinator: LibreHardwareMonitorCoordinator,
entry_id: str,
sensor_data: LibreHardwareMonitorSensorData,
) -> None:
"""Initialize an LibreHardwareMonitor sensor."""
@@ -58,13 +57,13 @@ class LibreHardwareMonitorSensor(
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._attr_unique_id: str = f"{entry_id}_{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)},
identifiers={(DOMAIN, f"{entry_id}_{sensor_data.device_id}")},
name=sensor_data.device_name,
model=sensor_data.device_type,
)

View File

@@ -31,6 +31,8 @@ def mock_config_entry() -> MockConfigEntry:
domain=DOMAIN,
title="192.168.0.20:8085",
data=VALID_CONFIG,
entry_id="test_entry_id",
version=2,
)

View File

@@ -32,7 +32,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-amdcpu-0-temperature-2',
'unique_id': 'test_entry_id_amdcpu-0-temperature-2',
'unit_of_measurement': '°C',
})
# ---
@@ -86,7 +86,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-amdcpu-0-load-0',
'unique_id': 'test_entry_id_amdcpu-0-load-0',
'unit_of_measurement': '%',
})
# ---
@@ -140,7 +140,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-amdcpu-0-power-0',
'unique_id': 'test_entry_id_amdcpu-0-power-0',
'unit_of_measurement': 'W',
})
# ---
@@ -194,7 +194,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-amdcpu-0-temperature-3',
'unique_id': 'test_entry_id_amdcpu-0-temperature-3',
'unit_of_measurement': '°C',
})
# ---
@@ -248,7 +248,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-amdcpu-0-voltage-3',
'unique_id': 'test_entry_id_amdcpu-0-voltage-3',
'unit_of_measurement': 'V',
})
# ---
@@ -302,7 +302,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-amdcpu-0-voltage-2',
'unique_id': 'test_entry_id_amdcpu-0-voltage-2',
'unit_of_measurement': 'V',
})
# ---
@@ -356,7 +356,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-voltage-0',
'unique_id': 'test_entry_id_lpc-nct6687d-0-voltage-0',
'unit_of_measurement': 'V',
})
# ---
@@ -410,7 +410,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-voltage-1',
'unique_id': 'test_entry_id_lpc-nct6687d-0-voltage-1',
'unit_of_measurement': 'V',
})
# ---
@@ -464,7 +464,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-fan-0',
'unique_id': 'test_entry_id_lpc-nct6687d-0-fan-0',
'unit_of_measurement': 'RPM',
})
# ---
@@ -518,7 +518,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-temperature-0',
'unique_id': 'test_entry_id_lpc-nct6687d-0-temperature-0',
'unit_of_measurement': '°C',
})
# ---
@@ -572,7 +572,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-fan-1',
'unique_id': 'test_entry_id_lpc-nct6687d-0-fan-1',
'unit_of_measurement': 'RPM',
})
# ---
@@ -626,7 +626,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-fan-2',
'unique_id': 'test_entry_id_lpc-nct6687d-0-fan-2',
'unit_of_measurement': None,
})
# ---
@@ -679,7 +679,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-temperature-1',
'unique_id': 'test_entry_id_lpc-nct6687d-0-temperature-1',
'unit_of_measurement': '°C',
})
# ---
@@ -733,7 +733,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-lpc-nct6687d-0-voltage-2',
'unique_id': 'test_entry_id_lpc-nct6687d-0-voltage-2',
'unit_of_measurement': 'V',
})
# ---
@@ -787,7 +787,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-clock-0',
'unique_id': 'test_entry_id_gpu-nvidia-0-clock-0',
'unit_of_measurement': 'MHz',
})
# ---
@@ -841,7 +841,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-load-0',
'unique_id': 'test_entry_id_gpu-nvidia-0-load-0',
'unit_of_measurement': '%',
})
# ---
@@ -895,7 +895,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-temperature-0',
'unique_id': 'test_entry_id_gpu-nvidia-0-temperature-0',
'unit_of_measurement': '°C',
})
# ---
@@ -949,7 +949,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-fan-1',
'unique_id': 'test_entry_id_gpu-nvidia-0-fan-1',
'unit_of_measurement': 'RPM',
})
# ---
@@ -1003,7 +1003,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-fan-2',
'unique_id': 'test_entry_id_gpu-nvidia-0-fan-2',
'unit_of_measurement': 'RPM',
})
# ---
@@ -1057,7 +1057,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-temperature-2',
'unique_id': 'test_entry_id_gpu-nvidia-0-temperature-2',
'unit_of_measurement': '°C',
})
# ---
@@ -1111,7 +1111,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-clock-4',
'unique_id': 'test_entry_id_gpu-nvidia-0-clock-4',
'unit_of_measurement': 'MHz',
})
# ---
@@ -1165,7 +1165,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-load-1',
'unique_id': 'test_entry_id_gpu-nvidia-0-load-1',
'unit_of_measurement': '%',
})
# ---
@@ -1219,7 +1219,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-power-0',
'unique_id': 'test_entry_id_gpu-nvidia-0-power-0',
'unit_of_measurement': 'W',
})
# ---
@@ -1273,7 +1273,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-throughput-1',
'unique_id': 'test_entry_id_gpu-nvidia-0-throughput-1',
'unit_of_measurement': 'MB/s',
})
# ---
@@ -1327,7 +1327,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'lhm-gpu-nvidia-0-load-2',
'unique_id': 'test_entry_id_gpu-nvidia-0-load-2',
'unit_of_measurement': '%',
})
# ---

View File

@@ -0,0 +1,91 @@
"""Tests for the LibreHardwareMonitor init."""
import logging
from homeassistant.components.libre_hardware_monitor.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import init_integration
from .conftest import VALID_CONFIG
from tests.common import MockConfigEntry
_LOGGER = logging.getLogger(__name__)
async def test_migration_to_unique_ids(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that non-unique legacy entity and device IDs are updated."""
legacy_config_entry_v1 = MockConfigEntry(
domain=DOMAIN,
title="192.168.0.20:8085",
data=VALID_CONFIG,
entry_id="test_entry_id",
version=1,
)
legacy_config_entry_v1.add_to_hass(hass)
# Set up devices with legacy device ID
legacy_device_ids = ["amdcpu-0", "gpu-nvidia-0", "lpc-nct6687d-0"]
for device_id in legacy_device_ids:
device_registry.async_get_or_create(
config_entry_id=legacy_config_entry_v1.entry_id,
identifiers={(DOMAIN, device_id)}, # Old format without entry_id prefix
name=f"Test Device {device_id}",
)
# Set up entity with legacy entity ID
existing_sensor_id = "lpc-nct6687d-0-voltage-0"
legacy_entity_id = f"lhm-{existing_sensor_id}"
entity_object_id = "sensor.msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage"
entity_registry.async_get_or_create(
"sensor",
DOMAIN,
legacy_entity_id,
suggested_object_id="msi_mag_b650m_mortar_wifi_ms_7d76_12v_voltage",
config_entry=legacy_config_entry_v1,
)
# Verify state before migration
device_entries_before = dr.async_entries_for_config_entry(
registry=device_registry, config_entry_id=legacy_config_entry_v1.entry_id
)
assert {
next(iter(device.identifiers))[1] for device in device_entries_before
} == set(legacy_device_ids)
assert (
entity_registry.async_get_entity_id("sensor", DOMAIN, legacy_entity_id)
== entity_object_id
)
await init_integration(hass, legacy_config_entry_v1)
# Verify state after migration
device_entries_after = dr.async_entries_for_config_entry(
registry=device_registry, config_entry_id=legacy_config_entry_v1.entry_id
)
expected_unique_device_ids = [
f"{legacy_config_entry_v1.entry_id}_{device_id}"
for device_id in legacy_device_ids
]
assert {
next(iter(device.identifiers))[1] for device in device_entries_after
} == set(expected_unique_device_ids)
entity_entry = entity_registry.async_get(entity_object_id)
assert entity_entry is not None, "Entity should exist after migration"
new_unique_entity_id = f"{legacy_config_entry_v1.entry_id}_{existing_sensor_id}"
assert entity_entry.unique_id == new_unique_entity_id, (
f"Unique ID not migrated: {entity_entry.unique_id}"
)
assert (
entity_registry.async_get_entity_id("sensor", DOMAIN, legacy_entity_id) is None
)

View File

@@ -88,11 +88,10 @@ async def test_sensors_are_updated(
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test sensors are updated."""
"""Test sensors are updated with properly formatted values."""
await init_integration(hass, mock_config_entry)
entity_id = "sensor.amd_ryzen_7_7800x3d_package_temperature"
state = hass.states.get(entity_id)
assert state
@@ -175,7 +174,7 @@ async def test_orphaned_devices_are_removed(
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")},
identifiers={(DOMAIN, f"{mock_config_entry.entry_id}_lpc-nct6687d-0")},
)
with patch.object(
@@ -209,4 +208,10 @@ async def test_integration_does_not_log_new_devices_on_first_refresh(
with caplog.at_level(logging.WARNING):
await init_integration(hass, mock_config_entry)
assert len(caplog.records) == 0
libre_hardware_monitor_logs = [
record
for record in caplog.records
if record.name.startswith("homeassistant.components.libre_hardware_monitor")
]
assert len(libre_hardware_monitor_logs) == 0