diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 98dc7cb47ae..b6a00bbaf10 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo @@ -29,73 +30,24 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) +from .config_flow import ConfigFlow from .const import DOMAIN PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.CAMERA, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -async def _migrate_to_version_2( - hass: HomeAssistant, entry: ConfigEntry -) -> PrusaLink | None: - """Migrate to Version 2.""" - _LOGGER.debug("Migrating entry to version 2") - - data = dict(entry.data) - # "maker" is currently hardcoded in the firmware - # https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/bfb0ffc745ee6546e7efdba618d0e7c0f4c909cd/lib/WUI/wui_api.h#L19 - data = { - **entry.data, - CONF_USERNAME: "maker", - CONF_PASSWORD: entry.data[CONF_API_KEY], - } - data.pop(CONF_API_KEY) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up PrusaLink from a config entry.""" + if entry.version == 1 and entry.minor_version < 2: + raise ConfigEntryError("Please upgrade your printer's firmware.") api = PrusaLink( async_get_clientsession(hass), - data[CONF_HOST], - data[CONF_USERNAME], - data[CONF_PASSWORD], + entry.data[CONF_HOST], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], ) - try: - await api.get_info() - except InvalidAuth: - # We are unable to reach the new API which usually means - # that the user is running an outdated firmware version - ir.async_create_issue( - hass, - DOMAIN, - "firmware_5_1_required", - is_fixable=False, - severity=ir.IssueSeverity.ERROR, - translation_key="firmware_5_1_required", - translation_placeholders={ - "entry_title": entry.title, - "prusa_mini_firmware_update": "https://help.prusa3d.com/article/firmware-updating-mini-mini_124784", - "prusa_mk4_xl_firmware_update": "https://help.prusa3d.com/article/how-to-update-firmware-mk4-xl_453086", - }, - ) - return None - - entry.version = 2 - hass.config_entries.async_update_entry(entry, data=data) - _LOGGER.info("Migrated config entry to version %d", entry.version) - return api - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up PrusaLink from a config entry.""" - if entry.version == 1: - if (api := await _migrate_to_version_2(hass, entry)) is None: - return False - ir.async_delete_issue(hass, DOMAIN, "firmware_5_1_required") - else: - api = PrusaLink( - async_get_clientsession(hass), - entry.data[CONF_HOST], - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - ) coordinators = { "legacy_status": LegacyStatusCoordinator(hass, api), @@ -112,9 +64,59 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" - # Version 1->2 migration are handled in async_setup_entry. + if config_entry.version > ConfigFlow.VERSION: + # This means the user has downgraded from a future version + return False + + new_data = dict(config_entry.data) + if config_entry.version == 1: + if config_entry.minor_version < 2: + # Add username and password + # "maker" is currently hardcoded in the firmware + # https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/bfb0ffc745ee6546e7efdba618d0e7c0f4c909cd/lib/WUI/wui_api.h#L19 + username = "maker" + password = config_entry.data[CONF_API_KEY] + + api = PrusaLink( + async_get_clientsession(hass), + config_entry.data[CONF_HOST], + username, + password, + ) + try: + await api.get_info() + except InvalidAuth: + # We are unable to reach the new API which usually means + # that the user is running an outdated firmware version + ir.async_create_issue( + hass, + DOMAIN, + "firmware_5_1_required", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="firmware_5_1_required", + translation_placeholders={ + "entry_title": config_entry.title, + "prusa_mini_firmware_update": "https://help.prusa3d.com/article/firmware-updating-mini-mini_124784", + "prusa_mk4_xl_firmware_update": "https://help.prusa3d.com/article/how-to-update-firmware-mk4-xl_453086", + }, + ) + # There is a check in the async_setup_entry to prevent the setup if minor_version < 2 + # Currently we can't reload the config entry + # if the migration returns False. + # Return True here to workaround that. + return True + + new_data[CONF_USERNAME] = username + new_data[CONF_PASSWORD] = password + + ir.async_delete_issue(hass, DOMAIN, "firmware_5_1_required") + config_entry.minor_version = 2 + + hass.config_entries.async_update_entry(config_entry, data=new_data) + return True diff --git a/homeassistant/components/prusalink/config_flow.py b/homeassistant/components/prusalink/config_flow.py index e967cefaffd..378c5e7395a 100644 --- a/homeassistant/components/prusalink/config_flow.py +++ b/homeassistant/components/prusalink/config_flow.py @@ -66,7 +66,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for PrusaLink.""" - VERSION = 2 + VERSION = 1 + MINOR_VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/tests/components/prusalink/conftest.py b/tests/components/prusalink/conftest.py index 97f4bd92d7d..1e514342068 100644 --- a/tests/components/prusalink/conftest.py +++ b/tests/components/prusalink/conftest.py @@ -14,7 +14,8 @@ def mock_config_entry(hass): entry = MockConfigEntry( domain=DOMAIN, data={"host": "http://example.com", "username": "dummy", "password": "dummypw"}, - version=2, + version=1, + minor_version=2, ) entry.add_to_hass(hass) return entry diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py index 963750ef8be..5b261207e93 100644 --- a/tests/components/prusalink/test_init.py +++ b/tests/components/prusalink/test_init.py @@ -6,6 +6,7 @@ from pyprusalink.types import InvalidAuth, PrusaLinkError import pytest from homeassistant.components.prusalink import DOMAIN +from homeassistant.components.prusalink.config_flow import ConfigFlow from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -14,11 +15,12 @@ from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed +pytestmark = pytest.mark.usefixtures("mock_api") + async def test_unloading( hass: HomeAssistant, mock_config_entry: ConfigEntry, - mock_api, ) -> None: """Test unloading prusalink.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -35,7 +37,7 @@ async def test_unloading( @pytest.mark.parametrize("exception", [InvalidAuth, PrusaLinkError]) async def test_failed_update( - hass: HomeAssistant, mock_config_entry: ConfigEntry, mock_api, exception + hass: HomeAssistant, mock_config_entry: ConfigEntry, exception ) -> None: """Test failed update marks prusalink unavailable.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -61,16 +63,17 @@ async def test_failed_update( assert state.state == "unavailable" -async def test_migration_1_2( - hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_api +async def test_migration_from_1_1_to_1_2( + hass: HomeAssistant, issue_registry: ir.IssueRegistry ) -> None: """Test migrating from version 1 to 2.""" + data = { + CONF_HOST: "http://prusaxl.local", + CONF_API_KEY: "api-key", + } entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_HOST: "http://prusaxl.local", - CONF_API_KEY: "api-key", - }, + data=data, version=1, ) entry.add_to_hass(hass) @@ -83,7 +86,7 @@ async def test_migration_1_2( # Ensure that we have username, password after migration assert len(config_entries) == 1 assert config_entries[0].data == { - CONF_HOST: "http://prusaxl.local", + **data, CONF_USERNAME: "maker", CONF_PASSWORD: "api-key", } @@ -91,10 +94,10 @@ async def test_migration_1_2( assert len(issue_registry.issues) == 0 -async def test_outdated_firmware_migration_1_2( - hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_api +async def test_migration_from_1_1_to_1_2_outdated_firmware( + hass: HomeAssistant, issue_registry: ir.IssueRegistry ) -> None: - """Test migrating from version 1 to 2.""" + """Test migrating from version 1.1 to 1.2.""" entry = MockConfigEntry( domain=DOMAIN, data={ @@ -107,14 +110,14 @@ async def test_outdated_firmware_migration_1_2( with patch( "pyprusalink.PrusaLink.get_info", - side_effect=InvalidAuth, + side_effect=InvalidAuth, # Simulate firmware update required ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state == ConfigEntryState.SETUP_ERROR - # Make sure that we don't have thrown the issues - assert len(issue_registry.issues) == 1 + assert entry.minor_version == 1 + assert (DOMAIN, "firmware_5_1_required") in issue_registry.issues # Reloading the integration with a working API (e.g. User updated firmware) await hass.config_entries.async_reload(entry.entry_id) @@ -122,4 +125,22 @@ async def test_outdated_firmware_migration_1_2( # Integration should be running now, the issue should be gone assert entry.state == ConfigEntryState.LOADED - assert len(issue_registry.issues) == 0 + assert entry.minor_version == 2 + assert (DOMAIN, "firmware_5_1_required") not in issue_registry.issues + + +async def test_migration_fails_on_future_version( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test migrating fails on a version higher than the current one.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + version=ConfigFlow.VERSION + 1, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.MIGRATION_ERROR