Compare commits

...

4 Commits

Author SHA1 Message Date
G Johansson
fde352b814 Mods 2025-10-25 19:45:44 +00:00
G Johansson
dace68bd7b Fixes 2025-10-25 19:45:43 +00:00
G Johansson
b72db367cf docstring 2025-10-25 19:45:43 +00:00
G Johansson
6d76ecf992 Non string unique id in entity registry now raise 2025-10-25 19:45:43 +00:00
17 changed files with 35 additions and 383 deletions

View File

@@ -3,7 +3,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .hub import PulseHub
@@ -19,8 +18,6 @@ async def async_setup_entry(
) -> bool:
"""Set up Rollease Acmeda Automate hub from a config entry."""
await _migrate_unique_ids(hass, config_entry)
hub = PulseHub(hass, config_entry)
if not await hub.async_setup():
@@ -32,19 +29,6 @@ async def async_setup_entry(
return True
async def _migrate_unique_ids(hass: HomeAssistant, entry: AcmedaConfigEntry) -> None:
"""Migrate pre-config flow unique ids."""
entity_registry = er.async_get(hass)
registry_entries = er.async_entries_for_config_entry(
entity_registry, entry.entry_id
)
for reg_entry in registry_entries:
if isinstance(reg_entry.unique_id, int): # type: ignore[unreachable]
entity_registry.async_update_entity( # type: ignore[unreachable]
reg_entry.entity_id, new_unique_id=str(reg_entry.unique_id)
)
async def async_unload_entry(
hass: HomeAssistant, config_entry: AcmedaConfigEntry
) -> bool:

View File

@@ -34,7 +34,7 @@ from homeassistant.components.climate import (
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.unit_conversion import TemperatureConverter
@@ -105,7 +105,6 @@ async def async_setup_entry(
heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE)
data = entry.runtime_data
_async_migrate_unique_id(hass, data.devices)
async_add_entities(
[
HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp)
@@ -115,21 +114,6 @@ async def async_setup_entry(
remove_stale_devices(hass, entry, data.devices)
def _async_migrate_unique_id(
hass: HomeAssistant, devices: dict[str, SomeComfortDevice]
) -> None:
"""Migrate entities to string."""
entity_registry = er.async_get(hass)
for device in devices.values():
entity_id = entity_registry.async_get_entity_id(
"climate", DOMAIN, device.deviceid
)
if entity_id is not None:
entity_registry.async_update_entity(
entity_id, new_unique_id=str(device.deviceid)
)
def remove_stale_devices(
hass: HomeAssistant,
config_entry: HoneywellConfigEntry,

View File

@@ -1,7 +1,6 @@
"""The Hunter Douglas PowerView integration."""
import logging
from typing import TYPE_CHECKING
from aiopvapi.resources.model import PowerviewData
from aiopvapi.rooms import Rooms
@@ -11,7 +10,7 @@ from aiopvapi.shades import Shades
from homeassistant.const import CONF_API_VERSION, CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN, HUB_EXCEPTIONS, MANUFACTURER
from .coordinator import PowerviewShadeUpdateCoordinator
@@ -138,7 +137,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: PowerviewConfigEntry)
if entry.minor_version == 1:
if entry.unique_id is None:
await _async_add_missing_entry_unique_id(hass, entry)
await _migrate_unique_ids(hass, entry)
hass.config_entries.async_update_entry(entry, minor_version=2)
_LOGGER.debug("Migrated to version %s.%s", entry.version, entry.minor_version)
@@ -156,29 +154,3 @@ async def _async_add_missing_entry_unique_id(
hass.config_entries.async_update_entry(
entry, unique_id=api.device_info.serial_number
)
async def _migrate_unique_ids(hass: HomeAssistant, entry: PowerviewConfigEntry) -> None:
"""Migrate int based unique ids to str."""
entity_registry = er.async_get(hass)
registry_entries = er.async_entries_for_config_entry(
entity_registry, entry.entry_id
)
if TYPE_CHECKING:
assert entry.unique_id
for reg_entry in registry_entries:
if isinstance(reg_entry.unique_id, int) or (
isinstance(reg_entry.unique_id, str)
and not reg_entry.unique_id.startswith(entry.unique_id)
):
_LOGGER.debug(
"Migrating %s: %s to %s_%s",
reg_entry.entity_id,
reg_entry.unique_id,
entry.unique_id,
reg_entry.unique_id,
)
entity_registry.async_update_entity(
reg_entry.entity_id,
new_unique_id=f"{entry.unique_id}_{reg_entry.unique_id}",
)

View File

@@ -36,7 +36,7 @@ class NexiaEntity(CoordinatorEntity[NexiaDataUpdateCoordinator]):
def __init__(self, coordinator: NexiaDataUpdateCoordinator, unique_id: str) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = unique_id
self._attr_unique_id = str(unique_id)
class NexiaThermostatEntity(NexiaEntity):

View File

@@ -69,7 +69,7 @@ class NukiDeviceEntity[_NukiDeviceT: NukiDevice](NukiEntity[_NukiDeviceT], LockE
@property
def unique_id(self) -> str | None:
"""Return a unique ID."""
return self._nuki_device.nuki_id
return str(self._nuki_device.nuki_id)
@property
def available(self) -> bool:

View File

@@ -104,33 +104,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: RingConfigEntry) -> bo
_LOGGER.debug(
"Migrating from version %s.%s", entry_version, entry_minor_version
)
# Migrate non-str unique ids
# This step used to run unconditionally from async_setup_entry
entity_registry = er.async_get(hass)
@callback
def _async_str_unique_id_migrator(
entity_entry: er.RegistryEntry,
) -> dict[str, str] | None:
# Old format for camera and light was int
unique_id = cast(str | int, entity_entry.unique_id)
if isinstance(unique_id, int):
new_unique_id = str(unique_id)
if existing_entity_id := entity_registry.async_get_entity_id(
entity_entry.domain, entity_entry.platform, new_unique_id
):
_LOGGER.error(
"Cannot migrate to unique_id '%s', already exists for '%s', "
"You may have to delete unavailable ring entities",
new_unique_id,
existing_entity_id,
)
return None
_LOGGER.debug("Fixing non string unique id %s", entity_entry.unique_id)
return {"new_unique_id": new_unique_id}
return None
await er.async_migrate_entries(hass, entry_id, _async_str_unique_id_migrator)
# Migrate the hardware id
hardware_id = str(uuid.uuid4())

View File

@@ -733,26 +733,12 @@ def _validate_item(
entity_category: EntityCategory | None | UndefinedType = None,
hidden_by: RegistryEntryHider | None | UndefinedType = None,
old_config_subentry_id: str | None = None,
report_non_string_unique_id: bool = True,
unique_id: str | Hashable | UndefinedType | Any,
) -> None:
"""Validate entity registry item."""
if unique_id is not UNDEFINED and not isinstance(unique_id, Hashable):
if unique_id is not UNDEFINED and not isinstance(unique_id, str):
raise TypeError(f"unique_id must be a string, got {unique_id}")
if (
report_non_string_unique_id
and unique_id is not UNDEFINED
and not isinstance(unique_id, str)
):
# In HA Core 2025.10, we should fail if unique_id is not a string
report_issue = async_suggest_report_issue(hass, integration_domain=platform)
_LOGGER.error(
"'%s' from integration %s has a non string unique_id '%s', please %s",
domain,
platform,
unique_id,
report_issue,
)
if config_entry_id and config_entry_id is not UNDEFINED:
if not hass.config_entries.async_get_entry(config_entry_id):
raise ValueError(
@@ -1506,7 +1492,6 @@ class EntityRegistry(BaseRegistry):
self.hass,
domain,
entity["platform"],
report_non_string_unique_id=False,
unique_id=entity["unique_id"],
)
except (TypeError, ValueError) as err:
@@ -1584,7 +1569,6 @@ class EntityRegistry(BaseRegistry):
self.hass,
domain,
entity["platform"],
report_non_string_unique_id=False,
unique_id=entity["unique_id"],
)
except (TypeError, ValueError):

View File

@@ -1,31 +0,0 @@
"""Define tests for the Acmeda config flow."""
import pytest
from homeassistant.components.acmeda.const import DOMAIN
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_hub_run")
async def test_cover_id_migration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating unique id."""
mock_config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
COVER_DOMAIN, DOMAIN, 1234567890123, config_entry=mock_config_entry
)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entities = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entities) == 1
assert entities[0].unique_id == "1234567890123"

View File

@@ -1,30 +0,0 @@
"""Define tests for the Acmeda config flow."""
import pytest
from homeassistant.components.acmeda.const import DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_hub_run")
async def test_sensor_id_migration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating unique id."""
mock_config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
SENSOR_DOMAIN, DOMAIN, 1234567890123, config_entry=mock_config_entry
)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entities = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entities) == 1
assert entities[0].unique_id == "1234567890123"

View File

@@ -24,5 +24,5 @@ def config_entry_fixture() -> MockConfigEntry:
data=data,
options={CONF_TTS_PAUSE_TIME: 0},
source=SOURCE_USER,
entry_id=1,
entry_id="1",
)

View File

@@ -114,7 +114,7 @@ async def test_no_aid_collision(
for unique_id in range(202):
ent = entity_registry.async_get_or_create(
"light", "device", unique_id, device_id=device_entry.id
"light", "device", str(unique_id), device_id=device_entry.id
)
hass.states.async_set(ent.entity_id, "on")
aid = aid_storage.get_or_allocate_aid_for_entity_id(ent.entity_id)

View File

@@ -29,7 +29,6 @@ from homeassistant.components.climate import (
HVACMode,
)
from homeassistant.components.honeywell.climate import (
DOMAIN,
MODE_PERMANENT_HOLD,
MODE_TEMPORARY_HOLD,
PRESET_HOLD,
@@ -41,7 +40,6 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
@@ -1193,26 +1191,6 @@ async def test_async_update_errors(
assert state.state == "unavailable"
async def test_unique_id(
hass: HomeAssistant,
device: MagicMock,
config_entry: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test unique id convert to string."""
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
Platform.CLIMATE,
DOMAIN,
device.deviceid,
config_entry=config_entry,
suggested_object_id=device.name,
)
await init_integration(hass, config_entry)
entity_entry = entity_registry.async_get(f"climate.{device.name}")
assert entity_entry.unique_id == str(device.deviceid)
async def test_preset_mode(
hass: HomeAssistant,
device: MagicMock,

View File

@@ -9,7 +9,6 @@ from homeassistant.components.hunterdouglas_powerview.const import DOMAIN
from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
@@ -354,54 +353,3 @@ async def test_form_unsupported_device(
assert result3["result"].unique_id == MOCK_SERIAL
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
@pytest.mark.parametrize("api_version", [1, 2, 3])
async def test_migrate_entry(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
api_version: int,
) -> None:
"""Test migrate to newest version."""
entry = MockConfigEntry(
domain=DOMAIN,
data={"host": "1.2.3.4"},
unique_id=MOCK_SERIAL,
version=1,
minor_version=1,
)
entry.add_to_hass(hass)
# Add entries with int unique_id
entity_registry.async_get_or_create(
domain="cover",
platform="hunterdouglas_powerview",
unique_id=123,
config_entry=entry,
)
# Add entries with a str unique_id not starting with entry.unique_id
entity_registry.async_get_or_create(
domain="cover",
platform="hunterdouglas_powerview",
unique_id="old_unique_id",
config_entry=entry,
)
assert entry.version == 1
assert entry.minor_version == 1
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.version == 1
assert entry.minor_version == 2
# Reload the registry entries
registry_entries = er.async_entries_for_config_entry(
entity_registry, entry.entry_id
)
# Ensure the IDs have been migrated
for reg_entry in registry_entries:
assert reg_entry.unique_id.startswith(f"{entry.unique_id}_")

View File

@@ -30,7 +30,7 @@
'suggested_object_id': None,
'supported_features': <LockEntityFeature: 1>,
'translation_key': 'nuki_lock',
'unique_id': 2,
'unique_id': '2',
'unit_of_measurement': None,
})
# ---
@@ -79,7 +79,7 @@
'suggested_object_id': None,
'supported_features': <LockEntityFeature: 1>,
'translation_key': 'nuki_lock',
'unique_id': 1,
'unique_id': '1',
'unit_of_measurement': None,
})
# ---

View File

@@ -241,7 +241,7 @@ async def test_fix_unique_id_duplicate(
(
SERIAL_NUMBER,
SERIAL_NUMBER,
SERIAL_NUMBER,
str(SERIAL_NUMBER),
str(SERIAL_NUMBER),
MAC_ADDRESS_UNIQUE_ID,
MAC_ADDRESS_UNIQUE_ID,
@@ -257,7 +257,7 @@ async def test_fix_unique_id_duplicate(
(
SERIAL_NUMBER,
SERIAL_NUMBER,
SERIAL_NUMBER,
str(SERIAL_NUMBER),
SERIAL_NUMBER,
MAC_ADDRESS_UNIQUE_ID,
MAC_ADDRESS_UNIQUE_ID,

View File

@@ -9,7 +9,6 @@ from ring_doorbell import AuthenticationError, Ring, RingError, RingTimeout
from homeassistant.components import ring
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.ring import DOMAIN
from homeassistant.components.ring.const import (
CONF_CONFIG_ENTRY_MINOR_VERSION,
@@ -282,107 +281,6 @@ async def test_error_on_device_update(
assert log_msg not in caplog.text
@pytest.mark.parametrize(
("domain", "old_unique_id", "new_unique_id"),
[
pytest.param(LIGHT_DOMAIN, 123456, "123456", id="Light integer"),
pytest.param(
CAMERA_DOMAIN,
654321,
"654321-last_recording",
id="Camera integer",
),
],
)
async def test_update_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
caplog: pytest.LogCaptureFixture,
mock_ring_client,
domain: str,
old_unique_id: int | str,
new_unique_id: str,
) -> None:
"""Test unique_id update of integration."""
entry = MockConfigEntry(
title="Ring",
domain=DOMAIN,
data={
CONF_USERNAME: "foo@bar.com",
"token": {"access_token": "mock-token"},
},
unique_id="foo@bar.com",
minor_version=1,
)
entry.add_to_hass(hass)
entity = entity_registry.async_get_or_create(
domain=domain,
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=entry,
)
assert entity.unique_id == old_unique_id
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_migrated = entity_registry.async_get(entity.entity_id)
assert entity_migrated
assert entity_migrated.unique_id == new_unique_id
assert (f"Fixing non string unique id {old_unique_id}") in caplog.text
assert entry.minor_version == CONF_CONFIG_ENTRY_MINOR_VERSION
async def test_update_unique_id_existing(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
caplog: pytest.LogCaptureFixture,
mock_ring_client,
) -> None:
"""Test unique_id update of integration."""
old_unique_id = 123456
entry = MockConfigEntry(
title="Ring",
domain=DOMAIN,
data={
CONF_USERNAME: "foo@bar.com",
"token": {"access_token": "mock-token"},
},
unique_id="foo@bar.com",
minor_version=1,
)
entry.add_to_hass(hass)
entity = entity_registry.async_get_or_create(
domain=CAMERA_DOMAIN,
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=entry,
)
entity_existing = entity_registry.async_get_or_create(
domain=CAMERA_DOMAIN,
platform=DOMAIN,
unique_id=str(old_unique_id),
config_entry=entry,
)
assert entity.unique_id == old_unique_id
assert entity_existing.unique_id == str(old_unique_id)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_not_migrated = entity_registry.async_get(entity.entity_id)
entity_existing = entity_registry.async_get(entity_existing.entity_id)
assert entity_not_migrated
assert entity_existing
assert entity_not_migrated.unique_id == old_unique_id
assert (
f"Cannot migrate to unique_id '{old_unique_id}', "
f"already exists for '{entity_existing.entity_id}', "
"You may have to delete unavailable ring entities"
) in caplog.text
assert entry.minor_version == CONF_CONFIG_ENTRY_MINOR_VERSION
async def test_update_unique_id_camera_update(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,

View File

@@ -657,24 +657,29 @@ async def test_load_bad_data(
await er.async_load(hass)
registry = er.async_get(hass)
assert len(registry.entities) == 1
assert set(registry.entities.keys()) == {"test.test1"}
assert len(registry.entities) == 0
assert set(registry.entities.keys()) == set()
assert len(registry.deleted_entities) == 1
assert set(registry.deleted_entities.keys()) == {("test", "super_platform", 234)}
assert len(registry.deleted_entities) == 0
assert set(registry.deleted_entities.keys()) == set()
assert (
"'test' from integration super_platform has a non string unique_id '123', "
"please create a bug report" not in caplog.text
"'test.test1' from integration super_platform could not be loaded:"
" 'unique_id must be a string, got 123', please create a bug report"
in caplog.text
)
assert (
"'test' from integration super_platform has a non string unique_id '234', "
"please create a bug report" not in caplog.text
"'test.test2' from integration super_platform could not be loaded:"
" 'unique_id must be a string, got ['not', 'valid']', please create a bug report"
in caplog.text
)
assert (
"Entity registry entry 'test.test2' from integration super_platform could not "
"be loaded: 'unique_id must be a string, got ['not', 'valid']', please create "
"a bug report" in caplog.text
"'test.test3' from integration super_platform could not be loaded:"
not in caplog.text
)
assert (
"'test.test4' from integration super_platform could not be loaded:"
not in caplog.text
)
@@ -2902,32 +2907,19 @@ async def test_hidden_by_str_not_allowed(entity_registry: er.EntityRegistry) ->
)
async def test_unique_id_non_hashable(entity_registry: er.EntityRegistry) -> None:
"""Test unique_id which is not hashable."""
async def test_unique_id_non_string(entity_registry: er.EntityRegistry) -> None:
"""Test unique_id which is not a string."""
with pytest.raises(TypeError):
entity_registry.async_get_or_create("light", "hue", ["not", "valid"])
with pytest.raises(TypeError):
entity_registry.async_get_or_create("light", "hue", 1234)
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
with pytest.raises(TypeError):
entity_registry.async_update_entity(entity_id, new_unique_id=["not", "valid"])
async def test_unique_id_non_string(
entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture
) -> None:
"""Test unique_id which is not a string."""
entity_registry.async_get_or_create("light", "hue", 1234)
assert (
"'light' from integration hue has a non string unique_id '1234', "
"please create a bug report" in caplog.text
)
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
entity_registry.async_update_entity(entity_id, new_unique_id=2345)
assert (
"'light' from integration hue has a non string unique_id '2345', "
"please create a bug report" in caplog.text
)
with pytest.raises(TypeError):
entity_registry.async_update_entity(entity_id, new_unique_id=1234)
@pytest.mark.parametrize(