diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 17370d1b463..9b5c9c81683 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,5 +1,8 @@ """Support for SleepIQ from SleepNumber.""" +from __future__ import annotations + import logging +from typing import Any from asyncsleepiq import ( AsyncSleepIQ, @@ -10,14 +13,15 @@ from asyncsleepiq import ( import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, PRESSURE, Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, IS_IN_BED, SLEEP_NUMBER from .coordinator import ( SleepIQData, SleepIQDataUpdateCoordinator, @@ -87,6 +91,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SleepIQAPIException as err: raise ConfigEntryNotReady(str(err) or "Error reading from SleepIQ API") from err + await _async_migrate_unique_ids(hass, entry, gateway) + coordinator = SleepIQDataUpdateCoordinator(hass, gateway, email) pause_coordinator = SleepIQPauseUpdateCoordinator(hass, gateway, email) @@ -110,3 +116,48 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def _async_migrate_unique_ids( + hass: HomeAssistant, entry: ConfigEntry, gateway: AsyncSleepIQ +) -> None: + """Migrate old unique ids.""" + names_to_ids = { + sleeper.name: sleeper.sleeper_id + for bed in gateway.beds.values() + for sleeper in bed.sleepers + } + + bed_ids = {bed.id for bed in gateway.beds.values()} + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: + # Old format for sleeper entities was {bed_id}_{sleeper.name}_{sensor_type}..... + # New format is {sleeper.sleeper_id}_{sensor_type}.... + sensor_types = [IS_IN_BED, PRESSURE, SLEEP_NUMBER] + + old_unique_id = entity_entry.unique_id + parts = old_unique_id.split("_") + + # If it doesn't begin with a bed id or end with one of the sensor types, + # it doesn't need to be migrated + if parts[0] not in bed_ids or not old_unique_id.endswith(tuple(sensor_types)): + return None + + sensor_type = next(filter(old_unique_id.endswith, sensor_types), None) + sleeper_name = "_".join(parts[1:]).removesuffix(f"_{sensor_type}") + sleeper_id = names_to_ids.get(sleeper_name) + + if not sleeper_id: + return None + + new_unique_id = f"{sleeper_id}_{sensor_type}" + + _LOGGER.info( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + return {"new_unique_id": new_unique_id} + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index e610119e2a0..c73988ce638 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -78,4 +78,4 @@ class SleepIQSleeperEntity(SleepIQBedEntity): super().__init__(coordinator, bed) self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[name]}" - self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" + self._attr_unique_id = f"{sleeper.sleeper_id}_{name}" diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index 2b265e19626..bce30ad2393 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -10,11 +10,12 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from tests.components.sleepiq.conftest import ( - BED_ID, BED_NAME, BED_NAME_LOWER, + SLEEPER_L_ID, SLEEPER_L_NAME, SLEEPER_L_NAME_LOWER, + SLEEPER_R_ID, SLEEPER_R_NAME, SLEEPER_R_NAME_LOWER, setup_platform, @@ -41,7 +42,7 @@ async def test_binary_sensors(hass, mock_asyncsleepiq): f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_is_in_bed" ) assert entity - assert entity.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_is_in_bed" + assert entity.unique_id == f"{SLEEPER_L_ID}_is_in_bed" state = hass.states.get( f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" @@ -58,4 +59,4 @@ async def test_binary_sensors(hass, mock_asyncsleepiq): f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" ) assert entity - assert entity.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_is_in_bed" + assert entity.unique_id == f"{SLEEPER_R_ID}_is_in_bed" diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 0aed23c4c50..e468734e063 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -5,14 +5,35 @@ from asyncsleepiq import ( SleepIQTimeoutException, ) -from homeassistant.components.sleepiq.const import DOMAIN +from homeassistant.components.sleepiq.const import ( + DOMAIN, + IS_IN_BED, + PRESSURE, + SLEEP_NUMBER, +) from homeassistant.components.sleepiq.coordinator import UPDATE_INTERVAL from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed -from tests.components.sleepiq.conftest import setup_platform +from tests.common import MockConfigEntry, async_fire_time_changed, mock_registry +from tests.components.sleepiq.conftest import ( + BED_ID, + SLEEPER_L_ID, + SLEEPER_L_NAME, + SLEEPER_L_NAME_LOWER, + SLEEPIQ_CONFIG, + setup_platform, +) + +ENTITY_IS_IN_BED = f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{IS_IN_BED}" +ENTITY_PRESSURE = f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{PRESSURE}" +ENTITY_SLEEP_NUMBER = ( + f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{SLEEP_NUMBER}" +) async def test_unload_entry(hass: HomeAssistant, mock_asyncsleepiq) -> None: @@ -64,3 +85,52 @@ async def test_api_timeout(hass: HomeAssistant, mock_asyncsleepiq) -> None: mock_asyncsleepiq.init_beds.side_effect = SleepIQTimeoutException entry = await setup_platform(hass, None) assert not await hass.config_entries.async_setup(entry.entry_id) + + +async def test_unique_id_migration(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test migration of sensor unique IDs.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=SLEEPIQ_CONFIG, + unique_id=SLEEPIQ_CONFIG[CONF_USERNAME].lower(), + ) + + mock_entry.add_to_hass(hass) + + mock_registry( + hass, + { + ENTITY_IS_IN_BED: er.RegistryEntry( + entity_id=ENTITY_IS_IN_BED, + unique_id=f"{BED_ID}_{SLEEPER_L_NAME}_{IS_IN_BED}", + platform=DOMAIN, + config_entry_id=mock_entry.entry_id, + ), + ENTITY_PRESSURE: er.RegistryEntry( + entity_id=ENTITY_PRESSURE, + unique_id=f"{BED_ID}_{SLEEPER_L_NAME}_{PRESSURE}", + platform=DOMAIN, + config_entry_id=mock_entry.entry_id, + ), + ENTITY_SLEEP_NUMBER: er.RegistryEntry( + entity_id=ENTITY_SLEEP_NUMBER, + unique_id=f"{BED_ID}_{SLEEPER_L_NAME}_{SLEEP_NUMBER}", + platform=DOMAIN, + config_entry_id=mock_entry.entry_id, + ), + }, + ) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + ent_reg = er.async_get(hass) + + sensor_is_in_bed = ent_reg.async_get(ENTITY_IS_IN_BED) + assert sensor_is_in_bed.unique_id == f"{SLEEPER_L_ID}_{IS_IN_BED}" + + sensor_pressure = ent_reg.async_get(ENTITY_PRESSURE) + assert sensor_pressure.unique_id == f"{SLEEPER_L_ID}_{PRESSURE}" + + sensor_sleep_number = ent_reg.async_get(ENTITY_SLEEP_NUMBER) + assert sensor_sleep_number.unique_id == f"{SLEEPER_L_ID}_{SLEEP_NUMBER}" diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index c2d8648ebd5..68ee5319db6 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -4,11 +4,12 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.helpers import entity_registry as er from tests.components.sleepiq.conftest import ( - BED_ID, BED_NAME, BED_NAME_LOWER, + SLEEPER_L_ID, SLEEPER_L_NAME, SLEEPER_L_NAME_LOWER, + SLEEPER_R_ID, SLEEPER_R_NAME, SLEEPER_R_NAME_LOWER, setup_platform, @@ -34,7 +35,7 @@ async def test_sleepnumber_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_sleepnumber" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_sleep_number" + assert entry.unique_id == f"{SLEEPER_L_ID}_sleep_number" state = hass.states.get( f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" @@ -50,7 +51,7 @@ async def test_sleepnumber_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_sleep_number" + assert entry.unique_id == f"{SLEEPER_R_ID}_sleep_number" async def test_pressure_sensors(hass, mock_asyncsleepiq): @@ -72,7 +73,7 @@ async def test_pressure_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_pressure" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_pressure" + assert entry.unique_id == f"{SLEEPER_L_ID}_pressure" state = hass.states.get( f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_pressure" @@ -88,4 +89,4 @@ async def test_pressure_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_pressure" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_pressure" + assert entry.unique_id == f"{SLEEPER_R_ID}_pressure"