From 10b186a20de3eab026863ed813b40ea1e36c8cfa Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 16 Sep 2025 10:08:08 +0200 Subject: [PATCH] Bump pylamarzocco to 2.1.0 (#152364) --- .../components/lamarzocco/__init__.py | 41 ++++++------- .../components/lamarzocco/config_flow.py | 13 +++- homeassistant/components/lamarzocco/const.py | 1 + .../components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lamarzocco/__init__.py | 3 + tests/components/lamarzocco/conftest.py | 24 +++++++- .../components/lamarzocco/test_config_flow.py | 17 +++++- tests/components/lamarzocco/test_init.py | 60 +++++++++---------- 10 files changed, 102 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 92184b4ac51c..15ff16346879 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +import uuid from packaging import version from pylamarzocco import ( @@ -11,6 +12,7 @@ from pylamarzocco import ( ) from pylamarzocco.const import FirmwareType from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful +from pylamarzocco.util import InstallationKey, generate_installation_key from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.const import ( @@ -25,7 +27,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CONF_USE_BLUETOOTH, DOMAIN +from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN from .coordinator import ( LaMarzoccoConfigEntry, LaMarzoccoConfigUpdateCoordinator, @@ -60,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + installation_key=InstallationKey.from_json(entry.data[CONF_INSTALLATION_KEY]), client=async_create_clientsession(hass), ) @@ -166,45 +169,37 @@ async def async_migrate_entry( hass: HomeAssistant, entry: LaMarzoccoConfigEntry ) -> bool: """Migrate config entry.""" - if entry.version > 3: + if entry.version > 4: # guard against downgrade from a future version return False - if entry.version == 1: + if entry.version in (1, 2): _LOGGER.error( - "Migration from version 1 is no longer supported, please remove and re-add the integration" + "Migration from version 1 or 2 is no longer supported, please remove and re-add the integration" ) return False - if entry.version == 2: + if entry.version == 3: + installation_key = generate_installation_key(str(uuid.uuid4()).lower()) cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + installation_key=installation_key, ) try: - things = await cloud_client.list_things() + await cloud_client.async_register_client() except (AuthFail, RequestNotSuccessful) as exc: _LOGGER.error("Migration failed with error %s", exc) return False - v3_data = { - CONF_USERNAME: entry.data[CONF_USERNAME], - CONF_PASSWORD: entry.data[CONF_PASSWORD], - CONF_TOKEN: next( - ( - thing.ble_auth_token - for thing in things - if thing.serial_number == entry.unique_id - ), - None, - ), - } - if CONF_MAC in entry.data: - v3_data[CONF_MAC] = entry.data[CONF_MAC] + hass.config_entries.async_update_entry( entry, - data=v3_data, - version=3, + data={ + **entry.data, + CONF_INSTALLATION_KEY: installation_key.to_json(), + }, + version=4, ) - _LOGGER.debug("Migrated La Marzocco config entry to version 2") + _LOGGER.debug("Migrated La Marzocco config entry to version 4") return True diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index fb968a0b4af9..7f08ac9a48ed 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -5,11 +5,13 @@ from __future__ import annotations from collections.abc import Mapping import logging from typing import Any +import uuid from aiohttp import ClientSession from pylamarzocco import LaMarzoccoCloudClient from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful from pylamarzocco.models import Thing +from pylamarzocco.util import InstallationKey, generate_installation_key import voluptuous as vol from homeassistant.components.bluetooth import ( @@ -45,7 +47,7 @@ from homeassistant.helpers.selector import ( ) from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -from .const import CONF_USE_BLUETOOTH, DOMAIN +from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN from .coordinator import LaMarzoccoConfigEntry CONF_MACHINE = "machine" @@ -57,9 +59,10 @@ _LOGGER = logging.getLogger(__name__) class LmConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for La Marzocco.""" - VERSION = 3 + VERSION = 4 _client: ClientSession + _installation_key: InstallationKey def __init__(self) -> None: """Initialize the config flow.""" @@ -84,12 +87,17 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): } self._client = async_create_clientsession(self.hass) + self._installation_key = generate_installation_key( + str(uuid.uuid4()).lower() + ) cloud_client = LaMarzoccoCloudClient( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], client=self._client, + installation_key=self._installation_key, ) try: + await cloud_client.async_register_client() things = await cloud_client.list_things() except AuthFail: _LOGGER.debug("Server rejected login credentials") @@ -184,6 +192,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): title=selected_device.name, data={ **self._config, + CONF_INSTALLATION_KEY: self._installation_key.to_json(), CONF_TOKEN: self._things[serial_number].ble_auth_token, }, ) diff --git a/homeassistant/components/lamarzocco/const.py b/homeassistant/components/lamarzocco/const.py index 57db84f94da1..680557d85f18 100644 --- a/homeassistant/components/lamarzocco/const.py +++ b/homeassistant/components/lamarzocco/const.py @@ -5,3 +5,4 @@ from typing import Final DOMAIN: Final = "lamarzocco" CONF_USE_BLUETOOTH: Final = "use_bluetooth" +CONF_INSTALLATION_KEY: Final = "installation_key" diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 3c070769b5b0..ec55a7e8c2b1 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.11"] + "requirements": ["pylamarzocco==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e40e6a9891d7..b7e231f3be21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2103,7 +2103,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.11 +pylamarzocco==2.1.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a956ae28415c..5fd585f3e03a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1748,7 +1748,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.11 +pylamarzocco==2.1.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/tests/components/lamarzocco/__init__.py b/tests/components/lamarzocco/__init__.py index 80493aa83c97..55335f720c3a 100644 --- a/tests/components/lamarzocco/__init__.py +++ b/tests/components/lamarzocco/__init__.py @@ -54,3 +54,6 @@ def get_bluetooth_service_info(model: ModelName, serial: str) -> BluetoothServic service_uuids=[], source="local", ) + + +MOCK_INSTALLATION_KEY = '{"secret": "K9ZW2vlMSb3QXmhySx4pxAbTHujWj3VZ01Jn3D/sO98=", "private_key": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8iotE8El786F6kHuEL8GyYhjDB7oo06vNhQwtewF37yhRANCAAQCLb9lHskiavvfkI4H2B+WsdkusfgBBFuFNRrGV8bqPMra1TK5myb/ecdZfHJBBJrcbdt90QMDmXQm5L3muXXe", "installation_id": "4e966f3f-2abc-49c4-a362-3cd3346f1a87"}' diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index ad1378a6dc19..7907a1d6a7ed 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -12,13 +12,14 @@ from pylamarzocco.models import ( ThingSettings, ThingStatistics, ) +from pylamarzocco.util import InstallationKey import pytest -from homeassistant.components.lamarzocco.const import DOMAIN +from homeassistant.components.lamarzocco.const import CONF_INSTALLATION_KEY, DOMAIN from homeassistant.const import CONF_ADDRESS, CONF_TOKEN from homeassistant.core import HomeAssistant -from . import SERIAL_DICT, USER_INPUT, async_init_integration +from . import MOCK_INSTALLATION_KEY, SERIAL_DICT, USER_INPUT, async_init_integration from tests.common import MockConfigEntry, load_json_object_fixture @@ -31,11 +32,12 @@ def mock_config_entry( return MockConfigEntry( title="My LaMarzocco", domain=DOMAIN, - version=3, + version=4, data=USER_INPUT | { CONF_ADDRESS: "000000000000", CONF_TOKEN: "token", + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, }, unique_id=mock_lamarzocco.serial_number, ) @@ -51,6 +53,22 @@ async def init_integration( return mock_config_entry +@pytest.fixture(autouse=True) +def mock_generate_installation_key() -> Generator[MagicMock]: + """Return a mocked generate_installation_key.""" + with ( + patch( + "homeassistant.components.lamarzocco.generate_installation_key", + return_value=InstallationKey.from_json(MOCK_INSTALLATION_KEY), + ) as mock_generate, + patch( + "homeassistant.components.lamarzocco.config_flow.generate_installation_key", + new=mock_generate, + ), + ): + yield mock_generate + + @pytest.fixture def device_fixture() -> ModelName: """Return the device fixture for a specific device.""" diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index e50707f71afd..5d0a514b7932 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -9,7 +9,11 @@ from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful import pytest from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE -from homeassistant.components.lamarzocco.const import CONF_USE_BLUETOOTH, DOMAIN +from homeassistant.components.lamarzocco.const import ( + CONF_INSTALLATION_KEY, + CONF_USE_BLUETOOTH, + DOMAIN, +) from homeassistant.config_entries import ( SOURCE_BLUETOOTH, SOURCE_DHCP, @@ -23,7 +27,12 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -from . import USER_INPUT, async_init_integration, get_bluetooth_service_info +from . import ( + MOCK_INSTALLATION_KEY, + USER_INPUT, + async_init_integration, + get_bluetooth_service_info, +) from tests.common import MockConfigEntry @@ -68,6 +77,7 @@ async def __do_sucessful_machine_selection_step( assert result["data"] == { **USER_INPUT, CONF_TOKEN: None, + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } assert result["result"].unique_id == "GS012345" @@ -344,6 +354,7 @@ async def test_bluetooth_discovery( **USER_INPUT, CONF_MAC: "aa:bb:cc:dd:ee:ff", CONF_TOKEN: "dummyToken", + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } @@ -407,6 +418,7 @@ async def test_bluetooth_discovery_errors( **USER_INPUT, CONF_MAC: "aa:bb:cc:dd:ee:ff", CONF_TOKEN: None, + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } @@ -438,6 +450,7 @@ async def test_dhcp_discovery( **USER_INPUT, CONF_ADDRESS: "aabbccddeeff", CONF_TOKEN: None, + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index 1e56e540e2a6..e6bf4a0af62e 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -8,15 +8,11 @@ from pylamarzocco.models import WebSocketDetails import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE -from homeassistant.components.lamarzocco.const import DOMAIN +from homeassistant.components.lamarzocco.const import CONF_INSTALLATION_KEY, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_ADDRESS, - CONF_HOST, CONF_MAC, - CONF_MODEL, - CONF_NAME, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, ) @@ -27,7 +23,12 @@ from homeassistant.helpers import ( issue_registry as ir, ) -from . import USER_INPUT, async_init_integration, get_bluetooth_service_info +from . import ( + MOCK_INSTALLATION_KEY, + USER_INPUT, + async_init_integration, + get_bluetooth_service_info, +) from tests.common import MockConfigEntry @@ -129,66 +130,65 @@ async def test_v1_migration_fails( assert entry_v1.state is ConfigEntryState.MIGRATION_ERROR -async def test_v2_migration( +async def test_v4_migration( hass: HomeAssistant, mock_lamarzocco: MagicMock, ) -> None: - """Test v2 -> v3 Migration.""" + """Test v3 -> v4 Migration.""" - entry_v2 = MockConfigEntry( + entry_v3 = MockConfigEntry( domain=DOMAIN, - version=2, + version=3, unique_id=mock_lamarzocco.serial_number, data={ **USER_INPUT, - CONF_HOST: "192.168.1.24", - CONF_NAME: "La Marzocco", - CONF_MODEL: ModelName.GS3_MP.value, - CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_ADDRESS: "000000000000", + CONF_TOKEN: "token", }, ) - entry_v2.add_to_hass(hass) + entry_v3.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry_v2.entry_id) - assert entry_v2.state is ConfigEntryState.LOADED - assert entry_v2.version == 3 - assert dict(entry_v2.data) == { + assert await hass.config_entries.async_setup(entry_v3.entry_id) + assert entry_v3.state is ConfigEntryState.LOADED + assert entry_v3.version == 4 + assert dict(entry_v3.data) == { **USER_INPUT, - CONF_MAC: "aa:bb:cc:dd:ee:ff", - CONF_TOKEN: None, + CONF_ADDRESS: "000000000000", + CONF_TOKEN: "token", + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, } async def test_migration_errors( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, mock_cloud_client: MagicMock, mock_lamarzocco: MagicMock, ) -> None: """Test errors during migration.""" - mock_cloud_client.list_things.side_effect = RequestNotSuccessful("Error") + mock_cloud_client.async_register_client.side_effect = RequestNotSuccessful("Error") - entry_v2 = MockConfigEntry( + entry_v3 = MockConfigEntry( domain=DOMAIN, - version=2, + version=3, unique_id=mock_lamarzocco.serial_number, data={ **USER_INPUT, - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_ADDRESS: "000000000000", + CONF_TOKEN: "token", }, ) - entry_v2.add_to_hass(hass) + entry_v3.add_to_hass(hass) - assert not await hass.config_entries.async_setup(entry_v2.entry_id) - assert entry_v2.state is ConfigEntryState.MIGRATION_ERROR + assert not await hass.config_entries.async_setup(entry_v3.entry_id) + assert entry_v3.state is ConfigEntryState.MIGRATION_ERROR async def test_config_flow_entry_migration_downgrade( hass: HomeAssistant, ) -> None: """Test that config entry fails setup if the version is from the future.""" - entry = MockConfigEntry(domain=DOMAIN, version=4) + entry = MockConfigEntry(domain=DOMAIN, version=5) entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(entry.entry_id)