From 2555827030a1bcb97bd378ae8b4a41a9bbae7b3e Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:06:22 +0200 Subject: [PATCH] Replace Solarlog unmaintained library (#117484) Co-authored-by: Robert Resch --- CODEOWNERS | 4 +- homeassistant/components/solarlog/__init__.py | 48 ++++++++ .../components/solarlog/config_flow.py | 73 ++++++++----- .../components/solarlog/coordinator.py | 41 +++---- .../components/solarlog/manifest.json | 6 +- homeassistant/components/solarlog/sensor.py | 7 +- .../components/solarlog/strings.json | 6 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/solarlog/conftest.py | 54 +++++++++ tests/components/solarlog/test_config_flow.py | 103 +++++++++++++----- tests/components/solarlog/test_init.py | 57 ++++++++++ 12 files changed, 320 insertions(+), 91 deletions(-) create mode 100644 tests/components/solarlog/conftest.py create mode 100644 tests/components/solarlog/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index fa8db6628ce..103c66d3994 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1305,8 +1305,8 @@ build.json @home-assistant/supervisor /homeassistant/components/solaredge/ @frenck @bdraco /tests/components/solaredge/ @frenck @bdraco /homeassistant/components/solaredge_local/ @drobtravels @scheric -/homeassistant/components/solarlog/ @Ernst79 -/tests/components/solarlog/ @Ernst79 +/homeassistant/components/solarlog/ @Ernst79 @dontinelli +/tests/components/solarlog/ @Ernst79 @dontinelli /homeassistant/components/solax/ @squishykid /tests/components/solax/ @squishykid /homeassistant/components/soma/ @ratsept @sebfortier2288 diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index d2a3c50295c..6975a420732 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -1,12 +1,17 @@ """Solar-Log integration.""" +import logging + 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 .const import DOMAIN from .coordinator import SolarlogData +_LOGGER = logging.getLogger(__name__) + PLATFORMS = [Platform.SENSOR] @@ -22,3 +27,46 @@ 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, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + + if config_entry.version == 1: + if config_entry.minor_version < 2: + # migrate old entity unique id + entity_reg = er.async_get(hass) + entities: list[er.RegistryEntry] = er.async_entries_for_config_entry( + entity_reg, config_entry.entry_id + ) + + for entity in entities: + if "time" in entity.unique_id: + new_uid = entity.unique_id.replace("time", "last_updated") + _LOGGER.debug( + "migrate unique id '%s' to '%s'", entity.unique_id, new_uid + ) + entity_reg.async_update_entity( + entity.entity_id, new_unique_id=new_uid + ) + + # migrate config_entry + new = {**config_entry.data} + new["extended_data"] = False + + hass.config_entries.async_update_entry( + config_entry, data=new, minor_version=2, version=1 + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 40343b5ac12..deda2d81779 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -1,13 +1,14 @@ """Config flow for solarlog integration.""" import logging +from typing import Any from urllib.parse import ParseResult, urlparse -from requests.exceptions import HTTPError, Timeout -from sunwatcher.solarlog.solarlog import SolarLog +from solarlog_cli.solarlog_connector import SolarLogConnector +from solarlog_cli.solarlog_exceptions import SolarLogConnectionError, SolarLogError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -29,6 +30,7 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for solarlog.""" VERSION = 1 + MINOR_VERSION = 2 def __init__(self) -> None: """Initialize the config flow.""" @@ -40,37 +42,44 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): return True return False + def _parse_url(self, host: str) -> str: + """Return parsed host url.""" + url = urlparse(host, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + return url.geturl() + async def _test_connection(self, host): """Check if we can connect to the Solar-Log device.""" + solarlog = SolarLogConnector(host) try: - await self.hass.async_add_executor_job(SolarLog, host) - except (OSError, HTTPError, Timeout): - self._errors[CONF_HOST] = "cannot_connect" - _LOGGER.error( - "Could not connect to Solar-Log device at %s, check host ip address", - host, - ) + await solarlog.test_connection() + except SolarLogConnectionError: + self._errors = {CONF_HOST: "cannot_connect"} return False + except SolarLogError: # pylint: disable=broad-except + self._errors = {CONF_HOST: "unknown"} + return False + finally: + solarlog.client.close() + return True - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: # set some defaults in case we need to return to the form - name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) - host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + user_input[CONF_NAME] = slugify(user_input[CONF_NAME]) + user_input[CONF_HOST] = self._parse_url(user_input[CONF_HOST]) - url = urlparse(host_entry, "http") - netloc = url.netloc or url.path - path = url.path if url.netloc else "" - url = ParseResult("http", netloc, path, *url[3:]) - host = url.geturl() - - if self._host_in_configuration_exists(host): + if self._host_in_configuration_exists(user_input[CONF_HOST]): self._errors[CONF_HOST] = "already_configured" - elif await self._test_connection(host): - return self.async_create_entry(title=name, data={CONF_HOST: host}) + elif await self._test_connection(user_input[CONF_HOST]): + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) else: user_input = {} user_input[CONF_NAME] = DEFAULT_NAME @@ -86,21 +95,25 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) ): str, + vol.Required("extended_data", default=False): bool, } ), errors=self._errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import a config entry.""" - host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) - url = urlparse(host_entry, "http") - netloc = url.netloc or url.path - path = url.path if url.netloc else "" - url = ParseResult("http", netloc, path, *url[3:]) - host = url.geturl() + user_input = { + CONF_HOST: DEFAULT_HOST, + CONF_NAME: DEFAULT_NAME, + "extended_data": False, + **user_input, + } - if self._host_in_configuration_exists(host): + user_input[CONF_HOST] = self._parse_url(user_input[CONF_HOST]) + + if self._host_in_configuration_exists(user_input[CONF_HOST]): return self.async_abort(reason="already_configured") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/solarlog/coordinator.py b/homeassistant/components/solarlog/coordinator.py index 6af7c96302d..794e556add5 100644 --- a/homeassistant/components/solarlog/coordinator.py +++ b/homeassistant/components/solarlog/coordinator.py @@ -4,12 +4,16 @@ from datetime import timedelta import logging from urllib.parse import ParseResult, urlparse -from requests.exceptions import HTTPError, Timeout -from sunwatcher.solarlog.solarlog import SolarLog +from solarlog_cli.solarlog_connector import SolarLogConnector +from solarlog_cli.solarlog_exceptions import ( + SolarLogConnectionError, + SolarLogUpdateError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator _LOGGER = logging.getLogger(__name__) @@ -34,24 +38,23 @@ class SolarlogData(update_coordinator.DataUpdateCoordinator): self.name = entry.title self.host = url.geturl() - async def _async_update_data(self): - """Update the data from the SolarLog device.""" - try: - data = await self.hass.async_add_executor_job(SolarLog, self.host) - except (OSError, Timeout, HTTPError) as err: - raise update_coordinator.UpdateFailed(err) from err + extended_data = entry.data["extended_data"] - if data.time.year == 1999: - raise update_coordinator.UpdateFailed( - "Invalid data returned (can happen after Solarlog restart)." - ) - - self.logger.debug( - ( - "Connection to Solarlog successful. Retrieving latest Solarlog update" - " of %s" - ), - data.time, + self.solarlog = SolarLogConnector( + self.host, extended_data, hass.config.time_zone ) + async def _async_update_data(self): + """Update the data from the SolarLog device.""" + _LOGGER.debug("Start data update") + + try: + data = await self.solarlog.update_data() + except SolarLogConnectionError as err: + raise ConfigEntryNotReady(err) from err + except SolarLogUpdateError as err: + raise update_coordinator.UpdateFailed(err) from err + + _LOGGER.debug("Data successfully updated") + return data diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 78075123996..0878d652f43 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -1,10 +1,10 @@ { "domain": "solarlog", "name": "Solar-Log", - "codeowners": ["@Ernst79"], + "codeowners": ["@Ernst79", "@dontinelli"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/solarlog", "iot_class": "local_polling", - "loggers": ["sunwatcher"], - "requirements": ["sunwatcher==0.2.1"] + "loggers": ["solarlog_cli"], + "requirements": ["solarlog_cli==0.1.5"] } diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index dcb4afcb863..0b5d56f1a9e 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -21,7 +21,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.dt import as_local from . import SolarlogData from .const import DOMAIN @@ -36,10 +35,9 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( - key="time", + key="last_updated", translation_key="last_update", device_class=SensorDeviceClass.TIMESTAMP, - value=as_local, ), SolarLogSensorEntityDescription( key="power_ac", @@ -231,7 +229,8 @@ class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): @property def native_value(self): """Return the native sensor value.""" - raw_attr = getattr(self.coordinator.data, self.entity_description.key) + raw_attr = self.coordinator.data.get(self.entity_description.key) + if self.entity_description.value: return self.entity_description.value(raw_attr) return raw_attr diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index 5f5e2ae7a5f..255f35114c1 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -5,7 +5,8 @@ "title": "Define your Solar-Log connection", "data": { "host": "[%key:common::config_flow::data::host%]", - "name": "The prefix to be used for your Solar-Log sensors" + "name": "The prefix to be used for your Solar-Log sensors", + "extended_data": "Get additional data from Solar-Log. Extended data is only accessible, if no password is set for the Solar-Log. Use at your own risk!" }, "data_description": { "host": "The hostname or IP address of your Solar-Log device." @@ -14,7 +15,8 @@ }, "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/requirements_all.txt b/requirements_all.txt index 49cf1b84843..4ecfd25800d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2601,6 +2601,9 @@ soco==0.30.4 # homeassistant.components.solaredge_local solaredge-local==0.2.3 +# homeassistant.components.solarlog +solarlog_cli==0.1.5 + # homeassistant.components.solax solax==3.1.1 @@ -2661,9 +2664,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.7.11 -# homeassistant.components.solarlog -sunwatcher==0.2.1 - # homeassistant.components.sunweg sunweg==3.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55d2ccabc17..8dcef7f1575 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2020,6 +2020,9 @@ snapcast==2.3.6 # homeassistant.components.sonos soco==0.30.4 +# homeassistant.components.solarlog +solarlog_cli==0.1.5 + # homeassistant.components.solax solax==3.1.1 @@ -2077,9 +2080,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.7.11 -# homeassistant.components.solarlog -sunwatcher==0.2.1 - # homeassistant.components.sunweg sunweg==3.0.1 diff --git a/tests/components/solarlog/conftest.py b/tests/components/solarlog/conftest.py new file mode 100644 index 00000000000..71034828025 --- /dev/null +++ b/tests/components/solarlog/conftest.py @@ -0,0 +1,54 @@ +"""Test helpers.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.core import HomeAssistant + +from tests.common import mock_device_registry, mock_registry + + +@pytest.fixture +def mock_solarlog(): + """Build a fixture for the SolarLog API that connects successfully and returns one device.""" + + mock_solarlog_api = AsyncMock() + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConnector", + return_value=mock_solarlog_api, + ) as mock_solarlog_api: + mock_solarlog_api.return_value.test_connection.return_value = True + yield mock_solarlog_api + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.solarlog.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="test_connect") +def mock_test_connection(): + """Mock a successful _test_connection.""" + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + return_value=True, + ): + yield + + +@pytest.fixture(name="device_reg") +def device_reg_fixture(hass: HomeAssistant): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture(name="entity_reg") +def entity_reg_fixture(hass: HomeAssistant): + """Return an empty, loaded, registry.""" + return mock_registry(hass) diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index c356a129806..63df582b0e1 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,8 +1,9 @@ """Test the solarlog config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest +from solarlog_cli.solarlog_exceptions import SolarLogConnectionError, SolarLogError from homeassistant import config_entries from homeassistant.components.solarlog import config_flow @@ -17,7 +18,7 @@ NAME = "Solarlog test 1 2 3" HOST = "http://1.1.1.1" -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -29,34 +30,22 @@ async def test_form(hass: HomeAssistant) -> None: with ( patch( "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - return_value={"title": "solarlog test 1 2 3"}, - ), - patch( - "homeassistant.components.solarlog.async_setup_entry", return_value=True, - ) as mock_setup_entry, + ), ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": HOST, "name": NAME} + result["flow_id"], + {CONF_HOST: HOST, CONF_NAME: NAME, "extended_data": False}, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "solarlog_test_1_2_3" - assert result2["data"] == {"host": "http://1.1.1.1"} + assert result2["data"][CONF_HOST] == "http://1.1.1.1" + assert result2["data"]["extended_data"] is False assert len(mock_setup_entry.mock_calls) == 1 -@pytest.fixture(name="test_connect") -def mock_controller(): - """Mock a successful _host_in_configuration_exists.""" - with patch( - "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - return_value=True, - ): - yield - - def init_config_flow(hass): """Init a configuration flow.""" flow = config_flow.SolarLogConfigFlow() @@ -64,19 +53,75 @@ def init_config_flow(hass): return flow -async def test_user(hass: HomeAssistant, test_connect) -> None: +@pytest.mark.usefixtures("test_connect") +async def test_user( + hass: HomeAssistant, + mock_solarlog: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: """Test user config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + # tests with all provided + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: HOST, CONF_NAME: NAME, "extended_data": False} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (SolarLogConnectionError, {CONF_HOST: "cannot_connect"}), + (SolarLogError, {CONF_HOST: "unknown"}), + ], +) +async def test_form_exceptions( + hass: HomeAssistant, + exception: Exception, + error: dict[str, str], + mock_solarlog: AsyncMock, +) -> None: + """Test we can handle Form exceptions.""" flow = init_config_flow(hass) result = await flow.async_step_user() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - # tets with all provided - result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) + mock_solarlog.return_value.test_connection.side_effect = exception + + # tests with connection error + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, "extended_data": False} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == error + + mock_solarlog.return_value.test_connection.side_effect = None + + # tests with all provided + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, "extended_data": False} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST + assert result["data"]["extended_data"] is False async def test_import(hass: HomeAssistant, test_connect) -> None: @@ -85,18 +130,24 @@ async def test_import(hass: HomeAssistant, test_connect) -> None: # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog" assert result["data"][CONF_HOST] == HOST # import with only name result = await flow.async_step_import({CONF_NAME: NAME}) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == DEFAULT_HOST # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -111,7 +162,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None # Should fail, same HOST different NAME (default) result = await flow.async_step_import( - {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} + {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9", "extended_data": False} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -123,7 +174,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None # SHOULD pass, diff HOST (without http://), different NAME result = await flow.async_step_import( - {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} + {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9", "extended_data": False} ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_7_8_9" @@ -131,8 +182,10 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None # SHOULD pass, diff HOST, same NAME result = await flow.async_step_import( - {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} + {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME, "extended_data": False} ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == "http://2.2.2.2" diff --git a/tests/components/solarlog/test_init.py b/tests/components/solarlog/test_init.py new file mode 100644 index 00000000000..9a8d6cb5bec --- /dev/null +++ b/tests/components/solarlog/test_init.py @@ -0,0 +1,57 @@ +"""Test the initialization.""" + +from homeassistant.components.solarlog.const import DOMAIN +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry + +from .test_config_flow import HOST, NAME + +from tests.common import MockConfigEntry + + +async def test_migrate_config_entry( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +) -> None: + """Test successful migration of entry data.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=NAME, + data={ + CONF_HOST: HOST, + }, + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + device = device_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Solar-Log", + name="solarlog", + ) + sensor_entity = entity_reg.async_get_or_create( + config_entry=entry, + platform=DOMAIN, + domain=Platform.SENSOR, + unique_id=f"{entry.entry_id}_time", + device_id=device.id, + ) + + assert entry.version == 1 + assert entry.minor_version == 1 + assert sensor_entity.unique_id == f"{entry.entry_id}_time" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_reg.async_get(sensor_entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == f"{entry.entry_id}_last_updated" + + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.data[CONF_HOST] == HOST + assert entry.data["extended_data"] is False