diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 398932bf4d6..e3f55904b77 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -1,7 +1,14 @@ """Support for the Swedish weather institute weather service.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_NAME, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries PLATFORMS = [Platform.WEATHER] @@ -11,7 +18,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setting unique id where missing if entry.unique_id is None: - unique_id = f"{entry.data[CONF_LATITUDE]}-{entry.data[CONF_LONGITUDE]}" + unique_id = f"{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" hass.config_entries.async_update_entry(entry, unique_id=unique_id) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -21,3 +28,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + if entry.version == 1: + new_data = { + CONF_NAME: entry.data[CONF_NAME], + CONF_LOCATION: { + CONF_LATITUDE: entry.data[CONF_LATITUDE], + CONF_LONGITUDE: entry.data[CONF_LONGITUDE], + }, + } + new_unique_id = f"smhi-{entry.data[CONF_LATITUDE]}-{entry.data[CONF_LONGITUDE]}" + + if not hass.config_entries.async_update_entry( + entry, data=new_data, unique_id=new_unique_id + ): + return False + + entry.version = 2 + new_unique_id_entity = f"smhi-{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" + + @callback + def update_unique_id(entity_entry: RegistryEntry) -> dict[str, str]: + """Update unique ID of entity entry.""" + return {"new_unique_id": new_unique_id_entity} + + await async_migrate_entries(hass, entry.entry_id, update_unique_id) + + return True diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 770f549efe0..8d338e3eb3e 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -7,11 +7,11 @@ from smhi.smhi_lib import Smhi, SmhiForecastException import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import LocationSelector from .const import DEFAULT_NAME, DOMAIN, HOME_LOCATION_NAME @@ -33,7 +33,7 @@ async def async_check_location( class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SMHI component.""" - VERSION = 1 + VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -43,8 +43,8 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - lat: float = user_input[CONF_LATITUDE] - lon: float = user_input[CONF_LONGITUDE] + lat: float = user_input[CONF_LOCATION][CONF_LATITUDE] + lon: float = user_input[CONF_LOCATION][CONF_LONGITUDE] if await async_check_location(self.hass, lon, lat): name = f"{DEFAULT_NAME} {round(lat, 6)} {round(lon, 6)}" if ( @@ -57,30 +57,20 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): HOME_LOCATION_NAME if name == HOME_LOCATION_NAME else DEFAULT_NAME ) - await self.async_set_unique_id(f"{lat}-{lon}") + await self.async_set_unique_id(f"smhi-{lat}-{lon}") self._abort_if_unique_id_configured() return self.async_create_entry(title=name, data=user_input) errors["base"] = "wrong_location" - default_lat: float = self.hass.config.latitude - default_lon: float = self.hass.config.longitude - - for entry in self.hass.config_entries.async_entries(DOMAIN): - if ( - entry.data[CONF_LATITUDE] == self.hass.config.latitude - and entry.data[CONF_LONGITUDE] == self.hass.config.longitude - ): - default_lat = 0 - default_lon = 0 - + home_location = { + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + } return self.async_show_form( step_id="user", data_schema=vol.Schema( - { - vol.Required(CONF_LATITUDE, default=default_lat): cv.latitude, - vol.Required(CONF_LONGITUDE, default=default_lon): cv.longitude, - } + {vol.Required(CONF_LOCATION, default=home_location): LocationSelector()} ), errors=errors, ) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index cbe2ad37fe9..5d1f3e3c87e 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -42,6 +42,7 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_LATITUDE, + CONF_LOCATION, CONF_LONGITUDE, CONF_NAME, LENGTH_KILOMETERS, @@ -106,8 +107,8 @@ async def async_setup_entry( entity = SmhiWeather( location[CONF_NAME], - location[CONF_LATITUDE], - location[CONF_LONGITUDE], + location[CONF_LOCATION][CONF_LATITUDE], + location[CONF_LOCATION][CONF_LONGITUDE], session=session, ) entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name) @@ -135,7 +136,7 @@ class SmhiWeather(WeatherEntity): """Initialize the SMHI weather entity.""" self._attr_name = name - self._attr_unique_id = f"{latitude}, {longitude}" + self._attr_unique_id = f"smhi-{latitude}-{longitude}" self._forecasts: list[SmhiForecast] | None = None self._fail_count = 0 self._smhi_api = Smhi(longitude, latitude, session=session) diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py index d815aafc8f5..377552da4d5 100644 --- a/tests/components/smhi/__init__.py +++ b/tests/components/smhi/__init__.py @@ -1,3 +1,14 @@ """Tests for the SMHI component.""" ENTITY_ID = "weather.smhi_test" -TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} +TEST_CONFIG = { + "name": "test", + "location": { + "longitude": "17.84197", + "latitude": "59.32624", + }, +} +TEST_CONFIG_MIGRATE = { + "name": "test", + "longitude": "17.84197", + "latitude": "17.84197", +} diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 60879e8af75..ab3e36f81de 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -7,7 +7,7 @@ from smhi.smhi_lib import SmhiForecastException from homeassistant import config_entries from homeassistant.components.smhi.const import DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -40,8 +40,10 @@ async def test_form(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 0.0, - CONF_LONGITUDE: 0.0, + CONF_LOCATION: { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } }, ) await hass.async_block_till_done() @@ -49,8 +51,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Home" assert result2["data"] == { - "latitude": 0.0, - "longitude": 0.0, + "location": { + "latitude": 0.0, + "longitude": 0.0, + }, "name": "Home", } assert len(mock_setup_entry.mock_calls) == 1 @@ -69,8 +73,10 @@ async def test_form(hass: HomeAssistant) -> None: result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], { - CONF_LATITUDE: 1.0, - CONF_LONGITUDE: 1.0, + CONF_LOCATION: { + CONF_LATITUDE: 1.0, + CONF_LONGITUDE: 1.0, + } }, ) await hass.async_block_till_done() @@ -78,8 +84,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "Weather 1.0 1.0" assert result4["data"] == { - "latitude": 1.0, - "longitude": 1.0, + "location": { + "latitude": 1.0, + "longitude": 1.0, + }, "name": "Weather", } @@ -97,8 +105,10 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 0.0, - CONF_LONGITUDE: 0.0, + CONF_LOCATION: { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } }, ) await hass.async_block_till_done() @@ -117,8 +127,10 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 2.0, - CONF_LONGITUDE: 2.0, + CONF_LOCATION: { + CONF_LATITUDE: 2.0, + CONF_LONGITUDE: 2.0, + } }, ) await hass.async_block_till_done() @@ -126,8 +138,10 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: assert result3["type"] == RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "Weather 2.0 2.0" assert result3["data"] == { - "latitude": 2.0, - "longitude": 2.0, + "location": { + "latitude": 2.0, + "longitude": 2.0, + }, "name": "Weather", } @@ -136,10 +150,12 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: """Test we handle unique id already exist.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="1.0-1.0", + unique_id="smhi-1.0-1.0", data={ - "latitude": 1.0, - "longitude": 1.0, + "location": { + "latitude": 1.0, + "longitude": 1.0, + }, "name": "Weather", }, ) @@ -155,8 +171,10 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 1.0, - CONF_LONGITUDE: 1.0, + CONF_LOCATION: { + CONF_LATITUDE: 1.0, + CONF_LONGITUDE: 1.0, + } }, ) await hass.async_block_till_done() diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index 2cf54ba7533..ea6d55fabf6 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -3,8 +3,9 @@ from smhi.smhi_lib import APIURL_TEMPLATE from homeassistant.components.smhi.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get -from . import ENTITY_ID, TEST_CONFIG +from . import ENTITY_ID, TEST_CONFIG, TEST_CONFIG_MIGRATE from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -14,9 +15,11 @@ async def test_setup_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test setup entry.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -30,9 +33,11 @@ async def test_remove_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test remove entry.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -46,3 +51,38 @@ async def test_remove_entry( state = hass.states.get(ENTITY_ID) assert not state + + +async def test_migrate_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test migrate entry and entities unique id.""" + uri = APIURL_TEMPLATE.format( + TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] + ) + aioclient_mock.get(uri, text=api_response) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG_MIGRATE) + entry.add_to_hass(hass) + assert entry.version == 1 + + entity_reg = async_get(hass) + entity = entity_reg.async_get_or_create( + domain="weather", + config_entry=entry, + original_name="Weather", + platform="smhi", + supported_features=0, + unique_id="17.84197, 17.84197", + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state + + assert entry.version == 2 + assert entry.unique_id == "smhi-17.84197-17.84197" + + entity_get = entity_reg.async_get(entity.entity_id) + assert entity_get.unique_id == "smhi-17.84197-17.84197" diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 0097a7a5c5a..f33e8c9fa71 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -46,10 +46,12 @@ async def test_setup_hass( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test for successfully setting up the smhi integration.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -87,7 +89,7 @@ async def test_setup_hass( async def test_properties_no_data(hass: HomeAssistant) -> None: """Test properties when no API data available.""" - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) with patch( @@ -176,7 +178,7 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: testdata = [data, data2, data3] - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) with patch( @@ -203,7 +205,7 @@ async def test_refresh_weather_forecast_retry( hass: HomeAssistant, error: Exception ) -> None: """Test the refresh weather forecast function.""" - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) now = utcnow() @@ -320,10 +322,12 @@ async def test_custom_speed_unit( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test Wind Gust speed with custom unit.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)