From 42b984ee4f84242374a1af63e096ab4b26a88b82 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Mon, 10 Jun 2024 19:59:39 +0200 Subject: [PATCH] Migrate lamarzocco to lmcloud 1.1 (#113935) * migrate to 1.1 * bump to 1.1.1 * fix newlines docstring * cleanup entity_description fns * strict generics * restructure import * tweaks to generics * tweaks to generics * removed exceptions * move initialization, websocket clean shutdown * get rid of duplicate entry addign * bump lmcloud * re-add calendar, auto on/off switches * use asdict for diagnostics * change number generator * use name as entry title * also migrate title * don't migrate title * remove generics for now * satisfy mypy * add s * adapt * migrate entry.runtime_data * remove auto/onoff * add issue on wrong gw firmware * silence mypy * remove breaks in ha version * parametrize issue test * Update update.py Co-authored-by: Joost Lekkerkerker * Update test_config_flow.py Co-authored-by: Joost Lekkerkerker * regen snapshots * mapping steam level * remove commented code * fix typo * coderabbitai availability tweak * remove microsecond moving * additonal schedule for coverage * be more specific on date offset * keep mappings the same * config_entry imports sharpened * remove unneccessary testcase, clenup date moving * remove superfluous calendar testcase from diag * guard against future version downgrade * use new entry for downgrade test * switch to lmcloud 1.1.11 * revert runtimedata * revert runtimedata * version to helper * conistent Generator * generator from typing_extensions --------- Co-authored-by: Joost Lekkerkerker --- .../components/lamarzocco/__init__.py | 148 ++++++- .../components/lamarzocco/binary_sensor.py | 12 +- homeassistant/components/lamarzocco/button.py | 8 +- .../components/lamarzocco/calendar.py | 64 ++- .../components/lamarzocco/config_flow.py | 53 ++- homeassistant/components/lamarzocco/const.py | 4 +- .../components/lamarzocco/coordinator.py | 174 ++++---- .../components/lamarzocco/diagnostics.py | 44 +- homeassistant/components/lamarzocco/entity.py | 38 +- .../components/lamarzocco/icons.json | 10 +- .../components/lamarzocco/manifest.json | 2 +- homeassistant/components/lamarzocco/number.py | 163 ++++---- homeassistant/components/lamarzocco/select.py | 74 +++- homeassistant/components/lamarzocco/sensor.py | 28 +- .../components/lamarzocco/strings.json | 11 +- homeassistant/components/lamarzocco/switch.py | 40 +- homeassistant/components/lamarzocco/update.py | 26 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lamarzocco/__init__.py | 23 +- tests/components/lamarzocco/conftest.py | 143 ++++--- .../lamarzocco/fixtures/config.json | 77 ++-- .../lamarzocco/fixtures/current_status.json | 59 --- .../lamarzocco/fixtures/schedule.json | 44 -- .../lamarzocco/snapshots/test_calendar.ambr | 314 +++++++++++---- .../snapshots/test_diagnostics.ambr | 376 +++++------------- .../lamarzocco/snapshots/test_number.ambr | 124 +++--- .../lamarzocco/snapshots/test_select.ambr | 14 +- .../lamarzocco/snapshots/test_sensor.ambr | 8 +- .../lamarzocco/snapshots/test_switch.ambr | 192 +-------- .../lamarzocco/snapshots/test_update.ambr | 8 +- .../lamarzocco/test_binary_sensor.py | 31 +- tests/components/lamarzocco/test_calendar.py | 68 ++-- .../components/lamarzocco/test_config_flow.py | 236 ++++++----- tests/components/lamarzocco/test_init.py | 159 +++++++- tests/components/lamarzocco/test_number.py | 212 ++++++---- tests/components/lamarzocco/test_select.py | 22 +- tests/components/lamarzocco/test_switch.py | 57 +-- tests/components/lamarzocco/test_update.py | 8 +- 39 files changed, 1579 insertions(+), 1499 deletions(-) delete mode 100644 tests/components/lamarzocco/fixtures/current_status.json delete mode 100644 tests/components/lamarzocco/fixtures/schedule.json diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index d2a7bbb6216..e6bb3b1d3ae 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -1,10 +1,31 @@ """The La Marzocco integration.""" -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +import logging -from .const import DOMAIN +from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient +from lmcloud.const import BT_MODEL_PREFIXES, FirmwareType +from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from packaging import version + +from homeassistant.components.bluetooth import async_discovered_service_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_MODEL, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers.httpx_client import get_async_client + +from .const import CONF_USE_BLUETOOTH, DOMAIN from .coordinator import LaMarzoccoUpdateCoordinator PLATFORMS = [ @@ -18,15 +39,89 @@ PLATFORMS = [ Platform.UPDATE, ] +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up La Marzocco as config entry.""" - coordinator = LaMarzoccoUpdateCoordinator(hass) + assert entry.unique_id + serial = entry.unique_id + cloud_client = LaMarzoccoCloudClient( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + + # initialize local API + local_client: LaMarzoccoLocalClient | None = None + if (host := entry.data.get(CONF_HOST)) is not None: + _LOGGER.debug("Initializing local API") + local_client = LaMarzoccoLocalClient( + host=host, + local_bearer=entry.data[CONF_TOKEN], + client=get_async_client(hass), + ) + + # initialize Bluetooth + bluetooth_client: LaMarzoccoBluetoothClient | None = None + if entry.options.get(CONF_USE_BLUETOOTH, True): + + def bluetooth_configured() -> bool: + return entry.data.get(CONF_MAC, "") and entry.data.get(CONF_NAME, "") + + if not bluetooth_configured(): + for discovery_info in async_discovered_service_info(hass): + if ( + (name := discovery_info.name) + and name.startswith(BT_MODEL_PREFIXES) + and name.split("_")[1] == serial + ): + _LOGGER.debug("Found Bluetooth device, configuring with Bluetooth") + # found a device, add MAC address to config entry + hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_MAC: discovery_info.address, + CONF_NAME: discovery_info.name, + }, + ) + break + + if bluetooth_configured(): + _LOGGER.debug("Initializing Bluetooth device") + bluetooth_client = LaMarzoccoBluetoothClient( + username=entry.data[CONF_USERNAME], + serial_number=serial, + token=entry.data[CONF_TOKEN], + address_or_ble_device=entry.data[CONF_MAC], + ) + + coordinator = LaMarzoccoUpdateCoordinator( + hass=hass, + local_client=local_client, + cloud_client=cloud_client, + bluetooth_client=bluetooth_client, + ) + + await coordinator.async_setup() await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version + if version.parse(gateway_version) < version.parse("v3.5-rc5"): + # incompatible gateway firmware, create an issue + ir.async_create_issue( + hass, + DOMAIN, + "unsupported_gateway_firmware", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="unsupported_gateway_firmware", + translation_placeholders={"gateway_version": gateway_version}, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -39,10 +134,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate config entry.""" + if entry.version > 2: + # guard against downgrade from a future version + return False + + if entry.version == 1: + cloud_client = LaMarzoccoCloudClient( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + try: + fleet = await cloud_client.get_customer_fleet() + except (AuthFail, RequestNotSuccessful) as exc: + _LOGGER.error("Migration failed with error %s", exc) + return False + + assert entry.unique_id is not None + device = fleet[entry.unique_id] + v2_data = { + CONF_USERNAME: entry.data[CONF_USERNAME], + CONF_PASSWORD: entry.data[CONF_PASSWORD], + CONF_MODEL: device.model, + CONF_NAME: device.name, + CONF_TOKEN: device.communication_key, + } + + if CONF_HOST in entry.data: + v2_data[CONF_HOST] = entry.data[CONF_HOST] + + if CONF_MAC in entry.data: + v2_data[CONF_MAC] = entry.data[CONF_MAC] + + hass.config_entries.async_update_entry( + entry, + data=v2_data, + version=2, + ) + _LOGGER.debug("Migrated La Marzocco config entry to version 2") + return True diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 0eb28fa9558..86b18888fc5 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -26,7 +26,7 @@ class LaMarzoccoBinarySensorEntityDescription( ): """Description of a La Marzocco binary sensor.""" - is_on_fn: Callable[[LaMarzoccoClient], bool] + is_on_fn: Callable[[LaMarzoccoMachineConfig], bool] ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( @@ -34,7 +34,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( key="water_tank", translation_key="water_tank", device_class=BinarySensorDeviceClass.PROBLEM, - is_on_fn=lambda lm: not lm.current_status.get("water_reservoir_contact"), + is_on_fn=lambda config: not config.water_contact, entity_category=EntityCategory.DIAGNOSTIC, supported_fn=lambda coordinator: coordinator.local_connection_configured, ), @@ -42,8 +42,8 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( key="brew_active", translation_key="brew_active", device_class=BinarySensorDeviceClass.RUNNING, - is_on_fn=lambda lm: bool(lm.current_status.get("brew_active")), - available_fn=lambda lm: lm.websocket_connected, + is_on_fn=lambda config: config.brew_active, + available_fn=lambda device: device.websocket_connected, entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -72,4 +72,4 @@ class LaMarzoccoBinarySensorEntity(LaMarzoccoEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.entity_description.is_on_fn(self.coordinator.lm) + return self.entity_description.is_on_fn(self.coordinator.device.config) diff --git a/homeassistant/components/lamarzocco/button.py b/homeassistant/components/lamarzocco/button.py index 68bae5feeb9..ec0477647d8 100644 --- a/homeassistant/components/lamarzocco/button.py +++ b/homeassistant/components/lamarzocco/button.py @@ -4,7 +4,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry @@ -22,14 +22,14 @@ class LaMarzoccoButtonEntityDescription( ): """Description of a La Marzocco button.""" - press_fn: Callable[[LaMarzoccoClient], Coroutine[Any, Any, None]] + press_fn: Callable[[LaMarzoccoMachine], Coroutine[Any, Any, None]] ENTITIES: tuple[LaMarzoccoButtonEntityDescription, ...] = ( LaMarzoccoButtonEntityDescription( key="start_backflush", translation_key="start_backflush", - press_fn=lambda lm: lm.start_backflush(), + press_fn=lambda machine: machine.start_backflush(), ), ) @@ -56,4 +56,4 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity): async def async_press(self) -> None: """Press button.""" - await self.entity_description.press_fn(self.coordinator.lm) + await self.entity_description.press_fn(self.coordinator.device) diff --git a/homeassistant/components/lamarzocco/calendar.py b/homeassistant/components/lamarzocco/calendar.py index 2a08a90a1b2..b3a8774a1cf 100644 --- a/homeassistant/components/lamarzocco/calendar.py +++ b/homeassistant/components/lamarzocco/calendar.py @@ -3,6 +3,8 @@ from collections.abc import Iterator from datetime import datetime, timedelta +from lmcloud.models import LaMarzoccoWakeUpSleepEntry + from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -10,10 +12,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import DOMAIN +from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoBaseEntity CALENDAR_KEY = "auto_on_off_schedule" +DAY_OF_WEEK = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +] + async def async_setup_entry( hass: HomeAssistant, @@ -23,7 +36,10 @@ async def async_setup_entry( """Set up switch entities and services.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY)]) + async_add_entities( + LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY, wake_up_sleep_entry) + for wake_up_sleep_entry in coordinator.device.config.wake_up_sleep_entries.values() + ) class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity): @@ -31,6 +47,17 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity): _attr_translation_key = CALENDAR_KEY + def __init__( + self, + coordinator: LaMarzoccoUpdateCoordinator, + key: str, + wake_up_sleep_entry: LaMarzoccoWakeUpSleepEntry, + ) -> None: + """Set up calendar.""" + super().__init__(coordinator, f"{key}_{wake_up_sleep_entry.entry_id}") + self.wake_up_sleep_entry = wake_up_sleep_entry + self._attr_translation_placeholders = {"id": wake_up_sleep_entry.entry_id} + @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" @@ -85,29 +112,36 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity): """Return calendar event for a given weekday.""" # check first if auto/on off is turned on in general - # because could still be on for that day but disabled - if self.coordinator.lm.current_status["global_auto"] != "Enabled": + if not self.wake_up_sleep_entry.enabled: return None # parse the schedule for the day - schedule_day = self.coordinator.lm.schedule[date.weekday()] - if schedule_day["enable"] == "Disabled": + + if DAY_OF_WEEK[date.weekday()] not in self.wake_up_sleep_entry.days: return None - hour_on, minute_on = schedule_day["on"].split(":") - hour_off, minute_off = schedule_day["off"].split(":") + + hour_on, minute_on = self.wake_up_sleep_entry.time_on.split(":") + hour_off, minute_off = self.wake_up_sleep_entry.time_off.split(":") + + # if off time is 24:00, then it means the off time is the next day + # only for legacy schedules + day_offset = 0 + if hour_off == "24": + day_offset = 1 + hour_off = "0" + + end_date = date.replace( + hour=int(hour_off), + minute=int(minute_off), + ) + end_date += timedelta(days=day_offset) + return CalendarEvent( start=date.replace( hour=int(hour_on), minute=int(minute_on), - second=0, - microsecond=0, - ), - end=date.replace( - hour=int(hour_off), - minute=int(minute_off), - second=0, - microsecond=0, ), + end=end_date, summary=f"Machine {self.coordinator.config_entry.title} on", description="Machine is scheduled to turn on at the start time and off at the end time", ) diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index 3cacdae1749..b4fed615733 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -4,8 +4,10 @@ from collections.abc import Mapping import logging from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.models import LaMarzoccoDeviceInfo import voluptuous as vol from homeassistant.components.bluetooth import BluetoothServiceInfo @@ -19,12 +21,15 @@ from homeassistant.config_entries import ( from homeassistant.const import ( CONF_HOST, CONF_MAC, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, + CONF_TOKEN, CONF_USERNAME, ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -32,7 +37,9 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, ) -from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN +from .const import CONF_USE_BLUETOOTH, DOMAIN + +CONF_MACHINE = "machine" _LOGGER = logging.getLogger(__name__) @@ -40,12 +47,14 @@ _LOGGER = logging.getLogger(__name__) class LmConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for La Marzocco.""" + VERSION = 2 + def __init__(self) -> None: """Initialize the config flow.""" self.reauth_entry: ConfigEntry | None = None self._config: dict[str, Any] = {} - self._machines: list[tuple[str, str]] = [] + self._fleet: dict[str, LaMarzoccoDeviceInfo] = {} self._discovered: dict[str, str] = {} async def async_step_user( @@ -65,9 +74,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): **self._discovered, } - lm = LaMarzoccoClient() + cloud_client = LaMarzoccoCloudClient( + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + ) try: - self._machines = await lm.get_all_machines(data) + self._fleet = await cloud_client.get_customer_fleet() except AuthFail: _LOGGER.debug("Server rejected login credentials") errors["base"] = "invalid_auth" @@ -75,7 +87,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("Error connecting to server: %s", exc) errors["base"] = "cannot_connect" else: - if not self._machines: + if not self._fleet: errors["base"] = "no_machines" if not errors: @@ -88,8 +100,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="reauth_successful") if self._discovered: - serials = [machine[0] for machine in self._machines] - if self._discovered[CONF_MACHINE] not in serials: + if self._discovered[CONF_MACHINE] not in self._fleet: errors["base"] = "machine_not_found" else: self._config = data @@ -128,28 +139,36 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): else: serial_number = self._discovered[CONF_MACHINE] + selected_device = self._fleet[serial_number] + # validate local connection if host is provided if user_input.get(CONF_HOST): - lm = LaMarzoccoClient() - if not await lm.check_local_connection( - credentials=self._config, + if not await LaMarzoccoLocalClient.validate_connection( + client=get_async_client(self.hass), host=user_input[CONF_HOST], - serial=serial_number, + token=selected_device.communication_key, ): errors[CONF_HOST] = "cannot_connect" + else: + self._config[CONF_HOST] = user_input[CONF_HOST] if not errors: return self.async_create_entry( - title=serial_number, - data=self._config | user_input, + title=selected_device.name, + data={ + **self._config, + CONF_NAME: selected_device.name, + CONF_MODEL: selected_device.model, + CONF_TOKEN: selected_device.communication_key, + }, ) machine_options = [ SelectOptionDict( - value=serial_number, - label=f"{model_name} ({serial_number})", + value=device.serial_number, + label=f"{device.model} ({device.serial_number})", ) - for serial_number, model_name in self._machines + for device in self._fleet.values() ] machine_selection_schema = vol.Schema( diff --git a/homeassistant/components/lamarzocco/const.py b/homeassistant/components/lamarzocco/const.py index 87878ea5089..57db84f94da 100644 --- a/homeassistant/components/lamarzocco/const.py +++ b/homeassistant/components/lamarzocco/const.py @@ -4,6 +4,4 @@ from typing import Final DOMAIN: Final = "lamarzocco" -CONF_MACHINE: Final = "machine" - -CONF_USE_BLUETOOTH = "use_bluetooth" +CONF_USE_BLUETOOTH: Final = "use_bluetooth" diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index c26e981208d..2c78a925ca4 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -3,133 +3,108 @@ from collections.abc import Callable, Coroutine from datetime import timedelta import logging +from time import time +from typing import Any -from bleak.backends.device import BLEDevice -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import BT_MODEL_NAMES +from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.lm_machine import LaMarzoccoMachine -from homeassistant.components.bluetooth import ( - async_ble_device_from_address, - async_discovered_service_info, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_USERNAME +from homeassistant.const import CONF_MODEL, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN +from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=30) +FIRMWARE_UPDATE_INTERVAL = 3600 +STATISTICS_UPDATE_INTERVAL = 300 _LOGGER = logging.getLogger(__name__) -NAME_PREFIXES = tuple(BT_MODEL_NAMES) - - class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): """Class to handle fetching data from the La Marzocco API centrally.""" config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant) -> None: + def __init__( + self, + hass: HomeAssistant, + cloud_client: LaMarzoccoCloudClient, + local_client: LaMarzoccoLocalClient | None, + bluetooth_client: LaMarzoccoBluetoothClient | None, + ) -> None: """Initialize coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) - self.lm = LaMarzoccoClient( - callback_websocket_notify=self.async_update_listeners, - ) - self.local_connection_configured = ( - self.config_entry.data.get(CONF_HOST) is not None - ) - self._use_bluetooth = False + self.local_connection_configured = local_client is not None - async def _async_update_data(self) -> None: - """Fetch data from API endpoint.""" - - if not self.lm.initialized: - await self._async_init_client() - - await self._async_handle_request( - self.lm.update_local_machine_status, force_update=True + assert self.config_entry.unique_id + self.device = LaMarzoccoMachine( + model=self.config_entry.data[CONF_MODEL], + serial_number=self.config_entry.unique_id, + name=self.config_entry.data[CONF_NAME], + cloud_client=cloud_client, + local_client=local_client, + bluetooth_client=bluetooth_client, ) - _LOGGER.debug("Current status: %s", str(self.lm.current_status)) + self._last_firmware_data_update: float | None = None + self._last_statistics_data_update: float | None = None + self._local_client = local_client - async def _async_init_client(self) -> None: - """Initialize the La Marzocco Client.""" - - # Initialize cloud API - _LOGGER.debug("Initializing Cloud API") - await self._async_handle_request( - self.lm.init_cloud_api, - credentials=self.config_entry.data, - machine_serial=self.config_entry.data[CONF_MACHINE], - ) - _LOGGER.debug("Model name: %s", self.lm.model_name) - - # initialize local API - if (host := self.config_entry.data.get(CONF_HOST)) is not None: - _LOGGER.debug("Initializing local API") - await self.lm.init_local_api( - host=host, - client=get_async_client(self.hass), - ) - - _LOGGER.debug("Init WebSocket in Background Task") + async def async_setup(self) -> None: + """Set up the coordinator.""" + if self._local_client is not None: + _LOGGER.debug("Init WebSocket in background task") self.config_entry.async_create_background_task( hass=self.hass, - target=self.lm.lm_local_api.websocket_connect( - callback=self.lm.on_websocket_message_received, - use_sigterm_handler=False, + target=self.device.websocket_connect( + notify_callback=lambda: self.async_set_updated_data(None) ), name="lm_websocket_task", ) - # initialize Bluetooth - if self.config_entry.options.get(CONF_USE_BLUETOOTH, True): + async def websocket_close(_: Any | None = None) -> None: + if ( + self._local_client is not None + and self._local_client.websocket is not None + and self._local_client.websocket.open + ): + self._local_client.terminating = True + await self._local_client.websocket.close() - def bluetooth_configured() -> bool: - return self.config_entry.data.get( - CONF_MAC, "" - ) and self.config_entry.data.get(CONF_NAME, "") - - if not bluetooth_configured(): - machine = self.config_entry.data[CONF_MACHINE] - for discovery_info in async_discovered_service_info(self.hass): - if ( - (name := discovery_info.name) - and name.startswith(NAME_PREFIXES) - and name.split("_")[1] == machine - ): - _LOGGER.debug( - "Found Bluetooth device, configuring with Bluetooth" - ) - # found a device, add MAC address to config entry - self.hass.config_entries.async_update_entry( - self.config_entry, - data={ - **self.config_entry.data, - CONF_MAC: discovery_info.address, - CONF_NAME: discovery_info.name, - }, - ) - break - - if bluetooth_configured(): - # config entry contains BT config - _LOGGER.debug("Initializing with known Bluetooth device") - await self.lm.init_bluetooth_with_known_device( - self.config_entry.data[CONF_USERNAME], - self.config_entry.data.get(CONF_MAC, ""), - self.config_entry.data.get(CONF_NAME, ""), + self.config_entry.async_on_unload( + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, websocket_close ) - self._use_bluetooth = True + ) + self.config_entry.async_on_unload(websocket_close) - self.lm.initialized = True + async def _async_update_data(self) -> None: + """Fetch data from API endpoint.""" + await self._async_handle_request(self.device.get_config) + + if ( + self._last_firmware_data_update is None + or (self._last_firmware_data_update + FIRMWARE_UPDATE_INTERVAL) < time() + ): + await self._async_handle_request(self.device.get_firmware) + self._last_firmware_data_update = time() + + if ( + self._last_statistics_data_update is None + or (self._last_statistics_data_update + STATISTICS_UPDATE_INTERVAL) < time() + ): + await self._async_handle_request(self.device.get_statistics) + self._last_statistics_data_update = time() + + _LOGGER.debug("Current status: %s", str(self.device.config)) async def _async_handle_request[**_P]( self, @@ -137,9 +112,8 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): *args: _P.args, **kwargs: _P.kwargs, ) -> None: - """Handle a request to the API.""" try: - await func(*args, **kwargs) + await func() except AuthFail as ex: msg = "Authentication failed." _LOGGER.debug(msg, exc_info=True) @@ -147,15 +121,3 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): except RequestNotSuccessful as ex: _LOGGER.debug(ex, exc_info=True) raise UpdateFailed(f"Querying API failed. Error: {ex}") from ex - - def async_get_ble_device(self) -> BLEDevice | None: - """Get a Bleak Client for the machine.""" - # according to HA best practices, we should not reuse the same client - # get a new BLE device from hass and init a new Bleak Client with it - if not self._use_bluetooth: - return None - - return async_ble_device_from_address( - self.hass, - self.lm.lm_bluetooth.address, - ) diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py index 648d1357a35..04aed25defe 100644 --- a/homeassistant/components/lamarzocco/diagnostics.py +++ b/homeassistant/components/lamarzocco/diagnostics.py @@ -2,7 +2,10 @@ from __future__ import annotations -from typing import Any +from dataclasses import asdict +from typing import Any, TypedDict + +from lmcloud.const import FirmwareType from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -13,31 +16,30 @@ from .coordinator import LaMarzoccoUpdateCoordinator TO_REDACT = { "serial_number", - "machine_sn", } +class DiagnosticsData(TypedDict): + """Diagnostic data for La Marzocco.""" + + model: str + config: dict[str, Any] + firmware: list[dict[FirmwareType, dict[str, Any]]] + statistics: dict[str, Any] + + async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: LaMarzoccoUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: LaMarzoccoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + device = coordinator.device # collect all data sources - data = {} - data["current_status"] = coordinator.lm.current_status - data["machine_info"] = coordinator.lm.machine_info - data["config"] = coordinator.lm.config - data["statistics"] = {"stats": coordinator.lm.statistics} # wrap to satisfy mypy + diagnostics_data = DiagnosticsData( + model=device.model, + config=asdict(device.config), + firmware=[{key: asdict(firmware)} for key, firmware in device.firmware.items()], + statistics=asdict(device.statistics), + ) - # build a firmware section - data["firmware"] = { - "machine": { - "version": coordinator.lm.firmware_version, - "latest_version": coordinator.lm.latest_firmware_version, - }, - "gateway": { - "version": coordinator.lm.gateway_version, - "latest_version": coordinator.lm.latest_gateway_version, - }, - } - return async_redact_data(data, TO_REDACT) + return async_redact_data(diagnostics_data, TO_REDACT) diff --git a/homeassistant/components/lamarzocco/entity.py b/homeassistant/components/lamarzocco/entity.py index 4cb9d4a580a..9cc2ce8ef6b 100644 --- a/homeassistant/components/lamarzocco/entity.py +++ b/homeassistant/components/lamarzocco/entity.py @@ -3,7 +3,8 @@ from collections.abc import Callable from dataclasses import dataclass -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.const import FirmwareType +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription @@ -17,11 +18,13 @@ from .coordinator import LaMarzoccoUpdateCoordinator class LaMarzoccoEntityDescription(EntityDescription): """Description for all LM entities.""" - available_fn: Callable[[LaMarzoccoClient], bool] = lambda _: True + available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True -class LaMarzoccoBaseEntity(CoordinatorEntity[LaMarzoccoUpdateCoordinator]): +class LaMarzoccoBaseEntity( + CoordinatorEntity[LaMarzoccoUpdateCoordinator], +): """Common elements for all entities.""" _attr_has_entity_name = True @@ -33,15 +36,15 @@ class LaMarzoccoBaseEntity(CoordinatorEntity[LaMarzoccoUpdateCoordinator]): ) -> None: """Initialize the entity.""" super().__init__(coordinator) - lm = coordinator.lm - self._attr_unique_id = f"{lm.serial_number}_{key}" + device = coordinator.device + self._attr_unique_id = f"{device.serial_number}_{key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, lm.serial_number)}, - name=lm.machine_name, + identifiers={(DOMAIN, device.serial_number)}, + name=device.name, manufacturer="La Marzocco", - model=lm.true_model_name, - serial_number=lm.serial_number, - sw_version=lm.firmware_version, + model=device.full_model_name, + serial_number=device.serial_number, + sw_version=device.firmware[FirmwareType.MACHINE].current_version, ) @@ -50,19 +53,18 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity): entity_description: LaMarzoccoEntityDescription + @property + def available(self) -> bool: + """Return True if entity is available.""" + if super().available: + return self.entity_description.available_fn(self.coordinator.device) + return False + def __init__( self, coordinator: LaMarzoccoUpdateCoordinator, entity_description: LaMarzoccoEntityDescription, ) -> None: """Initialize the entity.""" - super().__init__(coordinator, entity_description.key) self.entity_description = entity_description - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return super().available and self.entity_description.available_fn( - self.coordinator.lm - ) diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index 727d3c66009..965ee7e3c3f 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -26,10 +26,7 @@ "default": "mdi:thermometer-water" }, "dose": { - "default": "mdi:weight-kilogram" - }, - "steam_temp": { - "default": "mdi:thermometer-water" + "default": "mdi:cup-water" }, "prebrew_off": { "default": "mdi:water-off" @@ -40,6 +37,9 @@ "preinfusion_off": { "default": "mdi:water" }, + "steam_temp": { + "default": "mdi:thermometer-water" + }, "tea_water_duration": { "default": "mdi:timer-sand" } @@ -58,7 +58,7 @@ "state": { "disabled": "mdi:water-pump-off", "prebrew": "mdi:water-pump", - "preinfusion": "mdi:water-pump" + "typeb": "mdi:water-pump" } } }, diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index ec6068e1988..7714b13d12b 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["lmcloud"], - "requirements": ["lmcloud==0.4.35"] + "requirements": ["lmcloud==1.1.11"] } diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index af5256bc77b..89bb5e75dd2 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -4,8 +4,15 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import KEYS_PER_MODEL, LaMarzoccoModel +from lmcloud.const import ( + KEYS_PER_MODEL, + BoilerType, + MachineModel, + PhysicalKey, + PrebrewMode, +) +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.number import ( NumberDeviceClass, @@ -35,10 +42,8 @@ class LaMarzoccoNumberEntityDescription( ): """Description of a La Marzocco number entity.""" - native_value_fn: Callable[[LaMarzoccoClient], float | int] - set_value_fn: Callable[ - [LaMarzoccoUpdateCoordinator, float | int], Coroutine[Any, Any, bool] - ] + native_value_fn: Callable[[LaMarzoccoMachineConfig], float | int] + set_value_fn: Callable[[LaMarzoccoMachine, float | int], Coroutine[Any, Any, bool]] @dataclass(frozen=True, kw_only=True) @@ -48,9 +53,9 @@ class LaMarzoccoKeyNumberEntityDescription( ): """Description of an La Marzocco number entity with keys.""" - native_value_fn: Callable[[LaMarzoccoClient, int], float | int] + native_value_fn: Callable[[LaMarzoccoMachineConfig, PhysicalKey], float | int] set_value_fn: Callable[ - [LaMarzoccoClient, float | int, int], Coroutine[Any, Any, bool] + [LaMarzoccoMachine, float | int, PhysicalKey], Coroutine[Any, Any, bool] ] @@ -63,10 +68,10 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_TENTHS, native_min_value=85, native_max_value=104, - set_value_fn=lambda coordinator, temp: coordinator.lm.set_coffee_temp( - temp, coordinator.async_get_ble_device() - ), - native_value_fn=lambda lm: lm.current_status["coffee_set_temp"], + set_value_fn=lambda machine, temp: machine.set_temp(BoilerType.COFFEE, temp), + native_value_fn=lambda config: config.boilers[ + BoilerType.COFFEE + ].target_temperature, ), LaMarzoccoNumberEntityDescription( key="steam_temp", @@ -76,14 +81,14 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_WHOLE, native_min_value=126, native_max_value=131, - set_value_fn=lambda coordinator, temp: coordinator.lm.set_steam_temp( - int(temp), coordinator.async_get_ble_device() - ), - native_value_fn=lambda lm: lm.current_status["steam_set_temp"], - supported_fn=lambda coordinator: coordinator.lm.model_name + set_value_fn=lambda machine, temp: machine.set_temp(BoilerType.STEAM, temp), + native_value_fn=lambda config: config.boilers[ + BoilerType.STEAM + ].target_temperature, + supported_fn=lambda coordinator: coordinator.device.model in ( - LaMarzoccoModel.GS3_AV, - LaMarzoccoModel.GS3_MP, + MachineModel.GS3_AV, + MachineModel.GS3_MP, ), ), LaMarzoccoNumberEntityDescription( @@ -94,54 +99,17 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_WHOLE, native_min_value=0, native_max_value=30, - set_value_fn=lambda coordinator, value: coordinator.lm.set_dose_hot_water( - value=int(value) - ), - native_value_fn=lambda lm: lm.current_status["dose_hot_water"], - supported_fn=lambda coordinator: coordinator.lm.model_name + set_value_fn=lambda machine, value: machine.set_dose_tea_water(int(value)), + native_value_fn=lambda config: config.dose_hot_water, + supported_fn=lambda coordinator: coordinator.device.model in ( - LaMarzoccoModel.GS3_AV, - LaMarzoccoModel.GS3_MP, + MachineModel.GS3_AV, + MachineModel.GS3_MP, ), ), ) -async def _set_prebrew_on( - lm: LaMarzoccoClient, - value: float, - key: int, -) -> bool: - return await lm.configure_prebrew( - on_time=int(value * 1000), - off_time=int(lm.current_status[f"prebrewing_toff_k{key}"] * 1000), - key=key, - ) - - -async def _set_prebrew_off( - lm: LaMarzoccoClient, - value: float, - key: int, -) -> bool: - return await lm.configure_prebrew( - on_time=int(lm.current_status[f"prebrewing_ton_k{key}"] * 1000), - off_time=int(value * 1000), - key=key, - ) - - -async def _set_preinfusion( - lm: LaMarzoccoClient, - value: float, - key: int, -) -> bool: - return await lm.configure_prebrew( - off_time=int(value * 1000), - key=key, - ) - - KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( LaMarzoccoKeyNumberEntityDescription( key="prebrew_off", @@ -152,11 +120,14 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=1, native_max_value=10, entity_category=EntityCategory.CONFIG, - set_value_fn=_set_prebrew_off, - native_value_fn=lambda lm, key: lm.current_status[f"prebrewing_ton_k{key}"], - available_fn=lambda lm: lm.current_status["enable_prebrewing"], - supported_fn=lambda coordinator: coordinator.lm.model_name - != LaMarzoccoModel.GS3_MP, + set_value_fn=lambda machine, value, key: machine.set_prebrew_time( + prebrew_off_time=value, key=key + ), + native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time, + available_fn=lambda device: len(device.config.prebrew_configuration) > 0 + and device.config.prebrew_mode == PrebrewMode.PREBREW, + supported_fn=lambda coordinator: coordinator.device.model + != MachineModel.GS3_MP, ), LaMarzoccoKeyNumberEntityDescription( key="prebrew_on", @@ -167,11 +138,14 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=2, native_max_value=10, entity_category=EntityCategory.CONFIG, - set_value_fn=_set_prebrew_on, - native_value_fn=lambda lm, key: lm.current_status[f"prebrewing_toff_k{key}"], - available_fn=lambda lm: lm.current_status["enable_prebrewing"], - supported_fn=lambda coordinator: coordinator.lm.model_name - != LaMarzoccoModel.GS3_MP, + set_value_fn=lambda machine, value, key: machine.set_prebrew_time( + prebrew_on_time=value, key=key + ), + native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time, + available_fn=lambda device: len(device.config.prebrew_configuration) > 0 + and device.config.prebrew_mode == PrebrewMode.PREBREW, + supported_fn=lambda coordinator: coordinator.device.model + != MachineModel.GS3_MP, ), LaMarzoccoKeyNumberEntityDescription( key="preinfusion_off", @@ -182,11 +156,16 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=2, native_max_value=29, entity_category=EntityCategory.CONFIG, - set_value_fn=_set_preinfusion, - native_value_fn=lambda lm, key: lm.current_status[f"preinfusion_k{key}"], - available_fn=lambda lm: lm.current_status["enable_preinfusion"], - supported_fn=lambda coordinator: coordinator.lm.model_name - != LaMarzoccoModel.GS3_MP, + set_value_fn=lambda machine, value, key: machine.set_preinfusion_time( + preinfusion_time=value, key=key + ), + native_value_fn=lambda config, key: config.prebrew_configuration[ + key + ].preinfusion_time, + available_fn=lambda device: len(device.config.prebrew_configuration) > 0 + and device.config.prebrew_mode == PrebrewMode.PREINFUSION, + supported_fn=lambda coordinator: coordinator.device.model + != MachineModel.GS3_MP, ), LaMarzoccoKeyNumberEntityDescription( key="dose", @@ -196,10 +175,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=0, native_max_value=999, entity_category=EntityCategory.CONFIG, - set_value_fn=lambda lm, ticks, key: lm.set_dose(key=key, value=int(ticks)), - native_value_fn=lambda lm, key: lm.current_status[f"dose_k{key}"], - supported_fn=lambda coordinator: coordinator.lm.model_name - == LaMarzoccoModel.GS3_AV, + set_value_fn=lambda machine, ticks, key: machine.set_dose( + dose=int(ticks), key=key + ), + native_value_fn=lambda config, key: config.doses[key], + supported_fn=lambda coordinator: coordinator.device.model + == MachineModel.GS3_AV, ), ) @@ -211,7 +192,6 @@ async def async_setup_entry( ) -> None: """Set up number entities.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[NumberEntity] = [ LaMarzoccoNumberEntity(coordinator, description) for description in ENTITIES @@ -220,12 +200,11 @@ async def async_setup_entry( for description in KEY_ENTITIES: if description.supported_fn(coordinator): - num_keys = KEYS_PER_MODEL[coordinator.lm.model_name] + num_keys = KEYS_PER_MODEL[MachineModel(coordinator.device.model)] entities.extend( LaMarzoccoKeyNumberEntity(coordinator, description, key) for key in range(min(num_keys, 1), num_keys + 1) ) - async_add_entities(entities) @@ -237,12 +216,13 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity): @property def native_value(self) -> float: """Return the current value.""" - return self.entity_description.native_value_fn(self.coordinator.lm) + return self.entity_description.native_value_fn(self.coordinator.device.config) async def async_set_native_value(self, value: float) -> None: """Set the value.""" - await self.entity_description.set_value_fn(self.coordinator, value) - self.async_write_ha_state() + if value != self.native_value: + await self.entity_description.set_value_fn(self.coordinator.device, value) + self.async_write_ha_state() class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity): @@ -273,12 +253,13 @@ class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity): def native_value(self) -> float: """Return the current value.""" return self.entity_description.native_value_fn( - self.coordinator.lm, self.pyhsical_key + self.coordinator.device.config, PhysicalKey(self.pyhsical_key) ) async def async_set_native_value(self, value: float) -> None: """Set the value.""" - await self.entity_description.set_value_fn( - self.coordinator.lm, value, self.pyhsical_key - ) - self.async_write_ha_state() + if value != self.native_value: + await self.entity_description.set_value_fn( + self.coordinator.device, value, PhysicalKey(self.pyhsical_key) + ) + self.async_write_ha_state() diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index f063f8e6336..4e202db7c7c 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -4,18 +4,43 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import LaMarzoccoModel +from lmcloud.const import MachineModel, PrebrewMode, SteamLevel +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription +STEAM_LEVEL_HA_TO_LM = { + "1": SteamLevel.LEVEL_1, + "2": SteamLevel.LEVEL_2, + "3": SteamLevel.LEVEL_3, +} + +STEAM_LEVEL_LM_TO_HA = { + SteamLevel.LEVEL_1: "1", + SteamLevel.LEVEL_2: "2", + SteamLevel.LEVEL_3: "3", +} + +PREBREW_MODE_HA_TO_LM = { + "disabled": PrebrewMode.DISABLED, + "prebrew": PrebrewMode.PREBREW, + "preinfusion": PrebrewMode.PREINFUSION, +} + +PREBREW_MODE_LM_TO_HA = { + PrebrewMode.DISABLED: "disabled", + PrebrewMode.PREBREW: "prebrew", + PrebrewMode.PREINFUSION: "preinfusion", +} + @dataclass(frozen=True, kw_only=True) class LaMarzoccoSelectEntityDescription( @@ -24,10 +49,8 @@ class LaMarzoccoSelectEntityDescription( ): """Description of a La Marzocco select entity.""" - current_option_fn: Callable[[LaMarzoccoClient], str] - select_option_fn: Callable[ - [LaMarzoccoUpdateCoordinator, str], Coroutine[Any, Any, bool] - ] + current_option_fn: Callable[[LaMarzoccoMachineConfig], str] + select_option_fn: Callable[[LaMarzoccoMachine, str], Coroutine[Any, Any, bool]] ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( @@ -35,25 +58,27 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( key="steam_temp_select", translation_key="steam_temp_select", options=["1", "2", "3"], - select_option_fn=lambda coordinator, option: coordinator.lm.set_steam_level( - int(option), coordinator.async_get_ble_device() + select_option_fn=lambda machine, option: machine.set_steam_level( + STEAM_LEVEL_HA_TO_LM[option] ), - current_option_fn=lambda lm: lm.current_status["steam_level_set"], - supported_fn=lambda coordinator: coordinator.lm.model_name - == LaMarzoccoModel.LINEA_MICRA, + current_option_fn=lambda config: STEAM_LEVEL_LM_TO_HA[config.steam_level], + supported_fn=lambda coordinator: coordinator.device.model + == MachineModel.LINEA_MICRA, ), LaMarzoccoSelectEntityDescription( key="prebrew_infusion_select", translation_key="prebrew_infusion_select", + entity_category=EntityCategory.CONFIG, options=["disabled", "prebrew", "preinfusion"], - select_option_fn=lambda coordinator, - option: coordinator.lm.select_pre_brew_infusion_mode(option.capitalize()), - current_option_fn=lambda lm: lm.pre_brew_infusion_mode.lower(), - supported_fn=lambda coordinator: coordinator.lm.model_name + select_option_fn=lambda machine, option: machine.set_prebrew_mode( + PREBREW_MODE_HA_TO_LM[option] + ), + current_option_fn=lambda config: PREBREW_MODE_LM_TO_HA[config.prebrew_mode], + supported_fn=lambda coordinator: coordinator.device.model in ( - LaMarzoccoModel.GS3_AV, - LaMarzoccoModel.LINEA_MICRA, - LaMarzoccoModel.LINEA_MINI, + MachineModel.GS3_AV, + MachineModel.LINEA_MICRA, + MachineModel.LINEA_MINI, ), ), ) @@ -82,9 +107,14 @@ class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity): @property def current_option(self) -> str: """Return the current selected option.""" - return str(self.entity_description.current_option_fn(self.coordinator.lm)) + return str( + self.entity_description.current_option_fn(self.coordinator.device.config) + ) async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.entity_description.select_option_fn(self.coordinator, option) - self.async_write_ha_state() + if option != self.current_option: + await self.entity_description.select_option_fn( + self.coordinator.device, option + ) + self.async_write_ha_state() diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index ea5a5e184e1..723661451c5 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -3,7 +3,8 @@ from collections.abc import Callable from dataclasses import dataclass -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.const import BoilerType, PhysicalKey +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.sensor import ( SensorDeviceClass, @@ -22,12 +23,11 @@ from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @dataclass(frozen=True, kw_only=True) class LaMarzoccoSensorEntityDescription( - LaMarzoccoEntityDescription, - SensorEntityDescription, + LaMarzoccoEntityDescription, SensorEntityDescription ): """Description of a La Marzocco sensor.""" - value_fn: Callable[[LaMarzoccoClient], float | int] + value_fn: Callable[[LaMarzoccoMachine], float | int] ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( @@ -36,7 +36,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( translation_key="drink_stats_coffee", native_unit_of_measurement="drinks", state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda lm: lm.current_status.get("drinks_k1", 0), + value_fn=lambda device: device.statistics.drink_stats.get(PhysicalKey.A, 0), + available_fn=lambda device: len(device.statistics.drink_stats) > 0, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoSensorEntityDescription( @@ -44,7 +45,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( translation_key="drink_stats_flushing", native_unit_of_measurement="drinks", state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda lm: lm.current_status.get("total_flushing", 0), + value_fn=lambda device: device.statistics.total_flushes, + available_fn=lambda device: len(device.statistics.drink_stats) > 0, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoSensorEntityDescription( @@ -53,8 +55,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DURATION, - value_fn=lambda lm: lm.current_status.get("brew_active_duration", 0), - available_fn=lambda lm: lm.websocket_connected, + value_fn=lambda device: device.config.brew_active_duration, + available_fn=lambda device: device.websocket_connected, entity_category=EntityCategory.DIAGNOSTIC, supported_fn=lambda coordinator: coordinator.local_connection_configured, ), @@ -65,7 +67,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda lm: lm.current_status.get("coffee_temp", 0), + value_fn=lambda device: device.config.boilers[ + BoilerType.COFFEE + ].current_temperature, ), LaMarzoccoSensorEntityDescription( key="current_temp_steam", @@ -74,7 +78,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda lm: lm.current_status.get("steam_temp", 0), + value_fn=lambda device: device.config.boilers[ + BoilerType.STEAM + ].current_temperature, ), ) @@ -102,4 +108,4 @@ class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity): @property def native_value(self) -> int | float: """State of the sensor.""" - return self.entity_description.value_fn(self.coordinator.lm) + return self.entity_description.value_fn(self.coordinator.device) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 03ce2eb93e8..744f4a0d63f 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -68,7 +68,7 @@ }, "calendar": { "auto_on_off_schedule": { - "name": "Auto on/off schedule" + "name": "Auto on/off schedule ({id})" } }, "number": { @@ -139,9 +139,6 @@ } }, "switch": { - "auto_on_off": { - "name": "Auto on/off" - }, "steam_boiler": { "name": "Steam boiler" } @@ -154,5 +151,11 @@ "name": "Gateway firmware" } } + }, + "issues": { + "unsupported_gateway_firmware": { + "title": "Unsupported gateway firmware", + "description": "Gateway firmware {gateway_version} is no longer supported by this integration, please update." + } } } diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index dd647bf4582..0c5939e6d59 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -4,14 +4,16 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any +from lmcloud.const import BoilerType +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig + from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -22,8 +24,8 @@ class LaMarzoccoSwitchEntityDescription( ): """Description of a La Marzocco Switch.""" - control_fn: Callable[[LaMarzoccoUpdateCoordinator, bool], Coroutine[Any, Any, bool]] - is_on_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] + control_fn: Callable[[LaMarzoccoMachine, bool], Coroutine[Any, Any, bool]] + is_on_fn: Callable[[LaMarzoccoMachineConfig], bool] ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( @@ -31,30 +33,14 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( key="main", translation_key="main", name=None, - control_fn=lambda coordinator, state: coordinator.lm.set_power( - state, coordinator.async_get_ble_device() - ), - is_on_fn=lambda coordinator: coordinator.lm.current_status["power"], - ), - LaMarzoccoSwitchEntityDescription( - key="auto_on_off", - translation_key="auto_on_off", - control_fn=lambda coordinator, state: coordinator.lm.set_auto_on_off_global( - state - ), - is_on_fn=lambda coordinator: coordinator.lm.current_status["global_auto"] - == "Enabled", - entity_category=EntityCategory.CONFIG, + control_fn=lambda machine, state: machine.set_power(state), + is_on_fn=lambda config: config.turned_on, ), LaMarzoccoSwitchEntityDescription( key="steam_boiler_enable", translation_key="steam_boiler", - control_fn=lambda coordinator, state: coordinator.lm.set_steam( - state, coordinator.async_get_ble_device() - ), - is_on_fn=lambda coordinator: coordinator.lm.current_status[ - "steam_boiler_enable" - ], + control_fn=lambda machine, state: machine.set_steam(state), + is_on_fn=lambda config: config.boilers[BoilerType.STEAM].enabled, ), ) @@ -81,15 +67,15 @@ class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" - await self.entity_description.control_fn(self.coordinator, True) + await self.entity_description.control_fn(self.coordinator.device, True) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" - await self.entity_description.control_fn(self.coordinator, False) + await self.entity_description.control_fn(self.coordinator.device, False) self.async_write_ha_state() @property def is_on(self) -> bool: """Return true if device is on.""" - return self.entity_description.is_on_fn(self.coordinator) + return self.entity_description.is_on_fn(self.coordinator.device.config) diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index cc3e665725b..f8891b30bf8 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -1,11 +1,9 @@ """Support for La Marzocco update entities.""" -from collections.abc import Callable from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import LaMarzoccoUpdateableComponent +from lmcloud.const import FirmwareType from homeassistant.components.update import ( UpdateDeviceClass, @@ -30,9 +28,7 @@ class LaMarzoccoUpdateEntityDescription( ): """Description of a La Marzocco update entities.""" - current_fw_fn: Callable[[LaMarzoccoClient], str] - latest_fw_fn: Callable[[LaMarzoccoClient], str] - component: LaMarzoccoUpdateableComponent + component: FirmwareType ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = ( @@ -40,18 +36,14 @@ ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = ( key="machine_firmware", translation_key="machine_firmware", device_class=UpdateDeviceClass.FIRMWARE, - current_fw_fn=lambda lm: lm.firmware_version, - latest_fw_fn=lambda lm: lm.latest_firmware_version, - component=LaMarzoccoUpdateableComponent.MACHINE, + component=FirmwareType.MACHINE, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoUpdateEntityDescription( key="gateway_firmware", translation_key="gateway_firmware", device_class=UpdateDeviceClass.FIRMWARE, - current_fw_fn=lambda lm: lm.gateway_version, - latest_fw_fn=lambda lm: lm.latest_gateway_version, - component=LaMarzoccoUpdateableComponent.GATEWAY, + component=FirmwareType.GATEWAY, entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -81,12 +73,16 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): @property def installed_version(self) -> str | None: """Return the current firmware version.""" - return self.entity_description.current_fw_fn(self.coordinator.lm) + return self.coordinator.device.firmware[ + self.entity_description.component + ].current_version @property def latest_version(self) -> str: """Return the latest firmware version.""" - return self.entity_description.latest_fw_fn(self.coordinator.lm) + return self.coordinator.device.firmware[ + self.entity_description.component + ].latest_version async def async_install( self, version: str | None, backup: bool, **kwargs: Any @@ -94,7 +90,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): """Install an update.""" self._attr_in_progress = True self.async_write_ha_state() - success = await self.coordinator.lm.update_firmware( + success = await self.coordinator.device.update_firmware( self.entity_description.component ) if not success: diff --git a/requirements_all.txt b/requirements_all.txt index c84760b1a07..0403cd555f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1260,7 +1260,7 @@ linear-garage-door==0.2.9 linode-api==4.1.9b1 # homeassistant.components.lamarzocco -lmcloud==0.4.35 +lmcloud==1.1.11 # homeassistant.components.google_maps locationsharinglib==5.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81cab2c4617..fe147205686 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ libsoundtouch==0.8 linear-garage-door==0.2.9 # homeassistant.components.lamarzocco -lmcloud==0.4.35 +lmcloud==1.1.11 # homeassistant.components.logi_circle logi-circle==0.2.3 diff --git a/tests/components/lamarzocco/__init__.py b/tests/components/lamarzocco/__init__.py index ed4d2e0990e..4d274d10baa 100644 --- a/tests/components/lamarzocco/__init__.py +++ b/tests/components/lamarzocco/__init__.py @@ -1,6 +1,6 @@ """Mock inputs for tests.""" -from lmcloud.const import LaMarzoccoModel +from lmcloud.const import MachineModel from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -18,31 +18,34 @@ PASSWORD_SELECTION = { USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"} -MODEL_DICT = { - LaMarzoccoModel.GS3_AV: ("GS01234", "GS3 AV"), - LaMarzoccoModel.GS3_MP: ("GS01234", "GS3 MP"), - LaMarzoccoModel.LINEA_MICRA: ("MR01234", "Linea Micra"), - LaMarzoccoModel.LINEA_MINI: ("LM01234", "Linea Mini"), +SERIAL_DICT = { + MachineModel.GS3_AV: "GS01234", + MachineModel.GS3_MP: "GS01234", + MachineModel.LINEA_MICRA: "MR01234", + MachineModel.LINEA_MINI: "LM01234", } +WAKE_UP_SLEEP_ENTRY_IDS = ["Os2OswX", "aXFz5bJ"] + async def async_init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Set up the La Marzocco integration for testing.""" + mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() def get_bluetooth_service_info( - model: LaMarzoccoModel, serial: str + model: MachineModel, serial: str ) -> BluetoothServiceInfo: """Return a mocked BluetoothServiceInfo.""" - if model in (LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP): + if model in (MachineModel.GS3_AV, MachineModel.GS3_MP): name = f"GS3_{serial}" - elif model == LaMarzoccoModel.LINEA_MINI: + elif model == MachineModel.LINEA_MINI: name = f"MINI_{serial}" - elif model == LaMarzoccoModel.LINEA_MICRA: + elif model == MachineModel.LINEA_MICRA: name = f"MICRA_{serial}" return BluetoothServiceInfo( name=name, diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 49aa20e3a46..6741ac0797c 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -1,22 +1,23 @@ """Lamarzocco session fixtures.""" +from collections.abc import Callable +import json from unittest.mock import MagicMock, patch -from lmcloud.const import LaMarzoccoModel +from bleak.backends.device import BLEDevice +from lmcloud.const import FirmwareType, MachineModel, SteamLevel +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoDeviceInfo import pytest from typing_extensions import Generator -from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.components.lamarzocco.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant -from . import MODEL_DICT, USER_INPUT, async_init_integration +from . import SERIAL_DICT, USER_INPUT, async_init_integration -from tests.common import ( - MockConfigEntry, - load_json_array_fixture, - load_json_object_fixture, -) +from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture @pytest.fixture @@ -27,12 +28,13 @@ def mock_config_entry( entry = MockConfigEntry( title="My LaMarzocco", domain=DOMAIN, + version=2, data=USER_INPUT | { - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MODEL: mock_lamarzocco.model, CONF_HOST: "host", - CONF_NAME: "name", - CONF_MAC: "mac", + CONF_TOKEN: "token", + CONF_NAME: "GS3", }, unique_id=mock_lamarzocco.serial_number, ) @@ -44,77 +46,96 @@ def mock_config_entry( async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock ) -> MockConfigEntry: - """Set up the LaMetric integration for testing.""" + """Set up the La Marzocco integration for testing.""" await async_init_integration(hass, mock_config_entry) return mock_config_entry @pytest.fixture -def device_fixture() -> LaMarzoccoModel: +def device_fixture() -> MachineModel: """Return the device fixture for a specific device.""" - return LaMarzoccoModel.GS3_AV + return MachineModel.GS3_AV @pytest.fixture -def mock_lamarzocco(device_fixture: LaMarzoccoModel) -> Generator[MagicMock]: - """Return a mocked LM client.""" - model_name = device_fixture +def mock_device_info() -> LaMarzoccoDeviceInfo: + """Return a mocked La Marzocco device info.""" + return LaMarzoccoDeviceInfo( + model=MachineModel.GS3_AV, + serial_number="GS01234", + name="GS3", + communication_key="token", + ) - (serial_number, true_model_name) = MODEL_DICT[model_name] + +@pytest.fixture +def mock_cloud_client( + mock_device_info: LaMarzoccoDeviceInfo, +) -> Generator[MagicMock]: + """Return a mocked LM cloud client.""" + with ( + patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoCloudClient", + autospec=True, + ) as cloud_client, + patch( + "homeassistant.components.lamarzocco.LaMarzoccoCloudClient", + new=cloud_client, + ), + ): + client = cloud_client.return_value + client.get_customer_fleet.return_value = { + mock_device_info.serial_number: mock_device_info + } + yield client + + +@pytest.fixture +def mock_lamarzocco(device_fixture: MachineModel) -> Generator[MagicMock]: + """Return a mocked LM client.""" + model = device_fixture + + serial_number = SERIAL_DICT[model] + + dummy_machine = LaMarzoccoMachine( + model=model, + serial_number=serial_number, + name=serial_number, + ) + config = load_json_object_fixture("config.json", DOMAIN) + statistics = json.loads(load_fixture("statistics.json", DOMAIN)) + + dummy_machine.parse_config(config) + dummy_machine.parse_statistics(statistics) with ( patch( - "homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient", + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine", autospec=True, ) as lamarzocco_mock, - patch( - "homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient", - new=lamarzocco_mock, - ), ): lamarzocco = lamarzocco_mock.return_value - lamarzocco.machine_info = { - "machine_name": serial_number, - "serial_number": serial_number, - } + lamarzocco.name = dummy_machine.name + lamarzocco.model = dummy_machine.model + lamarzocco.serial_number = dummy_machine.serial_number + lamarzocco.full_model_name = dummy_machine.full_model_name + lamarzocco.config = dummy_machine.config + lamarzocco.statistics = dummy_machine.statistics + lamarzocco.firmware = dummy_machine.firmware + lamarzocco.steam_level = SteamLevel.LEVEL_1 - lamarzocco.model_name = model_name - lamarzocco.true_model_name = true_model_name - lamarzocco.machine_name = serial_number - lamarzocco.serial_number = serial_number - - lamarzocco.firmware_version = "1.1" - lamarzocco.latest_firmware_version = "1.2" - lamarzocco.gateway_version = "v2.2-rc0" - lamarzocco.latest_gateway_version = "v3.1-rc4" - lamarzocco.update_firmware.return_value = True - - lamarzocco.current_status = load_json_object_fixture( - "current_status.json", DOMAIN - ) - lamarzocco.config = load_json_object_fixture("config.json", DOMAIN) - lamarzocco.statistics = load_json_array_fixture("statistics.json", DOMAIN) - lamarzocco.schedule = load_json_array_fixture("schedule.json", DOMAIN) - - lamarzocco.get_all_machines.return_value = [ - (serial_number, model_name), - ] - lamarzocco.check_local_connection.return_value = True - lamarzocco.initialized = False - lamarzocco.websocket_connected = True + lamarzocco.firmware[FirmwareType.GATEWAY].latest_version = "v3.5-rc3" + lamarzocco.firmware[FirmwareType.MACHINE].latest_version = "1.55" async def websocket_connect_mock( - callback: MagicMock, use_sigterm_handler: MagicMock + notify_callback: Callable | None, ) -> None: """Mock the websocket connect method.""" return None - lamarzocco.lm_local_api.websocket_connect = websocket_connect_mock - - lamarzocco.lm_bluetooth = MagicMock() - lamarzocco.lm_bluetooth.address = "AA:BB:CC:DD:EE:FF" + lamarzocco.websocket_connect = websocket_connect_mock yield lamarzocco @@ -133,3 +154,11 @@ def remove_local_connection( @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" + + +@pytest.fixture +def mock_ble_device() -> BLEDevice: + """Return a mock BLE device.""" + return BLEDevice( + "00:00:00:00:00:00", "GS_GS01234", details={"path": "path"}, rssi=50 + ) diff --git a/tests/components/lamarzocco/fixtures/config.json b/tests/components/lamarzocco/fixtures/config.json index 60d11b0d470..ea6e2ee76b8 100644 --- a/tests/components/lamarzocco/fixtures/config.json +++ b/tests/components/lamarzocco/fixtures/config.json @@ -13,11 +13,16 @@ "schedulingType": "weeklyScheduling" } ], - "machine_sn": "GS01234", + "machine_sn": "Sn01239157", "machine_hw": "2", "isPlumbedIn": true, "isBackFlushEnabled": false, "standByTime": 0, + "smartStandBy": { + "enabled": true, + "minutes": 10, + "mode": "LastBrewing" + }, "tankStatus": true, "groupCapabilities": [ { @@ -121,58 +126,32 @@ } ] }, - "weeklySchedulingConfig": { - "enabled": true, - "monday": { + "wakeUpSleepEntries": [ + { + "days": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 + "id": "Os2OswX", + "steam": true, + "timeOff": "24:0", + "timeOn": "22:0" }, - "tuesday": { + { + "days": ["sunday"], "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "wednesday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "thursday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "friday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "saturday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "sunday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 + "id": "aXFz5bJ", + "steam": true, + "timeOff": "7:30", + "timeOn": "7:0" } - }, + ], "clock": "1901-07-08T10:29:00", "firmwareVersions": [ { diff --git a/tests/components/lamarzocco/fixtures/current_status.json b/tests/components/lamarzocco/fixtures/current_status.json deleted file mode 100644 index f99c3d5c331..00000000000 --- a/tests/components/lamarzocco/fixtures/current_status.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "power": true, - "global_auto": "Enabled", - "enable_prebrewing": true, - "coffee_boiler_on": true, - "steam_boiler_on": true, - "enable_preinfusion": false, - "steam_boiler_enable": true, - "steam_temp": 113, - "steam_set_temp": 128, - "steam_level_set": 3, - "coffee_temp": 93, - "coffee_set_temp": 95, - "water_reservoir_contact": true, - "brew_active": false, - "drinks_k1": 13, - "drinks_k2": 2, - "drinks_k3": 42, - "drinks_k4": 34, - "total_flushing": 69, - "mon_auto": "Disabled", - "mon_on_time": "00:00", - "mon_off_time": "00:00", - "tue_auto": "Disabled", - "tue_on_time": "00:00", - "tue_off_time": "00:00", - "wed_auto": "Disabled", - "wed_on_time": "00:00", - "wed_off_time": "00:00", - "thu_auto": "Disabled", - "thu_on_time": "00:00", - "thu_off_time": "00:00", - "fri_auto": "Disabled", - "fri_on_time": "00:00", - "fri_off_time": "00:00", - "sat_auto": "Disabled", - "sat_on_time": "00:00", - "sat_off_time": "00:00", - "sun_auto": "Disabled", - "sun_on_time": "00:00", - "sun_off_time": "00:00", - "dose_k1": 1023, - "dose_k2": 1023, - "dose_k3": 1023, - "dose_k4": 1023, - "dose_hot_water": 1023, - "prebrewing_ton_k1": 3, - "prebrewing_toff_k1": 5, - "prebrewing_ton_k2": 3, - "prebrewing_toff_k2": 5, - "prebrewing_ton_k3": 3, - "prebrewing_toff_k3": 5, - "prebrewing_ton_k4": 3, - "prebrewing_toff_k4": 5, - "preinfusion_k1": 4, - "preinfusion_k2": 4, - "preinfusion_k3": 4, - "preinfusion_k4": 4 -} diff --git a/tests/components/lamarzocco/fixtures/schedule.json b/tests/components/lamarzocco/fixtures/schedule.json deleted file mode 100644 index 62550caaa0b..00000000000 --- a/tests/components/lamarzocco/fixtures/schedule.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "day": "MONDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - }, - { - "day": "TUESDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - }, - { - "day": "WEDNESDAY", - "enable": "Enabled", - "on": "08:00", - "off": "13:00" - }, - { - "day": "THURSDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - }, - { - "day": "FRIDAY", - "enable": "Enabled", - "on": "06:00", - "off": "09:00" - }, - { - "day": "SATURDAY", - "enable": "Enabled", - "on": "10:00", - "off": "23:00" - }, - { - "day": "SUNDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - } -] diff --git a/tests/components/lamarzocco/snapshots/test_calendar.ambr b/tests/components/lamarzocco/snapshots/test_calendar.ambr index 676c0f1b2ad..2fd5dab846a 100644 --- a/tests/components/lamarzocco/snapshots/test_calendar.ambr +++ b/tests/components/lamarzocco/snapshots/test_calendar.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_calendar_edge_cases[start_date0-end_date0] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -15,7 +15,7 @@ # --- # name: test_calendar_edge_cases[start_date1-end_date1] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -29,7 +29,7 @@ # --- # name: test_calendar_edge_cases[start_date2-end_date2] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -43,7 +43,7 @@ # --- # name: test_calendar_edge_cases[start_date3-end_date3] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -57,7 +57,7 @@ # --- # name: test_calendar_edge_cases[start_date4-end_date4] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ ]), }), @@ -65,7 +65,7 @@ # --- # name: test_calendar_edge_cases[start_date5-end_date5] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -83,26 +83,7 @@ }), }) # --- -# name: test_calendar_events - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'all_day': False, - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end_time': '2024-01-13 23:00:00', - 'friendly_name': 'GS01234 Auto on/off schedule', - 'location': '', - 'message': 'Machine My LaMarzocco on', - 'start_time': '2024-01-13 10:00:00', - }), - 'context': , - 'entity_id': 'calendar.gs01234_auto_on_off_schedule', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_calendar_events.1 +# name: test_calendar_events[entry.GS01234_auto_on_off_schedule_axfz5bj] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -114,7 +95,7 @@ 'disabled_by': None, 'domain': 'calendar', 'entity_category': None, - 'entity_id': 'calendar.gs01234_auto_on_off_schedule', + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_axfz5bj', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -126,86 +107,267 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Auto on/off schedule', + 'original_name': 'Auto on/off schedule (aXFz5bJ)', 'platform': 'lamarzocco', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off_schedule', - 'unique_id': 'GS01234_auto_on_off_schedule', + 'unique_id': 'GS01234_auto_on_off_schedule_aXFz5bJ', 'unit_of_measurement': None, }) # --- -# name: test_calendar_events.2 +# name: test_calendar_events[entry.GS01234_auto_on_off_schedule_os2oswx] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_os2oswx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto on/off schedule (Os2OswX)', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_on_off_schedule', + 'unique_id': 'GS01234_auto_on_off_schedule_Os2OswX', + 'unit_of_measurement': None, + }) +# --- +# name: test_calendar_events[events.GS01234_auto_on_off_schedule_axfz5bj] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-13T23:00:00-08:00', - 'start': '2024-01-13T10:00:00-08:00', + 'end': '2024-01-14T07:30:00-08:00', + 'start': '2024-01-14T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-17T13:00:00-08:00', - 'start': '2024-01-17T08:00:00-08:00', + 'end': '2024-01-21T07:30:00-08:00', + 'start': '2024-01-21T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-19T09:00:00-08:00', - 'start': '2024-01-19T06:00:00-08:00', + 'end': '2024-01-28T07:30:00-08:00', + 'start': '2024-01-28T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-20T23:00:00-08:00', - 'start': '2024-01-20T10:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-24T13:00:00-08:00', - 'start': '2024-01-24T08:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-26T09:00:00-08:00', - 'start': '2024-01-26T06:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-27T23:00:00-08:00', - 'start': '2024-01-27T10:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-31T13:00:00-08:00', - 'start': '2024-01-31T08:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-02-02T09:00:00-08:00', - 'start': '2024-02-02T06:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-02-03T23:00:00-08:00', - 'start': '2024-02-03T10:00:00-08:00', + 'end': '2024-02-04T07:30:00-08:00', + 'start': '2024-02-04T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), ]), }), }) # --- +# name: test_calendar_events[events.GS01234_auto_on_off_schedule_os2oswx] + dict({ + 'calendar.gs01234_auto_on_off_schedule_os2oswx': dict({ + 'events': list([ + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-13T00:00:00-08:00', + 'start': '2024-01-12T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-14T00:00:00-08:00', + 'start': '2024-01-13T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-15T00:00:00-08:00', + 'start': '2024-01-14T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-16T00:00:00-08:00', + 'start': '2024-01-15T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-17T00:00:00-08:00', + 'start': '2024-01-16T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-18T00:00:00-08:00', + 'start': '2024-01-17T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-19T00:00:00-08:00', + 'start': '2024-01-18T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-20T00:00:00-08:00', + 'start': '2024-01-19T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-21T00:00:00-08:00', + 'start': '2024-01-20T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-22T00:00:00-08:00', + 'start': '2024-01-21T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-23T00:00:00-08:00', + 'start': '2024-01-22T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-24T00:00:00-08:00', + 'start': '2024-01-23T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-25T00:00:00-08:00', + 'start': '2024-01-24T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-26T00:00:00-08:00', + 'start': '2024-01-25T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-27T00:00:00-08:00', + 'start': '2024-01-26T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-28T00:00:00-08:00', + 'start': '2024-01-27T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-29T00:00:00-08:00', + 'start': '2024-01-28T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-30T00:00:00-08:00', + 'start': '2024-01-29T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-31T00:00:00-08:00', + 'start': '2024-01-30T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-01T00:00:00-08:00', + 'start': '2024-01-31T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-02T00:00:00-08:00', + 'start': '2024-02-01T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-03T00:00:00-08:00', + 'start': '2024-02-02T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-04T00:00:00-08:00', + 'start': '2024-02-03T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + ]), + }), + }) +# --- +# name: test_calendar_events[state.GS01234_auto_on_off_schedule_axfz5bj] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': False, + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end_time': '2024-01-14 07:30:00', + 'friendly_name': 'GS01234 Auto on/off schedule (aXFz5bJ)', + 'location': '', + 'message': 'Machine My LaMarzocco on', + 'start_time': '2024-01-14 07:00:00', + }), + 'context': , + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_axfz5bj', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_calendar_events[state.GS01234_auto_on_off_schedule_os2oswx] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': False, + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end_time': '2024-01-13 00:00:00', + 'friendly_name': 'GS01234 Auto on/off schedule (Os2OswX)', + 'location': '', + 'message': 'Machine My LaMarzocco on', + 'start_time': '2024-01-12 22:00:00', + }), + 'context': , + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_os2oswx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_no_calendar_events_global_disable dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_os2oswx': dict({ 'events': list([ ]), }), diff --git a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr index ec44100fe1e..29512f0b7b0 100644 --- a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr +++ b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr @@ -2,297 +2,107 @@ # name: test_diagnostics dict({ 'config': dict({ - 'boilerTargetTemperature': dict({ - 'CoffeeBoiler1': 95, - 'SteamBoiler': 123.9000015258789, - }), - 'boilers': list([ - dict({ - 'current': 123.80000305175781, - 'id': 'SteamBoiler', - 'isEnabled': True, - 'target': 123.9000015258789, + 'boilers': dict({ + 'CoffeeBoiler1': dict({ + 'current_temperature': 96.5, + 'enabled': True, + 'target_temperature': 95, }), - dict({ - 'current': 96.5, - 'id': 'CoffeeBoiler1', - 'isEnabled': True, - 'target': 95, - }), - ]), - 'clock': '1901-07-08T10:29:00', - 'firmwareVersions': list([ - dict({ - 'fw_version': '1.40', - 'name': 'machine_firmware', - }), - dict({ - 'fw_version': 'v3.1-rc4', - 'name': 'gateway_firmware', - }), - ]), - 'groupCapabilities': list([ - dict({ - 'capabilities': dict({ - 'boilerId': 'CoffeeBoiler1', - 'groupNumber': 'Group1', - 'groupType': 'AV_Group', - 'hasFlowmeter': True, - 'hasScale': False, - 'numberOfDoses': 4, - }), - 'doseMode': dict({ - 'brewingType': 'PulsesType', - 'groupNumber': 'Group1', - }), - 'doses': list([ - dict({ - 'doseIndex': 'DoseA', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 135, - }), - dict({ - 'doseIndex': 'DoseB', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 97, - }), - dict({ - 'doseIndex': 'DoseC', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 108, - }), - dict({ - 'doseIndex': 'DoseD', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 121, - }), - ]), - }), - ]), - 'isBackFlushEnabled': False, - 'isPlumbedIn': True, - 'machineCapabilities': list([ - dict({ - 'coffeeBoilersNumber': 1, - 'family': 'GS3AV', - 'groupsNumber': 1, - 'hasCupWarmer': False, - 'machineModes': list([ - 'BrewingMode', - 'StandBy', - ]), - 'schedulingType': 'weeklyScheduling', - 'steamBoilersNumber': 1, - 'teaDosesNumber': 1, - }), - ]), - 'machineMode': 'BrewingMode', - 'machine_hw': '2', - 'machine_sn': '**REDACTED**', - 'preinfusionMode': dict({ - 'Group1': dict({ - 'groupNumber': 'Group1', - 'preinfusionStyle': 'PreinfusionByDoseType', + 'SteamBoiler': dict({ + 'current_temperature': 123.80000305175781, + 'enabled': True, + 'target_temperature': 123.9000015258789, }), }), - 'preinfusionModesAvailable': list([ - 'ByDoseType', - ]), - 'preinfusionSettings': dict({ - 'Group1': list([ - dict({ - 'doseType': 'DoseA', - 'groupNumber': 'Group1', - 'preWetHoldTime': 1, - 'preWetTime': 0.5, - }), - dict({ - 'doseType': 'DoseB', - 'groupNumber': 'Group1', - 'preWetHoldTime': 1, - 'preWetTime': 0.5, - }), - dict({ - 'doseType': 'DoseC', - 'groupNumber': 'Group1', - 'preWetHoldTime': 3.299999952316284, - 'preWetTime': 3.299999952316284, - }), - dict({ - 'doseType': 'DoseD', - 'groupNumber': 'Group1', - 'preWetHoldTime': 2, - 'preWetTime': 2, - }), - ]), - 'mode': 'TypeB', - }), - 'standByTime': 0, - 'tankStatus': True, - 'teaDoses': dict({ - 'DoseA': dict({ - 'doseIndex': 'DoseA', - 'stopTarget': 8, - }), - }), - 'version': 'v1', - 'weeklySchedulingConfig': dict({ - 'enabled': True, - 'friday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'monday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'saturday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'sunday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'thursday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'tuesday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'wednesday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - }), - }), - 'current_status': dict({ 'brew_active': False, - 'coffee_boiler_on': True, - 'coffee_set_temp': 95, - 'coffee_temp': 93, - 'dose_hot_water': 1023, - 'dose_k1': 1023, - 'dose_k2': 1023, - 'dose_k3': 1023, - 'dose_k4': 1023, - 'drinks_k1': 13, - 'drinks_k2': 2, - 'drinks_k3': 42, - 'drinks_k4': 34, - 'enable_prebrewing': True, - 'enable_preinfusion': False, - 'fri_auto': 'Disabled', - 'fri_off_time': '00:00', - 'fri_on_time': '00:00', - 'global_auto': 'Enabled', - 'mon_auto': 'Disabled', - 'mon_off_time': '00:00', - 'mon_on_time': '00:00', - 'power': True, - 'prebrewing_toff_k1': 5, - 'prebrewing_toff_k2': 5, - 'prebrewing_toff_k3': 5, - 'prebrewing_toff_k4': 5, - 'prebrewing_ton_k1': 3, - 'prebrewing_ton_k2': 3, - 'prebrewing_ton_k3': 3, - 'prebrewing_ton_k4': 3, - 'preinfusion_k1': 4, - 'preinfusion_k2': 4, - 'preinfusion_k3': 4, - 'preinfusion_k4': 4, - 'sat_auto': 'Disabled', - 'sat_off_time': '00:00', - 'sat_on_time': '00:00', - 'steam_boiler_enable': True, - 'steam_boiler_on': True, - 'steam_level_set': 3, - 'steam_set_temp': 128, - 'steam_temp': 113, - 'sun_auto': 'Disabled', - 'sun_off_time': '00:00', - 'sun_on_time': '00:00', - 'thu_auto': 'Disabled', - 'thu_off_time': '00:00', - 'thu_on_time': '00:00', - 'total_flushing': 69, - 'tue_auto': 'Disabled', - 'tue_off_time': '00:00', - 'tue_on_time': '00:00', - 'water_reservoir_contact': True, - 'wed_auto': 'Disabled', - 'wed_off_time': '00:00', - 'wed_on_time': '00:00', - }), - 'firmware': dict({ - 'gateway': dict({ - 'latest_version': 'v3.1-rc4', - 'version': 'v2.2-rc0', + 'brew_active_duration': 0, + 'dose_hot_water': 8, + 'doses': dict({ + '1': 135, + '2': 97, + '3': 108, + '4': 121, }), - 'machine': dict({ - 'latest_version': '1.2', - 'version': '1.1', + 'plumbed_in': True, + 'prebrew_configuration': dict({ + '1': dict({ + 'off_time': 1, + 'on_time': 0.5, + }), + '2': dict({ + 'off_time': 1, + 'on_time': 0.5, + }), + '3': dict({ + 'off_time': 3.299999952316284, + 'on_time': 3.299999952316284, + }), + '4': dict({ + 'off_time': 2, + 'on_time': 2, + }), }), + 'prebrew_mode': 'TypeB', + 'smart_standby': dict({ + 'enabled': True, + 'minutes': 10, + 'mode': 'LastBrewing', + }), + 'turned_on': True, + 'wake_up_sleep_entries': dict({ + 'Os2OswX': dict({ + 'days': list([ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ]), + 'enabled': True, + 'entry_id': 'Os2OswX', + 'steam': True, + 'time_off': '24:0', + 'time_on': '22:0', + }), + 'aXFz5bJ': dict({ + 'days': list([ + 'sunday', + ]), + 'enabled': True, + 'entry_id': 'aXFz5bJ', + 'steam': True, + 'time_off': '7:30', + 'time_on': '7:0', + }), + }), + 'water_contact': True, }), - 'machine_info': dict({ - 'machine_name': 'GS01234', - 'serial_number': '**REDACTED**', - }), + 'firmware': list([ + dict({ + 'machine': dict({ + 'current_version': '1.40', + 'latest_version': '1.55', + }), + }), + dict({ + 'gateway': dict({ + 'current_version': 'v3.1-rc4', + 'latest_version': 'v3.5-rc3', + }), + }), + ]), + 'model': 'GS3 AV', 'statistics': dict({ - 'stats': list([ - dict({ - 'coffeeType': 0, - 'count': 1047, - }), - dict({ - 'coffeeType': 1, - 'count': 560, - }), - dict({ - 'coffeeType': 2, - 'count': 468, - }), - dict({ - 'coffeeType': 3, - 'count': 312, - }), - dict({ - 'coffeeType': 4, - 'count': 2252, - }), - dict({ - 'coffeeType': -1, - 'count': 1740, - }), - ]), + 'continous': 2252, + 'drink_stats': dict({ + '1': 1047, + '2': 560, + '3': 468, + '4': 312, + }), + 'total_flushes': 1740, }), }) # --- diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index da35bf718f6..8265e7d7646 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -56,7 +56,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 AV] +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 AV] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -72,10 +72,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '128', + 'state': '123.900001525879', }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 AV].1 +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 AV].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -113,7 +113,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 MP] +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 MP] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -129,10 +129,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '128', + 'state': '123.900001525879', }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 MP].1 +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 MP].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -170,7 +170,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 AV] +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 AV] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -186,10 +186,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '8', }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 AV].1 +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 AV].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -227,7 +227,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 MP] +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 MP] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -243,10 +243,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '8', }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 MP].1 +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 MP].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -284,7 +284,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_1-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 1', @@ -299,10 +299,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '135', }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_2-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 2', @@ -317,10 +317,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '97', }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_3-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 3', @@ -335,10 +335,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '108', }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_4-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 4', @@ -353,10 +353,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '121', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_1-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -372,10 +372,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_2-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -391,10 +391,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_3-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -410,10 +410,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '3.29999995231628', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_4-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -429,10 +429,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '2', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_1-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -448,10 +448,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_2-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -467,10 +467,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_3-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -486,10 +486,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '3.29999995231628', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_4-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -505,10 +505,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '2', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_1-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -524,10 +524,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_2-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -543,10 +543,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_3-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -562,10 +562,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '3.29999995231628', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_4-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -581,10 +581,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '2', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Linea Mini] +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Linea Mini] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -600,10 +600,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Linea Mini].1 +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Linea Mini].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -641,7 +641,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Micra] +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Micra] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -657,10 +657,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Micra].1 +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Micra].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -698,7 +698,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Linea Mini] +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Linea Mini] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -714,10 +714,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Linea Mini].1 +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Linea Mini].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -755,7 +755,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Micra] +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Micra] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -771,10 +771,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Micra].1 +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Micra].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -812,7 +812,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Linea Mini] +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Linea Mini] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -828,10 +828,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Linea Mini].1 +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Linea Mini].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -869,7 +869,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Micra] +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Micra] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -885,10 +885,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Micra].1 +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Micra].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr index 1ee5ae7115f..be56af2b092 100644 --- a/tests/components/lamarzocco/snapshots/test_select.ambr +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -14,7 +14,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'preinfusion', }) # --- # name: test_pre_brew_infusion_select[GS3 AV].1 @@ -34,7 +34,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.gs01234_prebrew_infusion_mode', 'has_entity_name': True, 'hidden_by': None, @@ -71,7 +71,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'preinfusion', }) # --- # name: test_pre_brew_infusion_select[Linea Mini].1 @@ -91,7 +91,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.lm01234_prebrew_infusion_mode', 'has_entity_name': True, 'hidden_by': None, @@ -128,7 +128,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'preinfusion', }) # --- # name: test_pre_brew_infusion_select[Micra].1 @@ -148,7 +148,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.mr01234_prebrew_infusion_mode', 'has_entity_name': True, 'hidden_by': None, @@ -185,7 +185,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- # name: test_steam_boiler_level[Micra].1 diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index 71422b8b850..2237a8416e1 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -50,7 +50,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '93', + 'state': '96.5', }) # --- # name: test_sensors[GS01234_current_steam_temperature-entry] @@ -104,7 +104,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '113', + 'state': '123.800003051758', }) # --- # name: test_sensors[GS01234_shot_timer-entry] @@ -205,7 +205,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '13', + 'state': '1047', }) # --- # name: test_sensors[GS01234_total_flushes_made-entry] @@ -255,6 +255,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '69', + 'state': '1740', }) # --- diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 59053c5c478..00205f48c21 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -20,16 +20,16 @@ 'labels': set({ }), 'manufacturer': 'La Marzocco', - 'model': 'GS3 AV', + 'model': , 'name': 'GS01234', 'name_by_user': None, 'serial_number': 'GS01234', 'suggested_area': None, - 'sw_version': '1.1', + 'sw_version': '1.40', 'via_device_id': None, }) # --- -# name: test_switches[-set_power-args_on0-args_off0] +# name: test_switches[-set_power] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234', @@ -42,7 +42,7 @@ 'state': 'on', }) # --- -# name: test_switches[-set_power-args_on0-args_off0].1 +# name: test_switches[-set_power].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -75,141 +75,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[-set_power-kwargs_on0-kwargs_off0] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234', - 'icon': 'mdi:power', - }), - 'context': , - 'entity_id': 'switch.gs01234', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[-set_power-kwargs_on0-kwargs_off0].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.gs01234', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:power', - 'original_name': None, - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'GS01234_main', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-args_on1-args_off1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234 Auto on/off', - }), - 'context': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-args_on1-args_off1].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Auto on/off', - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'auto_on_off', - 'unique_id': 'GS01234_auto_on_off', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-kwargs_on1-kwargs_off1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234 Auto on/off', - 'icon': 'mdi:alarm', - }), - 'context': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-kwargs_on1-kwargs_off1].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alarm', - 'original_name': 'Auto on/off', - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'auto_on_off', - 'unique_id': 'GS01234_auto_on_off', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2] +# name: test_switches[_steam_boiler-set_steam] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Steam boiler', @@ -222,53 +88,7 @@ 'state': 'on', }) # --- -# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.gs01234_steam_boiler', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Steam boiler', - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'steam_boiler', - 'unique_id': 'GS01234_steam_boiler_enable', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234 Steam boiler', - 'icon': 'mdi:water-boiler', - }), - 'context': , - 'entity_id': 'switch.gs01234_steam_boiler', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2].1 +# name: test_switches[_steam_boiler-set_steam].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), diff --git a/tests/components/lamarzocco/snapshots/test_update.ambr b/tests/components/lamarzocco/snapshots/test_update.ambr index 811b1a6f598..4ab8e35ffd0 100644 --- a/tests/components/lamarzocco/snapshots/test_update.ambr +++ b/tests/components/lamarzocco/snapshots/test_update.ambr @@ -7,8 +7,8 @@ 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', 'friendly_name': 'GS01234 Gateway firmware', 'in_progress': False, - 'installed_version': 'v2.2-rc0', - 'latest_version': 'v3.1-rc4', + 'installed_version': 'v3.1-rc4', + 'latest_version': 'v3.5-rc3', 'release_summary': None, 'release_url': None, 'skipped_version': None, @@ -64,8 +64,8 @@ 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', 'friendly_name': 'GS01234 Machine firmware', 'in_progress': False, - 'installed_version': '1.1', - 'latest_version': '1.2', + 'installed_version': '1.40', + 'latest_version': '1.55', 'release_summary': None, 'release_url': None, 'skipped_version': None, diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index bb1e16f09a5..36acde91a68 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -1,7 +1,10 @@ """Tests for La Marzocco binary sensors.""" +from datetime import timedelta from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -11,7 +14,7 @@ from homeassistant.helpers import entity_registry as er from . import async_init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed BINARY_SENSORS = ( "brewing_active", @@ -70,3 +73,29 @@ async def test_brew_active_unavailable( ) assert state assert state.state == STATE_UNAVAILABLE + + +async def test_sensor_going_unavailable( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor is going unavailable after an unsuccessful update.""" + brewing_active_sensor = ( + f"binary_sensor.{mock_lamarzocco.serial_number}_brewing_active" + ) + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get(brewing_active_sensor) + assert state + assert state.state != STATE_UNAVAILABLE + + mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("") + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(brewing_active_sensor) + assert state + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/lamarzocco/test_calendar.py b/tests/components/lamarzocco/test_calendar.py index d26faa615e6..dd590a20db1 100644 --- a/tests/components/lamarzocco/test_calendar.py +++ b/tests/components/lamarzocco/test_calendar.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from . import async_init_integration +from . import WAKE_UP_SLEEP_ENTRY_IDS, async_init_integration from tests.common import MockConfigEntry @@ -40,27 +40,37 @@ async def test_calendar_events( serial_number = mock_lamarzocco.serial_number - state = hass.states.get(f"calendar.{serial_number}_auto_on_off_schedule") - assert state - assert state == snapshot + for identifier in WAKE_UP_SLEEP_ENTRY_IDS: + identifier = identifier.lower() + state = hass.states.get( + f"calendar.{serial_number}_auto_on_off_schedule_{identifier}" + ) + assert state + assert state == snapshot( + name=f"state.{serial_number}_auto_on_off_schedule_{identifier}" + ) - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry == snapshot + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot( + name=f"entry.{serial_number}_auto_on_off_schedule_{identifier}" + ) - events = await hass.services.async_call( - CALENDAR_DOMAIN, - SERVICE_GET_EVENTS, - { - ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule", - EVENT_START_DATETIME: test_time, - EVENT_END_DATETIME: test_time + timedelta(days=23), - }, - blocking=True, - return_response=True, - ) + events = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule_{identifier}", + EVENT_START_DATETIME: test_time, + EVENT_END_DATETIME: test_time + timedelta(days=23), + }, + blocking=True, + return_response=True, + ) - assert events == snapshot + assert events == snapshot( + name=f"events.{serial_number}_auto_on_off_schedule_{identifier}" + ) @pytest.mark.parametrize( @@ -89,21 +99,13 @@ async def test_calendar_edge_cases( start_date = start_date.replace(tzinfo=dt_util.get_default_time_zone()) end_date = end_date.replace(tzinfo=dt_util.get_default_time_zone()) - # set schedule to be only on Sunday, 07:00 - 07:30 - mock_lamarzocco.schedule[2]["enable"] = "Disabled" - mock_lamarzocco.schedule[4]["enable"] = "Disabled" - mock_lamarzocco.schedule[5]["enable"] = "Disabled" - mock_lamarzocco.schedule[6]["enable"] = "Enabled" - mock_lamarzocco.schedule[6]["on"] = "07:00" - mock_lamarzocco.schedule[6]["off"] = "07:30" - await async_init_integration(hass, mock_config_entry) events = await hass.services.async_call( CALENDAR_DOMAIN, SERVICE_GET_EVENTS, { - ATTR_ENTITY_ID: f"calendar.{mock_lamarzocco.serial_number}_auto_on_off_schedule", + ATTR_ENTITY_ID: f"calendar.{mock_lamarzocco.serial_number}_auto_on_off_schedule_{WAKE_UP_SLEEP_ENTRY_IDS[1].lower()}", EVENT_START_DATETIME: start_date, EVENT_END_DATETIME: end_date, }, @@ -123,7 +125,9 @@ async def test_no_calendar_events_global_disable( ) -> None: """Assert no events when global auto on/off is disabled.""" - mock_lamarzocco.current_status["global_auto"] = "Disabled" + wake_up_sleep_entry_id = WAKE_UP_SLEEP_ENTRY_IDS[0] + + mock_lamarzocco.config.wake_up_sleep_entries[wake_up_sleep_entry_id].enabled = False test_time = datetime(2024, 1, 12, 11, tzinfo=dt_util.get_default_time_zone()) freezer.move_to(test_time) @@ -131,14 +135,16 @@ async def test_no_calendar_events_global_disable( serial_number = mock_lamarzocco.serial_number - state = hass.states.get(f"calendar.{serial_number}_auto_on_off_schedule") + state = hass.states.get( + f"calendar.{serial_number}_auto_on_off_schedule_{wake_up_sleep_entry_id.lower()}" + ) assert state events = await hass.services.async_call( CALENDAR_DOMAIN, SERVICE_GET_EVENTS, { - ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule", + ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule_{wake_up_sleep_entry_id.lower()}", EVENT_START_DATETIME: test_time, EVENT_END_DATETIME: test_time + timedelta(days=23), }, diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index 14f794000d8..92ecd0a13f4 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -1,17 +1,26 @@ """Test the La Marzocco config flow.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.models import LaMarzoccoDeviceInfo -from homeassistant import config_entries -from homeassistant.components.lamarzocco.const import ( - CONF_MACHINE, - CONF_USE_BLUETOOTH, - DOMAIN, +from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE +from homeassistant.components.lamarzocco.const import CONF_USE_BLUETOOTH, DOMAIN +from homeassistant.config_entries import ( + SOURCE_BLUETOOTH, + SOURCE_REAUTH, + SOURCE_USER, + ConfigEntryState, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_MODEL, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType @@ -21,7 +30,7 @@ from tests.common import MockConfigEntry async def __do_successful_user_step( - hass: HomeAssistant, result: FlowResult + hass: HomeAssistant, result: FlowResult, mock_cloud_client: MagicMock ) -> FlowResult: """Successfully configure the user step.""" result2 = await hass.config_entries.flow.async_configure( @@ -36,51 +45,63 @@ async def __do_successful_user_step( async def __do_sucessful_machine_selection_step( - hass: HomeAssistant, result2: FlowResult, mock_lamarzocco: MagicMock + hass: HomeAssistant, result2: FlowResult, mock_device_info: LaMarzoccoDeviceInfo ) -> None: """Successfully configure the machine selection step.""" - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, - }, - ) + + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + CONF_MACHINE: mock_device_info.serial_number, + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == mock_lamarzocco.serial_number + assert result3["title"] == "GS3" assert result3["data"] == { **USER_INPUT, CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MODEL: mock_device_info.model, + CONF_NAME: mock_device_info.name, + CONF_TOKEN: mock_device_info.communication_key, } -async def test_form(hass: HomeAssistant, mock_lamarzocco: MagicMock) -> None: +async def test_form( + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" - result2 = await __do_successful_user_step(hass, result) - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) - - assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1 + result2 = await __do_successful_user_step(hass, result, mock_cloud_client) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_form_abort_already_configured( hass: HomeAssistant, - mock_lamarzocco: MagicMock, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, mock_config_entry: MockConfigEntry, ) -> None: """Test we abort if already configured.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -98,7 +119,7 @@ async def test_form_abort_already_configured( result2["flow_id"], { CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MACHINE: mock_device_info.serial_number, }, ) await hass.async_block_till_done() @@ -108,13 +129,15 @@ async def test_form_abort_already_configured( async def test_form_invalid_auth( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_device_info: LaMarzoccoDeviceInfo, + mock_cloud_client: MagicMock, ) -> None: """Test invalid auth error.""" - mock_lamarzocco.get_all_machines.side_effect = AuthFail("") + mock_cloud_client.get_customer_fleet.side_effect = AuthFail("") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure( @@ -124,20 +147,22 @@ async def test_form_invalid_auth( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 # test recovery from failure - mock_lamarzocco.get_all_machines.side_effect = None - result2 = await __do_successful_user_step(hass, result) - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) + mock_cloud_client.get_customer_fleet.side_effect = None + result2 = await __do_successful_user_step(hass, result, mock_cloud_client) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_form_invalid_host( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, ) -> None: """Test invalid auth error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -148,38 +173,41 @@ async def test_form_invalid_host( ) await hass.async_block_till_done() - mock_lamarzocco.check_local_connection.return_value = False - assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, - }, - ) + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=False, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + CONF_MACHINE: mock_device_info.serial_number, + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"host": "cannot_connect"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 # test recovery from failure - mock_lamarzocco.check_local_connection.return_value = True - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_form_cannot_connect( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, ) -> None: """Test cannot connect error.""" - mock_lamarzocco.get_all_machines.return_value = [] + mock_cloud_client.get_customer_fleet.return_value = {} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure( @@ -189,9 +217,9 @@ async def test_form_cannot_connect( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_machines"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 - mock_lamarzocco.get_all_machines.side_effect = RequestNotSuccessful("") + mock_cloud_client.get_customer_fleet.side_effect = RequestNotSuccessful("") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -199,21 +227,26 @@ async def test_form_cannot_connect( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 2 # test recovery from failure - mock_lamarzocco.get_all_machines.side_effect = None - mock_lamarzocco.get_all_machines.return_value = [ - (mock_lamarzocco.serial_number, mock_lamarzocco.model_name) - ] - result2 = await __do_successful_user_step(hass, result) - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) + mock_cloud_client.get_customer_fleet.side_effect = None + mock_cloud_client.get_customer_fleet.return_value = { + mock_device_info.serial_number: mock_device_info + } + result2 = await __do_successful_user_step(hass, result, mock_cloud_client) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_reauth_flow( - hass: HomeAssistant, mock_lamarzocco: MagicMock, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test that the reauth flow.""" + + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={ @@ -235,19 +268,21 @@ async def test_reauth_flow( assert result2["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert result2["reason"] == "reauth_successful" - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 assert mock_config_entry.data[CONF_PASSWORD] == "new_password" async def test_bluetooth_discovery( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_cloud_client: MagicMock, ) -> None: """Test bluetooth discovery.""" service_info = get_bluetooth_service_info( - mock_lamarzocco.model_name, mock_lamarzocco.serial_number + mock_lamarzocco.model, mock_lamarzocco.serial_number ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=service_info + DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info ) assert result["type"] is FlowResultType.FORM @@ -260,82 +295,95 @@ async def test_bluetooth_discovery( assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - }, - ) + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == mock_lamarzocco.serial_number + assert result3["title"] == "GS3" assert result3["data"] == { **USER_INPUT, CONF_HOST: "192.168.1.1", CONF_MACHINE: mock_lamarzocco.serial_number, - CONF_NAME: service_info.name, + CONF_NAME: "GS3", CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_MODEL: mock_lamarzocco.model, + CONF_TOKEN: "token", } - assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1 - async def test_bluetooth_discovery_errors( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, ) -> None: """Test bluetooth discovery errors.""" service_info = get_bluetooth_service_info( - mock_lamarzocco.model_name, mock_lamarzocco.serial_number + mock_lamarzocco.model, mock_lamarzocco.serial_number ) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_BLUETOOTH}, + context={"source": SOURCE_BLUETOOTH}, data=service_info, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - mock_lamarzocco.get_all_machines.return_value = [("GS98765", "GS3 MP")] + mock_cloud_client.get_customer_fleet.return_value = {"GS98765", ""} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "machine_not_found"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 - mock_lamarzocco.get_all_machines.return_value = [ - (mock_lamarzocco.serial_number, mock_lamarzocco.model_name) - ] + mock_cloud_client.get_customer_fleet.return_value = { + mock_device_info.serial_number: mock_device_info + } result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 2 - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - }, - ) + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == mock_lamarzocco.serial_number + assert result3["title"] == "GS3" assert result3["data"] == { **USER_INPUT, CONF_HOST: "192.168.1.1", CONF_MACHINE: mock_lamarzocco.serial_number, - CONF_NAME: service_info.name, + CONF_NAME: "GS3", CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_MODEL: mock_lamarzocco.model, + CONF_TOKEN: "token", } diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index a4bc25f64af..2c812f79438 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -1,15 +1,19 @@ """Test initialization of lamarzocco.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch +from lmcloud.const import FirmwareType from lmcloud.exceptions import AuthFail, RequestNotSuccessful +import pytest +from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_MAC, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir -from . import async_init_integration, get_bluetooth_service_info +from . import USER_INPUT, async_init_integration, get_bluetooth_service_info from tests.common import MockConfigEntry @@ -20,7 +24,9 @@ async def test_load_unload_config_entry( mock_lamarzocco: MagicMock, ) -> None: """Test loading and unloading the integration.""" - await async_init_integration(hass, mock_config_entry) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED @@ -36,11 +42,13 @@ async def test_config_entry_not_ready( mock_lamarzocco: MagicMock, ) -> None: """Test the La Marzocco configuration entry not ready.""" - mock_lamarzocco.update_local_machine_status.side_effect = RequestNotSuccessful("") + mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("") - await async_init_integration(hass, mock_config_entry) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 + assert len(mock_lamarzocco.get_config.mock_calls) == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY @@ -50,11 +58,13 @@ async def test_invalid_auth( mock_lamarzocco: MagicMock, ) -> None: """Test auth error during setup.""" - mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("") - await async_init_integration(hass, mock_config_entry) + mock_lamarzocco.get_config.side_effect = AuthFail("") + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR - assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 + assert len(mock_lamarzocco.get_config.mock_calls) == 1 flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -68,27 +78,132 @@ async def test_invalid_auth( assert flow["context"].get("entry_id") == mock_config_entry.entry_id +async def test_v1_migration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_cloud_client: MagicMock, + mock_lamarzocco: MagicMock, +) -> None: + """Test v1 -> v2 Migration.""" + entry_v1 = MockConfigEntry( + domain=DOMAIN, + version=1, + unique_id=mock_lamarzocco.serial_number, + data={ + **USER_INPUT, + CONF_HOST: "host", + CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ) + + entry_v1.add_to_hass(hass) + await hass.config_entries.async_setup(entry_v1.entry_id) + await hass.async_block_till_done() + + assert entry_v1.version == 2 + assert dict(entry_v1.data) == dict(mock_config_entry.data) | { + CONF_MAC: "aa:bb:cc:dd:ee:ff" + } + + +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.get_customer_fleet.side_effect = RequestNotSuccessful("Error") + + entry_v1 = MockConfigEntry( + domain=DOMAIN, + version=1, + unique_id=mock_lamarzocco.serial_number, + data={ + **USER_INPUT, + CONF_MACHINE: mock_lamarzocco.serial_number, + }, + ) + entry_v1.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry_v1.entry_id) + assert entry_v1.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=3) + entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry.entry_id) + + async def test_bluetooth_is_set_from_discovery( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock, ) -> None: - """Assert we're not searching for a new BT device when we already found one previously.""" - - # remove the bluetooth configuration from entry - data = mock_config_entry.data.copy() - del data[CONF_NAME] - del data[CONF_MAC] - hass.config_entries.async_update_entry(mock_config_entry, data=data) + """Check we can fill a device from discovery info.""" service_info = get_bluetooth_service_info( - mock_lamarzocco.model_name, mock_lamarzocco.serial_number + mock_lamarzocco.model, mock_lamarzocco.serial_number ) - with patch( - "homeassistant.components.lamarzocco.coordinator.async_discovered_service_info", - return_value=[service_info], + with ( + patch( + "homeassistant.components.lamarzocco.async_discovered_service_info", + return_value=[service_info], + ) as discovery, + patch( + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine" + ) as init_device, ): await async_init_integration(hass, mock_config_entry) - mock_lamarzocco.init_bluetooth_with_known_device.assert_called_once() + discovery.assert_called_once() + init_device.assert_called_once() + _, kwargs = init_device.call_args + assert kwargs["bluetooth_client"] is not None assert mock_config_entry.data[CONF_NAME] == service_info.name assert mock_config_entry.data[CONF_MAC] == service_info.address + + +async def test_websocket_closed_on_unload( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_lamarzocco: MagicMock, +) -> None: + """Test the websocket is closed on unload.""" + with patch( + "homeassistant.components.lamarzocco.LaMarzoccoLocalClient", + autospec=True, + ) as local_client: + client = local_client.return_value + client.websocket = AsyncMock() + client.websocket.connected = True + await async_init_integration(hass, mock_config_entry) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + client.websocket.close.assert_called_once() + + +@pytest.mark.parametrize( + ("version", "issue_exists"), [("v3.5-rc6", False), ("v3.3-rc4", True)] +) +async def test_gateway_version_issue( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_lamarzocco: MagicMock, + version: str, + issue_exists: bool, +) -> None: + """Make sure we get the issue for certain gateway firmware versions.""" + mock_lamarzocco.firmware[FirmwareType.GATEWAY].current_version = version + + await async_init_integration(hass, mock_config_entry) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, "unsupported_gateway_firmware") + assert (issue is not None) == issue_exists diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 8cba3d2387d..288c78c26dd 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -2,7 +2,13 @@ from unittest.mock import MagicMock -from lmcloud.const import KEYS_PER_MODEL, LaMarzoccoModel +from lmcloud.const import ( + KEYS_PER_MODEL, + BoilerType, + MachineModel, + PhysicalKey, + PrebrewMode, +) import pytest from syrupy import SnapshotAssertion @@ -15,17 +21,22 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -pytestmark = pytest.mark.usefixtures("init_integration") +from . import async_init_integration + +from tests.common import MockConfigEntry async def test_coffee_boiler( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: """Test the La Marzocco coffee temperature Number.""" + + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number state = hass.states.get(f"number.{serial_number}_coffee_target_temperature") @@ -47,35 +58,34 @@ async def test_coffee_boiler( SERVICE_SET_VALUE, { ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature", - ATTR_VALUE: 95, + ATTR_VALUE: 94, }, blocking=True, ) - assert len(mock_lamarzocco.set_coffee_temp.mock_calls) == 1 - mock_lamarzocco.set_coffee_temp.assert_called_once_with( - temperature=95, ble_device=None + assert len(mock_lamarzocco.set_temp.mock_calls) == 1 + mock_lamarzocco.set_temp.assert_called_once_with( + boiler=BoilerType.COFFEE, temperature=94 ) -@pytest.mark.parametrize( - "device_fixture", [LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP] -) +@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV, MachineModel.GS3_MP]) @pytest.mark.parametrize( ("entity_name", "value", "func_name", "kwargs"), [ ( "steam_target_temperature", 131, - "set_steam_temp", - {"temperature": 131, "ble_device": None}, + "set_temp", + {"boiler": BoilerType.STEAM, "temperature": 131}, ), - ("tea_water_duration", 15, "set_dose_hot_water", {"value": 15}), + ("tea_water_duration", 15, "set_dose_tea_water", {"dose": 15}), ], ) async def test_gs3_exclusive( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, @@ -85,7 +95,7 @@ async def test_gs3_exclusive( kwargs: dict[str, float], ) -> None: """Test exclusive entities for GS3 AV/MP.""" - + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number func = getattr(mock_lamarzocco, func_name) @@ -118,14 +128,15 @@ async def test_gs3_exclusive( @pytest.mark.parametrize( - "device_fixture", [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI] + "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI] ) async def test_gs3_exclusive_none( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Ensure GS3 exclusive is None for unsupported models.""" - + await async_init_integration(hass, mock_config_entry) ENTITIES = ("steam_target_temperature", "tea_water_duration") serial_number = mock_lamarzocco.serial_number @@ -135,29 +146,50 @@ async def test_gs3_exclusive_none( @pytest.mark.parametrize( - "device_fixture", [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI] + "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI] ) @pytest.mark.parametrize( - ("entity_name", "value", "kwargs"), + ("entity_name", "function_name", "prebrew_mode", "value", "kwargs"), [ - ("prebrew_off_time", 6, {"on_time": 3000, "off_time": 6000, "key": 1}), - ("prebrew_on_time", 6, {"on_time": 6000, "off_time": 5000, "key": 1}), - ("preinfusion_time", 7, {"off_time": 7000, "key": 1}), + ( + "prebrew_off_time", + "set_prebrew_time", + PrebrewMode.PREBREW, + 6, + {"prebrew_off_time": 6.0, "key": PhysicalKey.A}, + ), + ( + "prebrew_on_time", + "set_prebrew_time", + PrebrewMode.PREBREW, + 6, + {"prebrew_on_time": 6.0, "key": PhysicalKey.A}, + ), + ( + "preinfusion_time", + "set_preinfusion_time", + PrebrewMode.PREINFUSION, + 7, + {"preinfusion_time": 7.0, "key": PhysicalKey.A}, + ), ], ) async def test_pre_brew_infusion_numbers( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, - device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, entity_name: str, + function_name: str, + prebrew_mode: PrebrewMode, value: float, kwargs: dict[str, float], ) -> None: """Test the La Marzocco prebrew/-infusion sensors.""" - mock_lamarzocco.current_status["enable_preinfusion"] = True + mock_lamarzocco.config.prebrew_mode = prebrew_mode + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number @@ -168,12 +200,8 @@ async def test_pre_brew_infusion_numbers( entry = entity_registry.async_get(state.entity_id) assert entry - assert entry.device_id assert entry == snapshot - device = device_registry.async_get(entry.device_id) - assert device - # service call await hass.services.async_call( NUMBER_DOMAIN, @@ -185,43 +213,97 @@ async def test_pre_brew_infusion_numbers( blocking=True, ) - assert len(mock_lamarzocco.configure_prebrew.mock_calls) == 1 - mock_lamarzocco.configure_prebrew.assert_called_once_with(**kwargs) + function = getattr(mock_lamarzocco, function_name) + function.assert_called_once_with(**kwargs) -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.GS3_AV]) +@pytest.mark.parametrize( + "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI] +) +@pytest.mark.parametrize( + ("prebrew_mode", "entity", "unavailable"), + [ + ( + PrebrewMode.PREBREW, + ("prebrew_off_time", "prebrew_on_time"), + ("preinfusion_time",), + ), + ( + PrebrewMode.PREINFUSION, + ("preinfusion_time",), + ("prebrew_off_time", "prebrew_on_time"), + ), + ], +) +async def test_pre_brew_infusion_numbers_unavailable( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + prebrew_mode: PrebrewMode, + entity: tuple[str, ...], + unavailable: tuple[str, ...], +) -> None: + """Test entities are unavailable depending on selected state.""" + + mock_lamarzocco.config.prebrew_mode = prebrew_mode + await async_init_integration(hass, mock_config_entry) + + serial_number = mock_lamarzocco.serial_number + for entity_name in entity: + state = hass.states.get(f"number.{serial_number}_{entity_name}") + assert state + assert state.state != STATE_UNAVAILABLE + + for entity_name in unavailable: + state = hass.states.get(f"number.{serial_number}_{entity_name}") + assert state + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV]) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("entity_name", "value", "function_name", "kwargs"), + ("entity_name", "value", "prebrew_mode", "function_name", "kwargs"), [ ( "prebrew_off_time", 6, - "configure_prebrew", - {"on_time": 3000, "off_time": 6000}, + PrebrewMode.PREBREW, + "set_prebrew_time", + {"prebrew_off_time": 6.0}, ), ( "prebrew_on_time", 6, - "configure_prebrew", - {"on_time": 6000, "off_time": 5000}, + PrebrewMode.PREBREW, + "set_prebrew_time", + {"prebrew_on_time": 6.0}, ), - ("preinfusion_time", 7, "configure_prebrew", {"off_time": 7000}), - ("dose", 6, "set_dose", {"value": 6}), + ( + "preinfusion_time", + 7, + PrebrewMode.PREINFUSION, + "set_preinfusion_time", + {"preinfusion_time": 7.0}, + ), + ("dose", 6, PrebrewMode.DISABLED, "set_dose", {"dose": 6}), ], ) async def test_pre_brew_infusion_key_numbers( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_name: str, value: float, + prebrew_mode: PrebrewMode, function_name: str, kwargs: dict[str, float], ) -> None: """Test the La Marzocco number sensors for GS3AV model.""" - mock_lamarzocco.current_status["enable_preinfusion"] = True + mock_lamarzocco.config.prebrew_mode = prebrew_mode + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number @@ -230,7 +312,7 @@ async def test_pre_brew_infusion_key_numbers( state = hass.states.get(f"number.{serial_number}_{entity_name}") assert state is None - for key in range(1, KEYS_PER_MODEL[mock_lamarzocco.model_name] + 1): + for key in PhysicalKey: state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}") assert state assert state == snapshot(name=f"{serial_number}_{entity_name}_key_{key}-state") @@ -248,17 +330,18 @@ async def test_pre_brew_infusion_key_numbers( kwargs["key"] = key - assert len(func.mock_calls) == key + assert len(func.mock_calls) == key.value func.assert_called_with(**kwargs) -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.GS3_AV]) +@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV]) async def test_disabled_entites( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test the La Marzocco prebrew/-infusion sensors for GS3AV model.""" - + await async_init_integration(hass, mock_config_entry) ENTITIES = ( "prebrew_off_time", "prebrew_on_time", @@ -269,21 +352,22 @@ async def test_disabled_entites( serial_number = mock_lamarzocco.serial_number for entity_name in ENTITIES: - for key in range(1, KEYS_PER_MODEL[mock_lamarzocco.model_name] + 1): + for key in PhysicalKey: state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}") assert state is None @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.GS3_MP, LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI], + [MachineModel.GS3_MP, MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI], ) -async def test_not_existing_key_entites( +async def test_not_existing_key_entities( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Assert not existing key entities.""" - + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number for entity in ( @@ -292,42 +376,6 @@ async def test_not_existing_key_entites( "preinfusion_time", "set_dose", ): - for key in range(1, KEYS_PER_MODEL[LaMarzoccoModel.GS3_AV] + 1): + for key in range(1, KEYS_PER_MODEL[MachineModel.GS3_AV] + 1): state = hass.states.get(f"number.{serial_number}_{entity}_key_{key}") assert state is None - - -@pytest.mark.parametrize( - "device_fixture", - [LaMarzoccoModel.GS3_MP], -) -async def test_not_existing_entites( - hass: HomeAssistant, - mock_lamarzocco: MagicMock, -) -> None: - """Assert not existing entities.""" - - serial_number = mock_lamarzocco.serial_number - - for entity in ( - "prebrew_off_time", - "prebrew_on_time", - "preinfusion_time", - "set_dose", - ): - state = hass.states.get(f"number.{serial_number}_{entity}") - assert state is None - - -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.LINEA_MICRA]) -async def test_not_settable_entites( - hass: HomeAssistant, - mock_lamarzocco: MagicMock, -) -> None: - """Assert not settable causes error.""" - - serial_number = mock_lamarzocco.serial_number - - state = hass.states.get(f"number.{serial_number}_preinfusion_time") - assert state - assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index 497a95f6d0d..e3521b473bd 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from lmcloud.const import LaMarzoccoModel +from lmcloud.const import MachineModel, PrebrewMode, SteamLevel import pytest from syrupy import SnapshotAssertion @@ -18,7 +18,7 @@ from homeassistant.helpers import entity_registry as er pytestmark = pytest.mark.usefixtures("init_integration") -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.LINEA_MICRA]) +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MICRA]) async def test_steam_boiler_level( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -44,18 +44,17 @@ async def test_steam_boiler_level( SERVICE_SELECT_OPTION, { ATTR_ENTITY_ID: f"select.{serial_number}_steam_level", - ATTR_OPTION: "1", + ATTR_OPTION: "2", }, blocking=True, ) - assert len(mock_lamarzocco.set_steam_level.mock_calls) == 1 - mock_lamarzocco.set_steam_level.assert_called_once_with(1, None) + mock_lamarzocco.set_steam_level.assert_called_once_with(level=SteamLevel.LEVEL_2) @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP, LaMarzoccoModel.LINEA_MINI], + [MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MINI], ) async def test_steam_boiler_level_none( hass: HomeAssistant, @@ -70,7 +69,7 @@ async def test_steam_boiler_level_none( @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.GS3_AV, LaMarzoccoModel.LINEA_MINI], + [MachineModel.LINEA_MICRA, MachineModel.GS3_AV, MachineModel.LINEA_MINI], ) async def test_pre_brew_infusion_select( hass: HomeAssistant, @@ -97,20 +96,17 @@ async def test_pre_brew_infusion_select( SERVICE_SELECT_OPTION, { ATTR_ENTITY_ID: f"select.{serial_number}_prebrew_infusion_mode", - ATTR_OPTION: "preinfusion", + ATTR_OPTION: "prebrew", }, blocking=True, ) - assert len(mock_lamarzocco.select_pre_brew_infusion_mode.mock_calls) == 1 - mock_lamarzocco.select_pre_brew_infusion_mode.assert_called_once_with( - mode="Preinfusion" - ) + mock_lamarzocco.set_prebrew_mode.assert_called_once_with(mode=PrebrewMode.PREBREW) @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.GS3_MP], + [MachineModel.GS3_MP], ) async def test_pre_brew_infusion_select_none( hass: HomeAssistant, diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index e1924f0a8ca..19950a0c21e 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock import pytest from syrupy import SnapshotAssertion -from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -15,35 +14,39 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import MockConfigEntry +from . import async_init_integration -pytestmark = pytest.mark.usefixtures("init_integration") +from tests.common import MockConfigEntry @pytest.mark.parametrize( - ("entity_name", "method_name", "args_on", "args_off"), + ( + "entity_name", + "method_name", + ), [ - ("", "set_power", (True, None), (False, None)), ( - "_auto_on_off", - "set_auto_on_off_global", - (True,), - (False,), + "", + "set_power", + ), + ( + "_steam_boiler", + "set_steam", ), - ("_steam_boiler", "set_steam", (True, None), (False, None)), ], ) async def test_switches( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, entity_name: str, method_name: str, - args_on: tuple, - args_off: tuple, ) -> None: """Test the La Marzocco switches.""" + await async_init_integration(hass, mock_config_entry) + serial_number = mock_lamarzocco.serial_number control_fn = getattr(mock_lamarzocco, method_name) @@ -66,7 +69,7 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 1 - control_fn.assert_called_once_with(*args_off) + control_fn.assert_called_once_with(False) await hass.services.async_call( SWITCH_DOMAIN, @@ -78,18 +81,21 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 2 - control_fn.assert_called_with(*args_on) + control_fn.assert_called_with(True) async def test_device( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test the device for one switch.""" + await async_init_integration(hass, mock_config_entry) + state = hass.states.get(f"switch.{mock_lamarzocco.serial_number}") assert state @@ -100,26 +106,3 @@ async def test_device( device = device_registry.async_get(entry.device_id) assert device assert device == snapshot - - -async def test_call_without_bluetooth_works( - hass: HomeAssistant, - mock_lamarzocco: MagicMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that if not using bluetooth, the switch still works.""" - serial_number = mock_lamarzocco.serial_number - coordinator = hass.data[DOMAIN][mock_config_entry.entry_id] - coordinator._use_bluetooth = False - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: f"switch.{serial_number}_steam_boiler", - }, - blocking=True, - ) - - assert len(mock_lamarzocco.set_steam.mock_calls) == 1 - mock_lamarzocco.set_steam.assert_called_once_with(False, None) diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index 3b1323d1c73..02330daf794 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from lmcloud.const import LaMarzoccoUpdateableComponent +from lmcloud.const import FirmwareType import pytest from syrupy import SnapshotAssertion @@ -18,8 +18,8 @@ pytestmark = pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize( ("entity_name", "component"), [ - ("machine_firmware", LaMarzoccoUpdateableComponent.MACHINE), - ("gateway_firmware", LaMarzoccoUpdateableComponent.GATEWAY), + ("machine_firmware", FirmwareType.MACHINE), + ("gateway_firmware", FirmwareType.GATEWAY), ], ) async def test_update_entites( @@ -28,7 +28,7 @@ async def test_update_entites( entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, entity_name: str, - component: LaMarzoccoUpdateableComponent, + component: FirmwareType, ) -> None: """Test the La Marzocco update entities."""