diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 8fcc56e449f9ce..5cccd54a32aab2 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -21,6 +21,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -151,7 +152,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err gateway_din = base_info.gateway_din - if gateway_din and entry.unique_id is not None and is_ip_address(entry.unique_id): + if entry.unique_id is not None and is_ip_address(entry.unique_id): hass.config_entries.async_update_entry(entry, unique_id=gateway_din) runtime_data = PowerwallRuntimeData( @@ -178,11 +179,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = runtime_data + await async_migrate_entity_unique_ids(hass, entry, base_info) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True +async def async_migrate_entity_unique_ids( + hass: HomeAssistant, entry: ConfigEntry, base_info: PowerwallBaseInfo +) -> None: + """Migrate old entity unique ids to use gateway_din.""" + old_base_unique_id = "_".join(base_info.serial_numbers) + new_base_unique_id = base_info.gateway_din + + dev_reg = dr.async_get(hass) + if device := dev_reg.async_get_device(identifiers={(DOMAIN, old_base_unique_id)}): + dev_reg.async_update_device( + device.id, new_identifiers={(DOMAIN, new_base_unique_id)} + ) + + ent_reg = er.async_get(hass) + for ent_entry in er.async_entries_for_config_entry(ent_reg, entry.entry_id): + current_unique_id = ent_entry.unique_id + if current_unique_id.startswith(old_base_unique_id): + new_unique_id = f"{new_base_unique_id}{current_unique_id.removeprefix(old_base_unique_id)}" + ent_reg.async_update_entity( + ent_entry.entity_id, new_unique_id=new_unique_id + ) + + async def _login_and_fetch_base_info( power_wall: Powerwall, host: str, password: str | None ) -> PowerwallBaseInfo: diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index f0cfec2cbc585b..0ee4249a8e9105 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -29,8 +29,7 @@ def __init__(self, powerwall_data: PowerwallRuntimeData) -> None: assert coordinator is not None super().__init__(coordinator) self.power_wall = powerwall_data[POWERWALL_API] - # The serial numbers of the powerwalls are unique to every site - self.base_unique_id = "_".join(base_info.serial_numbers) + self.base_unique_id = base_info.gateway_din self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.base_unique_id)}, manufacturer=MANUFACTURER, diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index d67a21a0d538c3..65213065d0ed0c 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -21,7 +21,7 @@ class PowerwallBaseInfo: """Base information for the powerwall integration.""" - gateway_din: None | str + gateway_din: str site_info: SiteInfoResponse status: PowerwallStatusResponse device_type: DeviceType diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index a58c30f332e546..bca176386293bd 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -16,10 +16,10 @@ STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.util.dt as dt_util -from .mocks import _mock_powerwall_with_fixtures +from .mocks import MOCK_GATEWAY_DIN, _mock_powerwall_with_fixtures from tests.common import MockConfigEntry, async_fire_time_changed @@ -44,7 +44,7 @@ async def test_sensors( device_registry = dr.async_get(hass) reg_device = device_registry.async_get_device( - identifiers={("powerwall", "TG0123456789AB_TG9876543210BA")}, + identifiers={("powerwall", MOCK_GATEWAY_DIN)}, ) assert reg_device.model == "PowerWall 2 (GW1)" assert reg_device.sw_version == "1.50.1 c58c2df3" @@ -173,3 +173,64 @@ async def test_sensors_with_empty_meters(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("sensor.mysite_solar_power") is None + + +async def test_unique_id_migrate( + hass: HomeAssistant, entity_registry_enabled_by_default: None +) -> None: + """Test we can migrate unique ids of the sensors.""" + device_registry = dr.async_get(hass) + ent_reg = er.async_get(hass) + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) + config_entry.add_to_hass(hass) + + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + old_unique_id = "_".join(sorted(["TG0123456789AB", "TG9876543210BA"])) + new_unique_id = MOCK_GATEWAY_DIN + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("powerwall", old_unique_id)}, + manufacturer="Tesla", + ) + old_mysite_load_power_entity = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + unique_id=f"{old_unique_id}_load_instant_power", + suggested_object_id="mysite_load_power", + config_entry=config_entry, + ) + assert old_mysite_load_power_entity.entity_id == "sensor.mysite_load_power" + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + reg_device = device_registry.async_get_device( + identifiers={("powerwall", MOCK_GATEWAY_DIN)}, + ) + old_reg_device = device_registry.async_get_device( + identifiers={("powerwall", old_unique_id)}, + ) + assert old_reg_device is None + assert reg_device is not None + + assert ( + ent_reg.async_get_entity_id( + "sensor", DOMAIN, f"{old_unique_id}_load_instant_power" + ) + is None + ) + assert ( + ent_reg.async_get_entity_id( + "sensor", DOMAIN, f"{new_unique_id}_load_instant_power" + ) + is not None + ) + + state = hass.states.get("sensor.mysite_load_power") + assert state.state == "1.971"