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 <joostlek@outlook.com>

* Update test_config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* 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 <joostlek@outlook.com>
This commit is contained in:
Josef Zweck 2024-06-10 19:59:39 +02:00 committed by GitHub
parent b7f74532dc
commit 42b984ee4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1579 additions and 1499 deletions

View File

@ -1,10 +1,31 @@
"""The La Marzocco integration.""" """The La Marzocco integration."""
from homeassistant.config_entries import ConfigEntry import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
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 from .coordinator import LaMarzoccoUpdateCoordinator
PLATFORMS = [ PLATFORMS = [
@ -18,15 +39,89 @@ PLATFORMS = [
Platform.UPDATE, Platform.UPDATE,
] ]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up La Marzocco as config entry.""" """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() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok 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

View File

@ -3,7 +3,7 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from lmcloud import LMCloud as LaMarzoccoClient from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
@ -26,7 +26,7 @@ class LaMarzoccoBinarySensorEntityDescription(
): ):
"""Description of a La Marzocco binary sensor.""" """Description of a La Marzocco binary sensor."""
is_on_fn: Callable[[LaMarzoccoClient], bool] is_on_fn: Callable[[LaMarzoccoMachineConfig], bool]
ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
@ -34,7 +34,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
key="water_tank", key="water_tank",
translation_key="water_tank", translation_key="water_tank",
device_class=BinarySensorDeviceClass.PROBLEM, 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, entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda coordinator: coordinator.local_connection_configured, supported_fn=lambda coordinator: coordinator.local_connection_configured,
), ),
@ -42,8 +42,8 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
key="brew_active", key="brew_active",
translation_key="brew_active", translation_key="brew_active",
device_class=BinarySensorDeviceClass.RUNNING, device_class=BinarySensorDeviceClass.RUNNING,
is_on_fn=lambda lm: bool(lm.current_status.get("brew_active")), is_on_fn=lambda config: config.brew_active,
available_fn=lambda lm: lm.websocket_connected, available_fn=lambda device: device.websocket_connected,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
) )
@ -72,4 +72,4 @@ class LaMarzoccoBinarySensorEntity(LaMarzoccoEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if the binary sensor is on.""" """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)

View File

@ -4,7 +4,7 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any 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.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -22,14 +22,14 @@ class LaMarzoccoButtonEntityDescription(
): ):
"""Description of a La Marzocco button.""" """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, ...] = ( ENTITIES: tuple[LaMarzoccoButtonEntityDescription, ...] = (
LaMarzoccoButtonEntityDescription( LaMarzoccoButtonEntityDescription(
key="start_backflush", key="start_backflush",
translation_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: async def async_press(self) -> None:
"""Press button.""" """Press button."""
await self.entity_description.press_fn(self.coordinator.lm) await self.entity_description.press_fn(self.coordinator.device)

View File

@ -3,6 +3,8 @@
from collections.abc import Iterator from collections.abc import Iterator
from datetime import datetime, timedelta from datetime import datetime, timedelta
from lmcloud.models import LaMarzoccoWakeUpSleepEntry
from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant 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 homeassistant.util import dt as dt_util
from .const import DOMAIN from .const import DOMAIN
from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoBaseEntity from .entity import LaMarzoccoBaseEntity
CALENDAR_KEY = "auto_on_off_schedule" CALENDAR_KEY = "auto_on_off_schedule"
DAY_OF_WEEK = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -23,7 +36,10 @@ async def async_setup_entry(
"""Set up switch entities and services.""" """Set up switch entities and services."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] 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): class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
@ -31,6 +47,17 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
_attr_translation_key = CALENDAR_KEY _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 @property
def event(self) -> CalendarEvent | None: def event(self) -> CalendarEvent | None:
"""Return the next upcoming event.""" """Return the next upcoming event."""
@ -85,29 +112,36 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
"""Return calendar event for a given weekday.""" """Return calendar event for a given weekday."""
# check first if auto/on off is turned on in general # check first if auto/on off is turned on in general
# because could still be on for that day but disabled if not self.wake_up_sleep_entry.enabled:
if self.coordinator.lm.current_status["global_auto"] != "Enabled":
return None return None
# parse the schedule for the day # 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 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( return CalendarEvent(
start=date.replace( start=date.replace(
hour=int(hour_on), hour=int(hour_on),
minute=int(minute_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", 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", description="Machine is scheduled to turn on at the start time and off at the end time",
) )

View File

@ -4,8 +4,10 @@ from collections.abc import Mapping
import logging import logging
from typing import Any 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.exceptions import AuthFail, RequestNotSuccessful
from lmcloud.models import LaMarzoccoDeviceInfo
import voluptuous as vol import voluptuous as vol
from homeassistant.components.bluetooth import BluetoothServiceInfo from homeassistant.components.bluetooth import BluetoothServiceInfo
@ -19,12 +21,15 @@ from homeassistant.config_entries import (
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_MAC, CONF_MAC,
CONF_MODEL,
CONF_NAME, CONF_NAME,
CONF_PASSWORD, CONF_PASSWORD,
CONF_TOKEN,
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
SelectOptionDict, SelectOptionDict,
SelectSelector, SelectSelector,
@ -32,7 +37,9 @@ from homeassistant.helpers.selector import (
SelectSelectorMode, SelectSelectorMode,
) )
from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN from .const import CONF_USE_BLUETOOTH, DOMAIN
CONF_MACHINE = "machine"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,12 +47,14 @@ _LOGGER = logging.getLogger(__name__)
class LmConfigFlow(ConfigFlow, domain=DOMAIN): class LmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for La Marzocco.""" """Handle a config flow for La Marzocco."""
VERSION = 2
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the config flow.""" """Initialize the config flow."""
self.reauth_entry: ConfigEntry | None = None self.reauth_entry: ConfigEntry | None = None
self._config: dict[str, Any] = {} self._config: dict[str, Any] = {}
self._machines: list[tuple[str, str]] = [] self._fleet: dict[str, LaMarzoccoDeviceInfo] = {}
self._discovered: dict[str, str] = {} self._discovered: dict[str, str] = {}
async def async_step_user( async def async_step_user(
@ -65,9 +74,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
**self._discovered, **self._discovered,
} }
lm = LaMarzoccoClient() cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
)
try: try:
self._machines = await lm.get_all_machines(data) self._fleet = await cloud_client.get_customer_fleet()
except AuthFail: except AuthFail:
_LOGGER.debug("Server rejected login credentials") _LOGGER.debug("Server rejected login credentials")
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
@ -75,7 +87,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("Error connecting to server: %s", exc) _LOGGER.error("Error connecting to server: %s", exc)
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: else:
if not self._machines: if not self._fleet:
errors["base"] = "no_machines" errors["base"] = "no_machines"
if not errors: if not errors:
@ -88,8 +100,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
) )
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
if self._discovered: if self._discovered:
serials = [machine[0] for machine in self._machines] if self._discovered[CONF_MACHINE] not in self._fleet:
if self._discovered[CONF_MACHINE] not in serials:
errors["base"] = "machine_not_found" errors["base"] = "machine_not_found"
else: else:
self._config = data self._config = data
@ -128,28 +139,36 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
else: else:
serial_number = self._discovered[CONF_MACHINE] serial_number = self._discovered[CONF_MACHINE]
selected_device = self._fleet[serial_number]
# validate local connection if host is provided # validate local connection if host is provided
if user_input.get(CONF_HOST): if user_input.get(CONF_HOST):
lm = LaMarzoccoClient() if not await LaMarzoccoLocalClient.validate_connection(
if not await lm.check_local_connection( client=get_async_client(self.hass),
credentials=self._config,
host=user_input[CONF_HOST], host=user_input[CONF_HOST],
serial=serial_number, token=selected_device.communication_key,
): ):
errors[CONF_HOST] = "cannot_connect" errors[CONF_HOST] = "cannot_connect"
else:
self._config[CONF_HOST] = user_input[CONF_HOST]
if not errors: if not errors:
return self.async_create_entry( return self.async_create_entry(
title=serial_number, title=selected_device.name,
data=self._config | user_input, data={
**self._config,
CONF_NAME: selected_device.name,
CONF_MODEL: selected_device.model,
CONF_TOKEN: selected_device.communication_key,
},
) )
machine_options = [ machine_options = [
SelectOptionDict( SelectOptionDict(
value=serial_number, value=device.serial_number,
label=f"{model_name} ({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( machine_selection_schema = vol.Schema(

View File

@ -4,6 +4,4 @@ from typing import Final
DOMAIN: Final = "lamarzocco" DOMAIN: Final = "lamarzocco"
CONF_MACHINE: Final = "machine" CONF_USE_BLUETOOTH: Final = "use_bluetooth"
CONF_USE_BLUETOOTH = "use_bluetooth"

View File

@ -3,133 +3,108 @@
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from datetime import timedelta from datetime import timedelta
import logging import logging
from time import time
from typing import Any
from bleak.backends.device import BLEDevice from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient
from lmcloud import LMCloud as LaMarzoccoClient from lmcloud.client_cloud import LaMarzoccoCloudClient
from lmcloud.const import BT_MODEL_NAMES from lmcloud.client_local import LaMarzoccoLocalClient
from lmcloud.exceptions import AuthFail, RequestNotSuccessful 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.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.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 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) SCAN_INTERVAL = timedelta(seconds=30)
FIRMWARE_UPDATE_INTERVAL = 3600
STATISTICS_UPDATE_INTERVAL = 300
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NAME_PREFIXES = tuple(BT_MODEL_NAMES)
class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to handle fetching data from the La Marzocco API centrally.""" """Class to handle fetching data from the La Marzocco API centrally."""
config_entry: ConfigEntry 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.""" """Initialize coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
self.lm = LaMarzoccoClient( self.local_connection_configured = local_client is not None
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
async def _async_update_data(self) -> None: assert self.config_entry.unique_id
"""Fetch data from API endpoint.""" self.device = LaMarzoccoMachine(
model=self.config_entry.data[CONF_MODEL],
if not self.lm.initialized: serial_number=self.config_entry.unique_id,
await self._async_init_client() name=self.config_entry.data[CONF_NAME],
cloud_client=cloud_client,
await self._async_handle_request( local_client=local_client,
self.lm.update_local_machine_status, force_update=True 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: async def async_setup(self) -> None:
"""Initialize the La Marzocco Client.""" """Set up the coordinator."""
if self._local_client is not None:
# Initialize cloud API _LOGGER.debug("Init WebSocket in background task")
_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")
self.config_entry.async_create_background_task( self.config_entry.async_create_background_task(
hass=self.hass, hass=self.hass,
target=self.lm.lm_local_api.websocket_connect( target=self.device.websocket_connect(
callback=self.lm.on_websocket_message_received, notify_callback=lambda: self.async_set_updated_data(None)
use_sigterm_handler=False,
), ),
name="lm_websocket_task", name="lm_websocket_task",
) )
# initialize Bluetooth async def websocket_close(_: Any | None = None) -> None:
if self.config_entry.options.get(CONF_USE_BLUETOOTH, True): 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: self.config_entry.async_on_unload(
return self.config_entry.data.get( self.hass.bus.async_listen_once(
CONF_MAC, "" EVENT_HOMEASSISTANT_STOP, websocket_close
) 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._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]( async def _async_handle_request[**_P](
self, self,
@ -137,9 +112,8 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
*args: _P.args, *args: _P.args,
**kwargs: _P.kwargs, **kwargs: _P.kwargs,
) -> None: ) -> None:
"""Handle a request to the API."""
try: try:
await func(*args, **kwargs) await func()
except AuthFail as ex: except AuthFail as ex:
msg = "Authentication failed." msg = "Authentication failed."
_LOGGER.debug(msg, exc_info=True) _LOGGER.debug(msg, exc_info=True)
@ -147,15 +121,3 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
except RequestNotSuccessful as ex: except RequestNotSuccessful as ex:
_LOGGER.debug(ex, exc_info=True) _LOGGER.debug(ex, exc_info=True)
raise UpdateFailed(f"Querying API failed. Error: {ex}") from ex 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,
)

View File

@ -2,7 +2,10 @@
from __future__ import annotations 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.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -13,31 +16,30 @@ from .coordinator import LaMarzoccoUpdateCoordinator
TO_REDACT = { TO_REDACT = {
"serial_number", "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( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """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 # collect all data sources
data = {} diagnostics_data = DiagnosticsData(
data["current_status"] = coordinator.lm.current_status model=device.model,
data["machine_info"] = coordinator.lm.machine_info config=asdict(device.config),
data["config"] = coordinator.lm.config firmware=[{key: asdict(firmware)} for key, firmware in device.firmware.items()],
data["statistics"] = {"stats": coordinator.lm.statistics} # wrap to satisfy mypy statistics=asdict(device.statistics),
)
# build a firmware section return async_redact_data(diagnostics_data, TO_REDACT)
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)

View File

@ -3,7 +3,8 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass 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.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
@ -17,11 +18,13 @@ from .coordinator import LaMarzoccoUpdateCoordinator
class LaMarzoccoEntityDescription(EntityDescription): class LaMarzoccoEntityDescription(EntityDescription):
"""Description for all LM entities.""" """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 supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
class LaMarzoccoBaseEntity(CoordinatorEntity[LaMarzoccoUpdateCoordinator]): class LaMarzoccoBaseEntity(
CoordinatorEntity[LaMarzoccoUpdateCoordinator],
):
"""Common elements for all entities.""" """Common elements for all entities."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -33,15 +36,15 @@ class LaMarzoccoBaseEntity(CoordinatorEntity[LaMarzoccoUpdateCoordinator]):
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator) super().__init__(coordinator)
lm = coordinator.lm device = coordinator.device
self._attr_unique_id = f"{lm.serial_number}_{key}" self._attr_unique_id = f"{device.serial_number}_{key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, lm.serial_number)}, identifiers={(DOMAIN, device.serial_number)},
name=lm.machine_name, name=device.name,
manufacturer="La Marzocco", manufacturer="La Marzocco",
model=lm.true_model_name, model=device.full_model_name,
serial_number=lm.serial_number, serial_number=device.serial_number,
sw_version=lm.firmware_version, sw_version=device.firmware[FirmwareType.MACHINE].current_version,
) )
@ -50,19 +53,18 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity):
entity_description: LaMarzoccoEntityDescription 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__( def __init__(
self, self,
coordinator: LaMarzoccoUpdateCoordinator, coordinator: LaMarzoccoUpdateCoordinator,
entity_description: LaMarzoccoEntityDescription, entity_description: LaMarzoccoEntityDescription,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator, entity_description.key) super().__init__(coordinator, entity_description.key)
self.entity_description = entity_description 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
)

View File

@ -26,10 +26,7 @@
"default": "mdi:thermometer-water" "default": "mdi:thermometer-water"
}, },
"dose": { "dose": {
"default": "mdi:weight-kilogram" "default": "mdi:cup-water"
},
"steam_temp": {
"default": "mdi:thermometer-water"
}, },
"prebrew_off": { "prebrew_off": {
"default": "mdi:water-off" "default": "mdi:water-off"
@ -40,6 +37,9 @@
"preinfusion_off": { "preinfusion_off": {
"default": "mdi:water" "default": "mdi:water"
}, },
"steam_temp": {
"default": "mdi:thermometer-water"
},
"tea_water_duration": { "tea_water_duration": {
"default": "mdi:timer-sand" "default": "mdi:timer-sand"
} }
@ -58,7 +58,7 @@
"state": { "state": {
"disabled": "mdi:water-pump-off", "disabled": "mdi:water-pump-off",
"prebrew": "mdi:water-pump", "prebrew": "mdi:water-pump",
"preinfusion": "mdi:water-pump" "typeb": "mdi:water-pump"
} }
} }
}, },

View File

@ -22,5 +22,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["lmcloud"], "loggers": ["lmcloud"],
"requirements": ["lmcloud==0.4.35"] "requirements": ["lmcloud==1.1.11"]
} }

View File

@ -4,8 +4,15 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from lmcloud import LMCloud as LaMarzoccoClient from lmcloud.const import (
from lmcloud.const import KEYS_PER_MODEL, LaMarzoccoModel KEYS_PER_MODEL,
BoilerType,
MachineModel,
PhysicalKey,
PrebrewMode,
)
from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.number import ( from homeassistant.components.number import (
NumberDeviceClass, NumberDeviceClass,
@ -35,10 +42,8 @@ class LaMarzoccoNumberEntityDescription(
): ):
"""Description of a La Marzocco number entity.""" """Description of a La Marzocco number entity."""
native_value_fn: Callable[[LaMarzoccoClient], float | int] native_value_fn: Callable[[LaMarzoccoMachineConfig], float | int]
set_value_fn: Callable[ set_value_fn: Callable[[LaMarzoccoMachine, float | int], Coroutine[Any, Any, bool]]
[LaMarzoccoUpdateCoordinator, float | int], Coroutine[Any, Any, bool]
]
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -48,9 +53,9 @@ class LaMarzoccoKeyNumberEntityDescription(
): ):
"""Description of an La Marzocco number entity with keys.""" """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[ 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_step=PRECISION_TENTHS,
native_min_value=85, native_min_value=85,
native_max_value=104, native_max_value=104,
set_value_fn=lambda coordinator, temp: coordinator.lm.set_coffee_temp( set_value_fn=lambda machine, temp: machine.set_temp(BoilerType.COFFEE, temp),
temp, coordinator.async_get_ble_device() native_value_fn=lambda config: config.boilers[
), BoilerType.COFFEE
native_value_fn=lambda lm: lm.current_status["coffee_set_temp"], ].target_temperature,
), ),
LaMarzoccoNumberEntityDescription( LaMarzoccoNumberEntityDescription(
key="steam_temp", key="steam_temp",
@ -76,14 +81,14 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_step=PRECISION_WHOLE, native_step=PRECISION_WHOLE,
native_min_value=126, native_min_value=126,
native_max_value=131, native_max_value=131,
set_value_fn=lambda coordinator, temp: coordinator.lm.set_steam_temp( set_value_fn=lambda machine, temp: machine.set_temp(BoilerType.STEAM, temp),
int(temp), coordinator.async_get_ble_device() native_value_fn=lambda config: config.boilers[
), BoilerType.STEAM
native_value_fn=lambda lm: lm.current_status["steam_set_temp"], ].target_temperature,
supported_fn=lambda coordinator: coordinator.lm.model_name supported_fn=lambda coordinator: coordinator.device.model
in ( in (
LaMarzoccoModel.GS3_AV, MachineModel.GS3_AV,
LaMarzoccoModel.GS3_MP, MachineModel.GS3_MP,
), ),
), ),
LaMarzoccoNumberEntityDescription( LaMarzoccoNumberEntityDescription(
@ -94,54 +99,17 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_step=PRECISION_WHOLE, native_step=PRECISION_WHOLE,
native_min_value=0, native_min_value=0,
native_max_value=30, native_max_value=30,
set_value_fn=lambda coordinator, value: coordinator.lm.set_dose_hot_water( set_value_fn=lambda machine, value: machine.set_dose_tea_water(int(value)),
value=int(value) native_value_fn=lambda config: config.dose_hot_water,
), supported_fn=lambda coordinator: coordinator.device.model
native_value_fn=lambda lm: lm.current_status["dose_hot_water"],
supported_fn=lambda coordinator: coordinator.lm.model_name
in ( in (
LaMarzoccoModel.GS3_AV, MachineModel.GS3_AV,
LaMarzoccoModel.GS3_MP, 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, ...] = ( KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
LaMarzoccoKeyNumberEntityDescription( LaMarzoccoKeyNumberEntityDescription(
key="prebrew_off", key="prebrew_off",
@ -152,11 +120,14 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
native_min_value=1, native_min_value=1,
native_max_value=10, native_max_value=10,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
set_value_fn=_set_prebrew_off, set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
native_value_fn=lambda lm, key: lm.current_status[f"prebrewing_ton_k{key}"], prebrew_off_time=value, key=key
available_fn=lambda lm: lm.current_status["enable_prebrewing"], ),
supported_fn=lambda coordinator: coordinator.lm.model_name native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time,
!= LaMarzoccoModel.GS3_MP, 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( LaMarzoccoKeyNumberEntityDescription(
key="prebrew_on", key="prebrew_on",
@ -167,11 +138,14 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
native_min_value=2, native_min_value=2,
native_max_value=10, native_max_value=10,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
set_value_fn=_set_prebrew_on, set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
native_value_fn=lambda lm, key: lm.current_status[f"prebrewing_toff_k{key}"], prebrew_on_time=value, key=key
available_fn=lambda lm: lm.current_status["enable_prebrewing"], ),
supported_fn=lambda coordinator: coordinator.lm.model_name native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time,
!= LaMarzoccoModel.GS3_MP, 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( LaMarzoccoKeyNumberEntityDescription(
key="preinfusion_off", key="preinfusion_off",
@ -182,11 +156,16 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
native_min_value=2, native_min_value=2,
native_max_value=29, native_max_value=29,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
set_value_fn=_set_preinfusion, set_value_fn=lambda machine, value, key: machine.set_preinfusion_time(
native_value_fn=lambda lm, key: lm.current_status[f"preinfusion_k{key}"], preinfusion_time=value, key=key
available_fn=lambda lm: lm.current_status["enable_preinfusion"], ),
supported_fn=lambda coordinator: coordinator.lm.model_name native_value_fn=lambda config, key: config.prebrew_configuration[
!= LaMarzoccoModel.GS3_MP, 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( LaMarzoccoKeyNumberEntityDescription(
key="dose", key="dose",
@ -196,10 +175,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
native_min_value=0, native_min_value=0,
native_max_value=999, native_max_value=999,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
set_value_fn=lambda lm, ticks, key: lm.set_dose(key=key, value=int(ticks)), set_value_fn=lambda machine, ticks, key: machine.set_dose(
native_value_fn=lambda lm, key: lm.current_status[f"dose_k{key}"], dose=int(ticks), key=key
supported_fn=lambda coordinator: coordinator.lm.model_name ),
== LaMarzoccoModel.GS3_AV, 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: ) -> None:
"""Set up number entities.""" """Set up number entities."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities: list[NumberEntity] = [ entities: list[NumberEntity] = [
LaMarzoccoNumberEntity(coordinator, description) LaMarzoccoNumberEntity(coordinator, description)
for description in ENTITIES for description in ENTITIES
@ -220,12 +200,11 @@ async def async_setup_entry(
for description in KEY_ENTITIES: for description in KEY_ENTITIES:
if description.supported_fn(coordinator): 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( entities.extend(
LaMarzoccoKeyNumberEntity(coordinator, description, key) LaMarzoccoKeyNumberEntity(coordinator, description, key)
for key in range(min(num_keys, 1), num_keys + 1) for key in range(min(num_keys, 1), num_keys + 1)
) )
async_add_entities(entities) async_add_entities(entities)
@ -237,12 +216,13 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
@property @property
def native_value(self) -> float: def native_value(self) -> float:
"""Return the current value.""" """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: async def async_set_native_value(self, value: float) -> None:
"""Set the value.""" """Set the value."""
await self.entity_description.set_value_fn(self.coordinator, value) if value != self.native_value:
self.async_write_ha_state() await self.entity_description.set_value_fn(self.coordinator.device, value)
self.async_write_ha_state()
class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity): class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity):
@ -273,12 +253,13 @@ class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity):
def native_value(self) -> float: def native_value(self) -> float:
"""Return the current value.""" """Return the current value."""
return self.entity_description.native_value_fn( 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: async def async_set_native_value(self, value: float) -> None:
"""Set the value.""" """Set the value."""
await self.entity_description.set_value_fn( if value != self.native_value:
self.coordinator.lm, value, self.pyhsical_key await self.entity_description.set_value_fn(
) self.coordinator.device, value, PhysicalKey(self.pyhsical_key)
self.async_write_ha_state() )
self.async_write_ha_state()

View File

@ -4,18 +4,43 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from lmcloud import LMCloud as LaMarzoccoClient from lmcloud.const import MachineModel, PrebrewMode, SteamLevel
from lmcloud.const import LaMarzoccoModel from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription 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) @dataclass(frozen=True, kw_only=True)
class LaMarzoccoSelectEntityDescription( class LaMarzoccoSelectEntityDescription(
@ -24,10 +49,8 @@ class LaMarzoccoSelectEntityDescription(
): ):
"""Description of a La Marzocco select entity.""" """Description of a La Marzocco select entity."""
current_option_fn: Callable[[LaMarzoccoClient], str] current_option_fn: Callable[[LaMarzoccoMachineConfig], str]
select_option_fn: Callable[ select_option_fn: Callable[[LaMarzoccoMachine, str], Coroutine[Any, Any, bool]]
[LaMarzoccoUpdateCoordinator, str], Coroutine[Any, Any, bool]
]
ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
@ -35,25 +58,27 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
key="steam_temp_select", key="steam_temp_select",
translation_key="steam_temp_select", translation_key="steam_temp_select",
options=["1", "2", "3"], options=["1", "2", "3"],
select_option_fn=lambda coordinator, option: coordinator.lm.set_steam_level( select_option_fn=lambda machine, option: machine.set_steam_level(
int(option), coordinator.async_get_ble_device() STEAM_LEVEL_HA_TO_LM[option]
), ),
current_option_fn=lambda lm: lm.current_status["steam_level_set"], current_option_fn=lambda config: STEAM_LEVEL_LM_TO_HA[config.steam_level],
supported_fn=lambda coordinator: coordinator.lm.model_name supported_fn=lambda coordinator: coordinator.device.model
== LaMarzoccoModel.LINEA_MICRA, == MachineModel.LINEA_MICRA,
), ),
LaMarzoccoSelectEntityDescription( LaMarzoccoSelectEntityDescription(
key="prebrew_infusion_select", key="prebrew_infusion_select",
translation_key="prebrew_infusion_select", translation_key="prebrew_infusion_select",
entity_category=EntityCategory.CONFIG,
options=["disabled", "prebrew", "preinfusion"], options=["disabled", "prebrew", "preinfusion"],
select_option_fn=lambda coordinator, select_option_fn=lambda machine, option: machine.set_prebrew_mode(
option: coordinator.lm.select_pre_brew_infusion_mode(option.capitalize()), PREBREW_MODE_HA_TO_LM[option]
current_option_fn=lambda lm: lm.pre_brew_infusion_mode.lower(), ),
supported_fn=lambda coordinator: coordinator.lm.model_name current_option_fn=lambda config: PREBREW_MODE_LM_TO_HA[config.prebrew_mode],
supported_fn=lambda coordinator: coordinator.device.model
in ( in (
LaMarzoccoModel.GS3_AV, MachineModel.GS3_AV,
LaMarzoccoModel.LINEA_MICRA, MachineModel.LINEA_MICRA,
LaMarzoccoModel.LINEA_MINI, MachineModel.LINEA_MINI,
), ),
), ),
) )
@ -82,9 +107,14 @@ class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity):
@property @property
def current_option(self) -> str: def current_option(self) -> str:
"""Return the current selected option.""" """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: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""
await self.entity_description.select_option_fn(self.coordinator, option) if option != self.current_option:
self.async_write_ha_state() await self.entity_description.select_option_fn(
self.coordinator.device, option
)
self.async_write_ha_state()

View File

@ -3,7 +3,8 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass 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 ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -22,12 +23,11 @@ from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class LaMarzoccoSensorEntityDescription( class LaMarzoccoSensorEntityDescription(
LaMarzoccoEntityDescription, LaMarzoccoEntityDescription, SensorEntityDescription
SensorEntityDescription,
): ):
"""Description of a La Marzocco sensor.""" """Description of a La Marzocco sensor."""
value_fn: Callable[[LaMarzoccoClient], float | int] value_fn: Callable[[LaMarzoccoMachine], float | int]
ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
@ -36,7 +36,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
translation_key="drink_stats_coffee", translation_key="drink_stats_coffee",
native_unit_of_measurement="drinks", native_unit_of_measurement="drinks",
state_class=SensorStateClass.TOTAL_INCREASING, 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, entity_category=EntityCategory.DIAGNOSTIC,
), ),
LaMarzoccoSensorEntityDescription( LaMarzoccoSensorEntityDescription(
@ -44,7 +45,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
translation_key="drink_stats_flushing", translation_key="drink_stats_flushing",
native_unit_of_measurement="drinks", native_unit_of_measurement="drinks",
state_class=SensorStateClass.TOTAL_INCREASING, 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, entity_category=EntityCategory.DIAGNOSTIC,
), ),
LaMarzoccoSensorEntityDescription( LaMarzoccoSensorEntityDescription(
@ -53,8 +55,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS, native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION, device_class=SensorDeviceClass.DURATION,
value_fn=lambda lm: lm.current_status.get("brew_active_duration", 0), value_fn=lambda device: device.config.brew_active_duration,
available_fn=lambda lm: lm.websocket_connected, available_fn=lambda device: device.websocket_connected,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda coordinator: coordinator.local_connection_configured, supported_fn=lambda coordinator: coordinator.local_connection_configured,
), ),
@ -65,7 +67,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
suggested_display_precision=1, suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, 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( LaMarzoccoSensorEntityDescription(
key="current_temp_steam", key="current_temp_steam",
@ -74,7 +78,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
suggested_display_precision=1, suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, 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 @property
def native_value(self) -> int | float: def native_value(self) -> int | float:
"""State of the sensor.""" """State of the sensor."""
return self.entity_description.value_fn(self.coordinator.lm) return self.entity_description.value_fn(self.coordinator.device)

View File

@ -68,7 +68,7 @@
}, },
"calendar": { "calendar": {
"auto_on_off_schedule": { "auto_on_off_schedule": {
"name": "Auto on/off schedule" "name": "Auto on/off schedule ({id})"
} }
}, },
"number": { "number": {
@ -139,9 +139,6 @@
} }
}, },
"switch": { "switch": {
"auto_on_off": {
"name": "Auto on/off"
},
"steam_boiler": { "steam_boiler": {
"name": "Steam boiler" "name": "Steam boiler"
} }
@ -154,5 +151,11 @@
"name": "Gateway firmware" "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."
}
} }
} }

View File

@ -4,14 +4,16 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any 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.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
@ -22,8 +24,8 @@ class LaMarzoccoSwitchEntityDescription(
): ):
"""Description of a La Marzocco Switch.""" """Description of a La Marzocco Switch."""
control_fn: Callable[[LaMarzoccoUpdateCoordinator, bool], Coroutine[Any, Any, bool]] control_fn: Callable[[LaMarzoccoMachine, bool], Coroutine[Any, Any, bool]]
is_on_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] is_on_fn: Callable[[LaMarzoccoMachineConfig], bool]
ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
@ -31,30 +33,14 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
key="main", key="main",
translation_key="main", translation_key="main",
name=None, name=None,
control_fn=lambda coordinator, state: coordinator.lm.set_power( control_fn=lambda machine, state: machine.set_power(state),
state, coordinator.async_get_ble_device() is_on_fn=lambda config: config.turned_on,
),
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,
), ),
LaMarzoccoSwitchEntityDescription( LaMarzoccoSwitchEntityDescription(
key="steam_boiler_enable", key="steam_boiler_enable",
translation_key="steam_boiler", translation_key="steam_boiler",
control_fn=lambda coordinator, state: coordinator.lm.set_steam( control_fn=lambda machine, state: machine.set_steam(state),
state, coordinator.async_get_ble_device() is_on_fn=lambda config: config.boilers[BoilerType.STEAM].enabled,
),
is_on_fn=lambda coordinator: coordinator.lm.current_status[
"steam_boiler_enable"
],
), ),
) )
@ -81,15 +67,15 @@ class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn device on.""" """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() self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn device off.""" """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() self.async_write_ha_state()
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if device is on.""" """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)

View File

@ -1,11 +1,9 @@
"""Support for La Marzocco update entities.""" """Support for La Marzocco update entities."""
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from lmcloud import LMCloud as LaMarzoccoClient from lmcloud.const import FirmwareType
from lmcloud.const import LaMarzoccoUpdateableComponent
from homeassistant.components.update import ( from homeassistant.components.update import (
UpdateDeviceClass, UpdateDeviceClass,
@ -30,9 +28,7 @@ class LaMarzoccoUpdateEntityDescription(
): ):
"""Description of a La Marzocco update entities.""" """Description of a La Marzocco update entities."""
current_fw_fn: Callable[[LaMarzoccoClient], str] component: FirmwareType
latest_fw_fn: Callable[[LaMarzoccoClient], str]
component: LaMarzoccoUpdateableComponent
ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = ( ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = (
@ -40,18 +36,14 @@ ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = (
key="machine_firmware", key="machine_firmware",
translation_key="machine_firmware", translation_key="machine_firmware",
device_class=UpdateDeviceClass.FIRMWARE, device_class=UpdateDeviceClass.FIRMWARE,
current_fw_fn=lambda lm: lm.firmware_version, component=FirmwareType.MACHINE,
latest_fw_fn=lambda lm: lm.latest_firmware_version,
component=LaMarzoccoUpdateableComponent.MACHINE,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
LaMarzoccoUpdateEntityDescription( LaMarzoccoUpdateEntityDescription(
key="gateway_firmware", key="gateway_firmware",
translation_key="gateway_firmware", translation_key="gateway_firmware",
device_class=UpdateDeviceClass.FIRMWARE, device_class=UpdateDeviceClass.FIRMWARE,
current_fw_fn=lambda lm: lm.gateway_version, component=FirmwareType.GATEWAY,
latest_fw_fn=lambda lm: lm.latest_gateway_version,
component=LaMarzoccoUpdateableComponent.GATEWAY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
) )
@ -81,12 +73,16 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
@property @property
def installed_version(self) -> str | None: def installed_version(self) -> str | None:
"""Return the current firmware version.""" """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 @property
def latest_version(self) -> str: def latest_version(self) -> str:
"""Return the latest firmware version.""" """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( async def async_install(
self, version: str | None, backup: bool, **kwargs: Any self, version: str | None, backup: bool, **kwargs: Any
@ -94,7 +90,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
"""Install an update.""" """Install an update."""
self._attr_in_progress = True self._attr_in_progress = True
self.async_write_ha_state() self.async_write_ha_state()
success = await self.coordinator.lm.update_firmware( success = await self.coordinator.device.update_firmware(
self.entity_description.component self.entity_description.component
) )
if not success: if not success:

View File

@ -1260,7 +1260,7 @@ linear-garage-door==0.2.9
linode-api==4.1.9b1 linode-api==4.1.9b1
# homeassistant.components.lamarzocco # homeassistant.components.lamarzocco
lmcloud==0.4.35 lmcloud==1.1.11
# homeassistant.components.google_maps # homeassistant.components.google_maps
locationsharinglib==5.0.1 locationsharinglib==5.0.1

View File

@ -1017,7 +1017,7 @@ libsoundtouch==0.8
linear-garage-door==0.2.9 linear-garage-door==0.2.9
# homeassistant.components.lamarzocco # homeassistant.components.lamarzocco
lmcloud==0.4.35 lmcloud==1.1.11
# homeassistant.components.logi_circle # homeassistant.components.logi_circle
logi-circle==0.2.3 logi-circle==0.2.3

View File

@ -1,6 +1,6 @@
"""Mock inputs for tests.""" """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.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -18,31 +18,34 @@ PASSWORD_SELECTION = {
USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"} USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"}
MODEL_DICT = { SERIAL_DICT = {
LaMarzoccoModel.GS3_AV: ("GS01234", "GS3 AV"), MachineModel.GS3_AV: "GS01234",
LaMarzoccoModel.GS3_MP: ("GS01234", "GS3 MP"), MachineModel.GS3_MP: "GS01234",
LaMarzoccoModel.LINEA_MICRA: ("MR01234", "Linea Micra"), MachineModel.LINEA_MICRA: "MR01234",
LaMarzoccoModel.LINEA_MINI: ("LM01234", "Linea Mini"), MachineModel.LINEA_MINI: "LM01234",
} }
WAKE_UP_SLEEP_ENTRY_IDS = ["Os2OswX", "aXFz5bJ"]
async def async_init_integration( async def async_init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None: ) -> None:
"""Set up the La Marzocco integration for testing.""" """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.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
def get_bluetooth_service_info( def get_bluetooth_service_info(
model: LaMarzoccoModel, serial: str model: MachineModel, serial: str
) -> BluetoothServiceInfo: ) -> BluetoothServiceInfo:
"""Return a mocked 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}" name = f"GS3_{serial}"
elif model == LaMarzoccoModel.LINEA_MINI: elif model == MachineModel.LINEA_MINI:
name = f"MINI_{serial}" name = f"MINI_{serial}"
elif model == LaMarzoccoModel.LINEA_MICRA: elif model == MachineModel.LINEA_MICRA:
name = f"MICRA_{serial}" name = f"MICRA_{serial}"
return BluetoothServiceInfo( return BluetoothServiceInfo(
name=name, name=name,

View File

@ -1,22 +1,23 @@
"""Lamarzocco session fixtures.""" """Lamarzocco session fixtures."""
from collections.abc import Callable
import json
from unittest.mock import MagicMock, patch 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 import pytest
from typing_extensions import Generator from typing_extensions import Generator
from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN
from homeassistant.core import HomeAssistant 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 ( from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture
MockConfigEntry,
load_json_array_fixture,
load_json_object_fixture,
)
@pytest.fixture @pytest.fixture
@ -27,12 +28,13 @@ def mock_config_entry(
entry = MockConfigEntry( entry = MockConfigEntry(
title="My LaMarzocco", title="My LaMarzocco",
domain=DOMAIN, domain=DOMAIN,
version=2,
data=USER_INPUT data=USER_INPUT
| { | {
CONF_MACHINE: mock_lamarzocco.serial_number, CONF_MODEL: mock_lamarzocco.model,
CONF_HOST: "host", CONF_HOST: "host",
CONF_NAME: "name", CONF_TOKEN: "token",
CONF_MAC: "mac", CONF_NAME: "GS3",
}, },
unique_id=mock_lamarzocco.serial_number, unique_id=mock_lamarzocco.serial_number,
) )
@ -44,77 +46,96 @@ def mock_config_entry(
async def init_integration( async def init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the LaMetric integration for testing.""" """Set up the La Marzocco integration for testing."""
await async_init_integration(hass, mock_config_entry) await async_init_integration(hass, mock_config_entry)
return mock_config_entry return mock_config_entry
@pytest.fixture @pytest.fixture
def device_fixture() -> LaMarzoccoModel: def device_fixture() -> MachineModel:
"""Return the device fixture for a specific device.""" """Return the device fixture for a specific device."""
return LaMarzoccoModel.GS3_AV return MachineModel.GS3_AV
@pytest.fixture @pytest.fixture
def mock_lamarzocco(device_fixture: LaMarzoccoModel) -> Generator[MagicMock]: def mock_device_info() -> LaMarzoccoDeviceInfo:
"""Return a mocked LM client.""" """Return a mocked La Marzocco device info."""
model_name = device_fixture 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 ( with (
patch( patch(
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient", "homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine",
autospec=True, autospec=True,
) as lamarzocco_mock, ) as lamarzocco_mock,
patch(
"homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient",
new=lamarzocco_mock,
),
): ):
lamarzocco = lamarzocco_mock.return_value lamarzocco = lamarzocco_mock.return_value
lamarzocco.machine_info = { lamarzocco.name = dummy_machine.name
"machine_name": serial_number, lamarzocco.model = dummy_machine.model
"serial_number": serial_number, 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.firmware[FirmwareType.GATEWAY].latest_version = "v3.5-rc3"
lamarzocco.true_model_name = true_model_name lamarzocco.firmware[FirmwareType.MACHINE].latest_version = "1.55"
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
async def websocket_connect_mock( async def websocket_connect_mock(
callback: MagicMock, use_sigterm_handler: MagicMock notify_callback: Callable | None,
) -> None: ) -> None:
"""Mock the websocket connect method.""" """Mock the websocket connect method."""
return None return None
lamarzocco.lm_local_api.websocket_connect = websocket_connect_mock lamarzocco.websocket_connect = websocket_connect_mock
lamarzocco.lm_bluetooth = MagicMock()
lamarzocco.lm_bluetooth.address = "AA:BB:CC:DD:EE:FF"
yield lamarzocco yield lamarzocco
@ -133,3 +154,11 @@ def remove_local_connection(
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_bluetooth(enable_bluetooth: None) -> None: def mock_bluetooth(enable_bluetooth: None) -> None:
"""Auto mock bluetooth.""" """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
)

View File

@ -13,11 +13,16 @@
"schedulingType": "weeklyScheduling" "schedulingType": "weeklyScheduling"
} }
], ],
"machine_sn": "GS01234", "machine_sn": "Sn01239157",
"machine_hw": "2", "machine_hw": "2",
"isPlumbedIn": true, "isPlumbedIn": true,
"isBackFlushEnabled": false, "isBackFlushEnabled": false,
"standByTime": 0, "standByTime": 0,
"smartStandBy": {
"enabled": true,
"minutes": 10,
"mode": "LastBrewing"
},
"tankStatus": true, "tankStatus": true,
"groupCapabilities": [ "groupCapabilities": [
{ {
@ -121,58 +126,32 @@
} }
] ]
}, },
"weeklySchedulingConfig": { "wakeUpSleepEntries": [
"enabled": true, {
"monday": { "days": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday"
],
"enabled": true, "enabled": true,
"h_on": 6, "id": "Os2OswX",
"h_off": 16, "steam": true,
"m_on": 0, "timeOff": "24:0",
"m_off": 0 "timeOn": "22:0"
}, },
"tuesday": { {
"days": ["sunday"],
"enabled": true, "enabled": true,
"h_on": 6, "id": "aXFz5bJ",
"h_off": 16, "steam": true,
"m_on": 0, "timeOff": "7:30",
"m_off": 0 "timeOn": "7: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
} }
}, ],
"clock": "1901-07-08T10:29:00", "clock": "1901-07-08T10:29:00",
"firmwareVersions": [ "firmwareVersions": [
{ {

View File

@ -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
}

View File

@ -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"
}
]

View File

@ -1,7 +1,7 @@
# serializer version: 1 # serializer version: 1
# name: test_calendar_edge_cases[start_date0-end_date0] # name: test_calendar_edge_cases[start_date0-end_date0]
dict({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([ 'events': list([
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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] # name: test_calendar_edge_cases[start_date1-end_date1]
dict({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([ 'events': list([
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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] # name: test_calendar_edge_cases[start_date2-end_date2]
dict({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([ 'events': list([
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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] # name: test_calendar_edge_cases[start_date3-end_date3]
dict({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([ 'events': list([
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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] # name: test_calendar_edge_cases[start_date4-end_date4]
dict({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([ 'events': list([
]), ]),
}), }),
@ -65,7 +65,7 @@
# --- # ---
# name: test_calendar_edge_cases[start_date5-end_date5] # name: test_calendar_edge_cases[start_date5-end_date5]
dict({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([ 'events': list([
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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 # name: test_calendar_events[entry.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-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': <ANY>,
'entity_id': 'calendar.gs01234_auto_on_off_schedule',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_calendar_events.1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -114,7 +95,7 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'calendar', 'domain': 'calendar',
'entity_category': None, 'entity_category': None,
'entity_id': 'calendar.gs01234_auto_on_off_schedule', 'entity_id': 'calendar.gs01234_auto_on_off_schedule_axfz5bj',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
@ -126,86 +107,267 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Auto on/off schedule', 'original_name': 'Auto on/off schedule (aXFz5bJ)',
'platform': 'lamarzocco', 'platform': 'lamarzocco',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': 'auto_on_off_schedule', '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, '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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([ 'events': list([
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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', 'end': '2024-01-14T07:30:00-08:00',
'start': '2024-01-13T10:00:00-08:00', 'start': '2024-01-14T07:00:00-08:00',
'summary': 'Machine My LaMarzocco on', 'summary': 'Machine My LaMarzocco on',
}), }),
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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', 'end': '2024-01-21T07:30:00-08:00',
'start': '2024-01-17T08:00:00-08:00', 'start': '2024-01-21T07:00:00-08:00',
'summary': 'Machine My LaMarzocco on', 'summary': 'Machine My LaMarzocco on',
}), }),
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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', 'end': '2024-01-28T07:30:00-08:00',
'start': '2024-01-19T06:00:00-08:00', 'start': '2024-01-28T07:00:00-08:00',
'summary': 'Machine My LaMarzocco on', 'summary': 'Machine My LaMarzocco on',
}), }),
dict({ dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time', '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', 'end': '2024-02-04T07:30:00-08:00',
'start': '2024-01-20T10:00:00-08:00', 'start': '2024-02-04T07: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',
'summary': 'Machine My LaMarzocco on', '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': <ANY>,
'entity_id': 'calendar.gs01234_auto_on_off_schedule_axfz5bj',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'entity_id': 'calendar.gs01234_auto_on_off_schedule_os2oswx',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_no_calendar_events_global_disable # name: test_no_calendar_events_global_disable
dict({ dict({
'calendar.gs01234_auto_on_off_schedule': dict({ 'calendar.gs01234_auto_on_off_schedule_os2oswx': dict({
'events': list([ 'events': list([
]), ]),
}), }),

View File

@ -2,297 +2,107 @@
# name: test_diagnostics # name: test_diagnostics
dict({ dict({
'config': dict({ 'config': dict({
'boilerTargetTemperature': dict({ 'boilers': dict({
'CoffeeBoiler1': 95, 'CoffeeBoiler1': dict({
'SteamBoiler': 123.9000015258789, 'current_temperature': 96.5,
}), 'enabled': True,
'boilers': list([ 'target_temperature': 95,
dict({
'current': 123.80000305175781,
'id': 'SteamBoiler',
'isEnabled': True,
'target': 123.9000015258789,
}), }),
dict({ 'SteamBoiler': dict({
'current': 96.5, 'current_temperature': 123.80000305175781,
'id': 'CoffeeBoiler1', 'enabled': True,
'isEnabled': True, 'target_temperature': 123.9000015258789,
'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',
}), }),
}), }),
'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, 'brew_active': False,
'coffee_boiler_on': True, 'brew_active_duration': 0,
'coffee_set_temp': 95, 'dose_hot_water': 8,
'coffee_temp': 93, 'doses': dict({
'dose_hot_water': 1023, '1': 135,
'dose_k1': 1023, '2': 97,
'dose_k2': 1023, '3': 108,
'dose_k3': 1023, '4': 121,
'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',
}), }),
'machine': dict({ 'plumbed_in': True,
'latest_version': '1.2', 'prebrew_configuration': dict({
'version': '1.1', '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({ 'firmware': list([
'machine_name': 'GS01234', dict({
'serial_number': '**REDACTED**', '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({ 'statistics': dict({
'stats': list([ 'continous': 2252,
dict({ 'drink_stats': dict({
'coffeeType': 0, '1': 1047,
'count': 1047, '2': 560,
}), '3': 468,
dict({ '4': 312,
'coffeeType': 1, }),
'count': 560, 'total_flushes': 1740,
}),
dict({
'coffeeType': 2,
'count': 468,
}),
dict({
'coffeeType': 3,
'count': 312,
}),
dict({
'coffeeType': 4,
'count': 2252,
}),
dict({
'coffeeType': -1,
'count': 1740,
}),
]),
}), }),
}) })
# --- # ---

View File

@ -56,7 +56,7 @@
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'temperature', 'device_class': 'temperature',
@ -72,10 +72,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -113,7 +113,7 @@
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'temperature', 'device_class': 'temperature',
@ -129,10 +129,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -170,7 +170,7 @@
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -186,10 +186,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -227,7 +227,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -243,10 +243,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -284,7 +284,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Dose Key 1', 'friendly_name': 'GS01234 Dose Key 1',
@ -299,10 +299,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Dose Key 2', 'friendly_name': 'GS01234 Dose Key 2',
@ -317,10 +317,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Dose Key 3', 'friendly_name': 'GS01234 Dose Key 3',
@ -335,10 +335,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Dose Key 4', 'friendly_name': 'GS01234 Dose Key 4',
@ -353,10 +353,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -372,10 +372,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -391,10 +391,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -410,10 +410,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -429,10 +429,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -448,10 +448,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -467,10 +467,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -486,10 +486,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -505,10 +505,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -524,10 +524,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -543,10 +543,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -562,10 +562,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -581,10 +581,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -600,10 +600,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -641,7 +641,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -657,10 +657,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -698,7 +698,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -714,10 +714,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -755,7 +755,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -771,10 +771,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -812,7 +812,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -828,10 +828,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -869,7 +869,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# 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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -885,10 +885,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),

View File

@ -14,7 +14,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unknown', 'state': 'preinfusion',
}) })
# --- # ---
# name: test_pre_brew_infusion_select[GS3 AV].1 # name: test_pre_brew_infusion_select[GS3 AV].1
@ -34,7 +34,7 @@
'device_id': <ANY>, 'device_id': <ANY>,
'disabled_by': None, 'disabled_by': None,
'domain': 'select', 'domain': 'select',
'entity_category': None, 'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.gs01234_prebrew_infusion_mode', 'entity_id': 'select.gs01234_prebrew_infusion_mode',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
@ -71,7 +71,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unknown', 'state': 'preinfusion',
}) })
# --- # ---
# name: test_pre_brew_infusion_select[Linea Mini].1 # name: test_pre_brew_infusion_select[Linea Mini].1
@ -91,7 +91,7 @@
'device_id': <ANY>, 'device_id': <ANY>,
'disabled_by': None, 'disabled_by': None,
'domain': 'select', 'domain': 'select',
'entity_category': None, 'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.lm01234_prebrew_infusion_mode', 'entity_id': 'select.lm01234_prebrew_infusion_mode',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
@ -128,7 +128,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unknown', 'state': 'preinfusion',
}) })
# --- # ---
# name: test_pre_brew_infusion_select[Micra].1 # name: test_pre_brew_infusion_select[Micra].1
@ -148,7 +148,7 @@
'device_id': <ANY>, 'device_id': <ANY>,
'disabled_by': None, 'disabled_by': None,
'domain': 'select', 'domain': 'select',
'entity_category': None, 'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.mr01234_prebrew_infusion_mode', 'entity_id': 'select.mr01234_prebrew_infusion_mode',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
@ -185,7 +185,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '3', 'state': '1',
}) })
# --- # ---
# name: test_steam_boiler_level[Micra].1 # name: test_steam_boiler_level[Micra].1

View File

@ -50,7 +50,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '93', 'state': '96.5',
}) })
# --- # ---
# name: test_sensors[GS01234_current_steam_temperature-entry] # name: test_sensors[GS01234_current_steam_temperature-entry]
@ -104,7 +104,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '113', 'state': '123.800003051758',
}) })
# --- # ---
# name: test_sensors[GS01234_shot_timer-entry] # name: test_sensors[GS01234_shot_timer-entry]
@ -205,7 +205,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '13', 'state': '1047',
}) })
# --- # ---
# name: test_sensors[GS01234_total_flushes_made-entry] # name: test_sensors[GS01234_total_flushes_made-entry]
@ -255,6 +255,6 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '69', 'state': '1740',
}) })
# --- # ---

View File

@ -20,16 +20,16 @@
'labels': set({ 'labels': set({
}), }),
'manufacturer': 'La Marzocco', 'manufacturer': 'La Marzocco',
'model': 'GS3 AV', 'model': <MachineModel.GS3_AV: 'GS3 AV'>,
'name': 'GS01234', 'name': 'GS01234',
'name_by_user': None, 'name_by_user': None,
'serial_number': 'GS01234', 'serial_number': 'GS01234',
'suggested_area': None, 'suggested_area': None,
'sw_version': '1.1', 'sw_version': '1.40',
'via_device_id': None, 'via_device_id': None,
}) })
# --- # ---
# name: test_switches[-set_power-args_on0-args_off0] # name: test_switches[-set_power]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234', 'friendly_name': 'GS01234',
@ -42,7 +42,7 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_switches[-set_power-args_on0-args_off0].1 # name: test_switches[-set_power].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -75,141 +75,7 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_switches[-set_power-kwargs_on0-kwargs_off0] # name: test_switches[_steam_boiler-set_steam]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS01234',
'icon': 'mdi:power',
}),
'context': <ANY>,
'entity_id': 'switch.gs01234',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switches[-set_power-kwargs_on0-kwargs_off0].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.gs01234',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'switch.gs01234_auto_on_off',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.gs01234_auto_on_off',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'switch.gs01234_auto_on_off',
'last_changed': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.gs01234_auto_on_off',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Steam boiler', 'friendly_name': 'GS01234 Steam boiler',
@ -222,53 +88,7 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2].1 # name: test_switches[_steam_boiler-set_steam].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.gs01234_steam_boiler',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'switch.gs01234_steam_boiler',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),

View File

@ -7,8 +7,8 @@
'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png',
'friendly_name': 'GS01234 Gateway firmware', 'friendly_name': 'GS01234 Gateway firmware',
'in_progress': False, 'in_progress': False,
'installed_version': 'v2.2-rc0', 'installed_version': 'v3.1-rc4',
'latest_version': 'v3.1-rc4', 'latest_version': 'v3.5-rc3',
'release_summary': None, 'release_summary': None,
'release_url': None, 'release_url': None,
'skipped_version': None, 'skipped_version': None,
@ -64,8 +64,8 @@
'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png',
'friendly_name': 'GS01234 Machine firmware', 'friendly_name': 'GS01234 Machine firmware',
'in_progress': False, 'in_progress': False,
'installed_version': '1.1', 'installed_version': '1.40',
'latest_version': '1.2', 'latest_version': '1.55',
'release_summary': None, 'release_summary': None,
'release_url': None, 'release_url': None,
'skipped_version': None, 'skipped_version': None,

View File

@ -1,7 +1,10 @@
"""Tests for La Marzocco binary sensors.""" """Tests for La Marzocco binary sensors."""
from datetime import timedelta
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from lmcloud.exceptions import RequestNotSuccessful
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -11,7 +14,7 @@ from homeassistant.helpers import entity_registry as er
from . import async_init_integration from . import async_init_integration
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
BINARY_SENSORS = ( BINARY_SENSORS = (
"brewing_active", "brewing_active",
@ -70,3 +73,29 @@ async def test_brew_active_unavailable(
) )
assert state assert state
assert state.state == STATE_UNAVAILABLE 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

View File

@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util 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 from tests.common import MockConfigEntry
@ -40,27 +40,37 @@ async def test_calendar_events(
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"calendar.{serial_number}_auto_on_off_schedule") for identifier in WAKE_UP_SLEEP_ENTRY_IDS:
assert state identifier = identifier.lower()
assert state == snapshot 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) entry = entity_registry.async_get(state.entity_id)
assert entry assert entry
assert entry == snapshot assert entry == snapshot(
name=f"entry.{serial_number}_auto_on_off_schedule_{identifier}"
)
events = await hass.services.async_call( events = await hass.services.async_call(
CALENDAR_DOMAIN, CALENDAR_DOMAIN,
SERVICE_GET_EVENTS, 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_{identifier}",
EVENT_START_DATETIME: test_time, EVENT_START_DATETIME: test_time,
EVENT_END_DATETIME: test_time + timedelta(days=23), EVENT_END_DATETIME: test_time + timedelta(days=23),
}, },
blocking=True, blocking=True,
return_response=True, return_response=True,
) )
assert events == snapshot assert events == snapshot(
name=f"events.{serial_number}_auto_on_off_schedule_{identifier}"
)
@pytest.mark.parametrize( @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()) start_date = start_date.replace(tzinfo=dt_util.get_default_time_zone())
end_date = end_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) await async_init_integration(hass, mock_config_entry)
events = await hass.services.async_call( events = await hass.services.async_call(
CALENDAR_DOMAIN, CALENDAR_DOMAIN,
SERVICE_GET_EVENTS, 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_START_DATETIME: start_date,
EVENT_END_DATETIME: end_date, EVENT_END_DATETIME: end_date,
}, },
@ -123,7 +125,9 @@ async def test_no_calendar_events_global_disable(
) -> None: ) -> None:
"""Assert no events when global auto on/off is disabled.""" """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()) test_time = datetime(2024, 1, 12, 11, tzinfo=dt_util.get_default_time_zone())
freezer.move_to(test_time) freezer.move_to(test_time)
@ -131,14 +135,16 @@ async def test_no_calendar_events_global_disable(
serial_number = mock_lamarzocco.serial_number 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 assert state
events = await hass.services.async_call( events = await hass.services.async_call(
CALENDAR_DOMAIN, CALENDAR_DOMAIN,
SERVICE_GET_EVENTS, 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_START_DATETIME: test_time,
EVENT_END_DATETIME: test_time + timedelta(days=23), EVENT_END_DATETIME: test_time + timedelta(days=23),
}, },

View File

@ -1,17 +1,26 @@
"""Test the La Marzocco config flow.""" """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.exceptions import AuthFail, RequestNotSuccessful
from lmcloud.models import LaMarzoccoDeviceInfo
from homeassistant import config_entries from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE
from homeassistant.components.lamarzocco.const import ( from homeassistant.components.lamarzocco.const import CONF_USE_BLUETOOTH, DOMAIN
CONF_MACHINE, from homeassistant.config_entries import (
CONF_USE_BLUETOOTH, SOURCE_BLUETOOTH,
DOMAIN, 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.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult, FlowResultType from homeassistant.data_entry_flow import FlowResult, FlowResultType
@ -21,7 +30,7 @@ from tests.common import MockConfigEntry
async def __do_successful_user_step( async def __do_successful_user_step(
hass: HomeAssistant, result: FlowResult hass: HomeAssistant, result: FlowResult, mock_cloud_client: MagicMock
) -> FlowResult: ) -> FlowResult:
"""Successfully configure the user step.""" """Successfully configure the user step."""
result2 = await hass.config_entries.flow.async_configure( 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( async def __do_sucessful_machine_selection_step(
hass: HomeAssistant, result2: FlowResult, mock_lamarzocco: MagicMock hass: HomeAssistant, result2: FlowResult, mock_device_info: LaMarzoccoDeviceInfo
) -> None: ) -> None:
"""Successfully configure the machine selection step.""" """Successfully configure the machine selection step."""
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], with patch(
{ "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection",
CONF_HOST: "192.168.1.1", return_value=True,
CONF_MACHINE: mock_lamarzocco.serial_number, ):
}, 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() await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == mock_lamarzocco.serial_number assert result3["title"] == "GS3"
assert result3["data"] == { assert result3["data"] == {
**USER_INPUT, **USER_INPUT,
CONF_HOST: "192.168.1.1", 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.""" """Test we get the form."""
result = await hass.config_entries.flow.async_init( 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["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
assert result["step_id"] == "user" assert result["step_id"] == "user"
result2 = await __do_successful_user_step(hass, result) result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1
async def test_form_abort_already_configured( async def test_form_abort_already_configured(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test we abort if already configured.""" """Test we abort if already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( 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["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
@ -98,7 +119,7 @@ async def test_form_abort_already_configured(
result2["flow_id"], result2["flow_id"],
{ {
CONF_HOST: "192.168.1.1", 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() await hass.async_block_till_done()
@ -108,13 +129,15 @@ async def test_form_abort_already_configured(
async def test_form_invalid_auth( async def test_form_invalid_auth(
hass: HomeAssistant, mock_lamarzocco: MagicMock hass: HomeAssistant,
mock_device_info: LaMarzoccoDeviceInfo,
mock_cloud_client: MagicMock,
) -> None: ) -> None:
"""Test invalid auth error.""" """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( 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( 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["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_auth"} 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 # test recovery from failure
mock_lamarzocco.get_all_machines.side_effect = None mock_cloud_client.get_customer_fleet.side_effect = None
result2 = await __do_successful_user_step(hass, result) result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
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_invalid_host( async def test_form_invalid_host(
hass: HomeAssistant, mock_lamarzocco: MagicMock hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
) -> None: ) -> None:
"""Test invalid auth error.""" """Test invalid auth error."""
result = await hass.config_entries.flow.async_init( 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["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
@ -148,38 +173,41 @@ async def test_form_invalid_host(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_lamarzocco.check_local_connection.return_value = False
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "machine_selection" assert result2["step_id"] == "machine_selection"
result3 = await hass.config_entries.flow.async_configure( with patch(
result2["flow_id"], "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection",
{ return_value=False,
CONF_HOST: "192.168.1.1", ):
CONF_MACHINE: mock_lamarzocco.serial_number, 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() await hass.async_block_till_done()
assert result3["type"] is FlowResultType.FORM assert result3["type"] is FlowResultType.FORM
assert result3["errors"] == {"host": "cannot_connect"} 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 # test recovery from failure
mock_lamarzocco.check_local_connection.return_value = True await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco)
async def test_form_cannot_connect( async def test_form_cannot_connect(
hass: HomeAssistant, mock_lamarzocco: MagicMock hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
) -> None: ) -> None:
"""Test cannot connect error.""" """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( 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( 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["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "no_machines"} 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
USER_INPUT, USER_INPUT,
@ -199,21 +227,26 @@ async def test_form_cannot_connect(
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"} 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 # test recovery from failure
mock_lamarzocco.get_all_machines.side_effect = None mock_cloud_client.get_customer_fleet.side_effect = None
mock_lamarzocco.get_all_machines.return_value = [ mock_cloud_client.get_customer_fleet.return_value = {
(mock_lamarzocco.serial_number, mock_lamarzocco.model_name) mock_device_info.serial_number: mock_device_info
] }
result2 = await __do_successful_user_step(hass, result) result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
async def test_reauth_flow( 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: ) -> None:
"""Test that the reauth flow.""" """Test that the reauth flow."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={ context={
@ -235,19 +268,21 @@ async def test_reauth_flow(
assert result2["type"] is FlowResultType.ABORT assert result2["type"] is FlowResultType.ABORT
await hass.async_block_till_done() await hass.async_block_till_done()
assert result2["reason"] == "reauth_successful" 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" assert mock_config_entry.data[CONF_PASSWORD] == "new_password"
async def test_bluetooth_discovery( async def test_bluetooth_discovery(
hass: HomeAssistant, mock_lamarzocco: MagicMock hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_cloud_client: MagicMock,
) -> None: ) -> None:
"""Test bluetooth discovery.""" """Test bluetooth discovery."""
service_info = get_bluetooth_service_info( 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( 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 assert result["type"] is FlowResultType.FORM
@ -260,82 +295,95 @@ async def test_bluetooth_discovery(
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "machine_selection" assert result2["step_id"] == "machine_selection"
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1
result3 = await hass.config_entries.flow.async_configure( with patch(
result2["flow_id"], "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection",
{ return_value=True,
CONF_HOST: "192.168.1.1", ):
}, result3 = await hass.config_entries.flow.async_configure(
) result2["flow_id"],
{
CONF_HOST: "192.168.1.1",
},
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == mock_lamarzocco.serial_number assert result3["title"] == "GS3"
assert result3["data"] == { assert result3["data"] == {
**USER_INPUT, **USER_INPUT,
CONF_HOST: "192.168.1.1", CONF_HOST: "192.168.1.1",
CONF_MACHINE: mock_lamarzocco.serial_number, CONF_MACHINE: mock_lamarzocco.serial_number,
CONF_NAME: service_info.name, CONF_NAME: "GS3",
CONF_MAC: "aa:bb:cc:dd:ee:ff", 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( 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: ) -> None:
"""Test bluetooth discovery errors.""" """Test bluetooth discovery errors."""
service_info = get_bluetooth_service_info( 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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH}, context={"source": SOURCE_BLUETOOTH},
data=service_info, data=service_info,
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user" 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
USER_INPUT, USER_INPUT,
) )
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "machine_not_found"} 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_cloud_client.get_customer_fleet.return_value = {
(mock_lamarzocco.serial_number, mock_lamarzocco.model_name) mock_device_info.serial_number: mock_device_info
] }
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
USER_INPUT, USER_INPUT,
) )
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "machine_selection" 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( with patch(
result2["flow_id"], "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection",
{ return_value=True,
CONF_HOST: "192.168.1.1", ):
}, result3 = await hass.config_entries.flow.async_configure(
) result2["flow_id"],
{
CONF_HOST: "192.168.1.1",
},
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == mock_lamarzocco.serial_number assert result3["title"] == "GS3"
assert result3["data"] == { assert result3["data"] == {
**USER_INPUT, **USER_INPUT,
CONF_HOST: "192.168.1.1", CONF_HOST: "192.168.1.1",
CONF_MACHINE: mock_lamarzocco.serial_number, CONF_MACHINE: mock_lamarzocco.serial_number,
CONF_NAME: service_info.name, CONF_NAME: "GS3",
CONF_MAC: "aa:bb:cc:dd:ee:ff", CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_MODEL: mock_lamarzocco.model,
CONF_TOKEN: "token",
} }

View File

@ -1,15 +1,19 @@
"""Test initialization of lamarzocco.""" """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 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.components.lamarzocco.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState 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.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 from tests.common import MockConfigEntry
@ -20,7 +24,9 @@ async def test_load_unload_config_entry(
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
) -> None: ) -> None:
"""Test loading and unloading the integration.""" """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 assert mock_config_entry.state is ConfigEntryState.LOADED
@ -36,11 +42,13 @@ async def test_config_entry_not_ready(
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
) -> None: ) -> None:
"""Test the La Marzocco configuration entry not ready.""" """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 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@ -50,11 +58,13 @@ async def test_invalid_auth(
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
) -> None: ) -> None:
"""Test auth error during setup.""" """Test auth error during setup."""
mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("") mock_lamarzocco.get_config.side_effect = AuthFail("")
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.SETUP_ERROR 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() flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1 assert len(flows) == 1
@ -68,27 +78,132 @@ async def test_invalid_auth(
assert flow["context"].get("entry_id") == mock_config_entry.entry_id 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( async def test_bluetooth_is_set_from_discovery(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
) -> None: ) -> None:
"""Assert we're not searching for a new BT device when we already found one previously.""" """Check we can fill a device from discovery info."""
# 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)
service_info = get_bluetooth_service_info( service_info = get_bluetooth_service_info(
mock_lamarzocco.model_name, mock_lamarzocco.serial_number mock_lamarzocco.model, mock_lamarzocco.serial_number
) )
with patch( with (
"homeassistant.components.lamarzocco.coordinator.async_discovered_service_info", patch(
return_value=[service_info], "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) 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_NAME] == service_info.name
assert mock_config_entry.data[CONF_MAC] == service_info.address 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

View File

@ -2,7 +2,13 @@
from unittest.mock import MagicMock 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 import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -15,17 +21,22 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er 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( async def test_coffee_boiler(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test the La Marzocco coffee temperature Number.""" """Test the La Marzocco coffee temperature Number."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"number.{serial_number}_coffee_target_temperature") state = hass.states.get(f"number.{serial_number}_coffee_target_temperature")
@ -47,35 +58,34 @@ async def test_coffee_boiler(
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
{ {
ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature", ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature",
ATTR_VALUE: 95, ATTR_VALUE: 94,
}, },
blocking=True, blocking=True,
) )
assert len(mock_lamarzocco.set_coffee_temp.mock_calls) == 1 assert len(mock_lamarzocco.set_temp.mock_calls) == 1
mock_lamarzocco.set_coffee_temp.assert_called_once_with( mock_lamarzocco.set_temp.assert_called_once_with(
temperature=95, ble_device=None boiler=BoilerType.COFFEE, temperature=94
) )
@pytest.mark.parametrize( @pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV, MachineModel.GS3_MP])
"device_fixture", [LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP]
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_name", "value", "func_name", "kwargs"), ("entity_name", "value", "func_name", "kwargs"),
[ [
( (
"steam_target_temperature", "steam_target_temperature",
131, 131,
"set_steam_temp", "set_temp",
{"temperature": 131, "ble_device": None}, {"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( async def test_gs3_exclusive(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
@ -85,7 +95,7 @@ async def test_gs3_exclusive(
kwargs: dict[str, float], kwargs: dict[str, float],
) -> None: ) -> None:
"""Test exclusive entities for GS3 AV/MP.""" """Test exclusive entities for GS3 AV/MP."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
func = getattr(mock_lamarzocco, func_name) func = getattr(mock_lamarzocco, func_name)
@ -118,14 +128,15 @@ async def test_gs3_exclusive(
@pytest.mark.parametrize( @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( async def test_gs3_exclusive_none(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Ensure GS3 exclusive is None for unsupported models.""" """Ensure GS3 exclusive is None for unsupported models."""
await async_init_integration(hass, mock_config_entry)
ENTITIES = ("steam_target_temperature", "tea_water_duration") ENTITIES = ("steam_target_temperature", "tea_water_duration")
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
@ -135,29 +146,50 @@ async def test_gs3_exclusive_none(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"device_fixture", [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI] "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI]
) )
@pytest.mark.parametrize( @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}), "prebrew_off_time",
("preinfusion_time", 7, {"off_time": 7000, "key": 1}), "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( async def test_pre_brew_infusion_numbers(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_name: str, entity_name: str,
function_name: str,
prebrew_mode: PrebrewMode,
value: float, value: float,
kwargs: dict[str, float], kwargs: dict[str, float],
) -> None: ) -> None:
"""Test the La Marzocco prebrew/-infusion sensors.""" """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 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) entry = entity_registry.async_get(state.entity_id)
assert entry assert entry
assert entry.device_id
assert entry == snapshot assert entry == snapshot
device = device_registry.async_get(entry.device_id)
assert device
# service call # service call
await hass.services.async_call( await hass.services.async_call(
NUMBER_DOMAIN, NUMBER_DOMAIN,
@ -185,43 +213,97 @@ async def test_pre_brew_infusion_numbers(
blocking=True, blocking=True,
) )
assert len(mock_lamarzocco.configure_prebrew.mock_calls) == 1 function = getattr(mock_lamarzocco, function_name)
mock_lamarzocco.configure_prebrew.assert_called_once_with(**kwargs) 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.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_name", "value", "function_name", "kwargs"), ("entity_name", "value", "prebrew_mode", "function_name", "kwargs"),
[ [
( (
"prebrew_off_time", "prebrew_off_time",
6, 6,
"configure_prebrew", PrebrewMode.PREBREW,
{"on_time": 3000, "off_time": 6000}, "set_prebrew_time",
{"prebrew_off_time": 6.0},
), ),
( (
"prebrew_on_time", "prebrew_on_time",
6, 6,
"configure_prebrew", PrebrewMode.PREBREW,
{"on_time": 6000, "off_time": 5000}, "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( async def test_pre_brew_infusion_key_numbers(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_name: str, entity_name: str,
value: float, value: float,
prebrew_mode: PrebrewMode,
function_name: str, function_name: str,
kwargs: dict[str, float], kwargs: dict[str, float],
) -> None: ) -> None:
"""Test the La Marzocco number sensors for GS3AV model.""" """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 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}") state = hass.states.get(f"number.{serial_number}_{entity_name}")
assert state is None 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}") state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}")
assert state assert state
assert state == snapshot(name=f"{serial_number}_{entity_name}_key_{key}-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 kwargs["key"] = key
assert len(func.mock_calls) == key assert len(func.mock_calls) == key.value
func.assert_called_with(**kwargs) 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( async def test_disabled_entites(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test the La Marzocco prebrew/-infusion sensors for GS3AV model.""" """Test the La Marzocco prebrew/-infusion sensors for GS3AV model."""
await async_init_integration(hass, mock_config_entry)
ENTITIES = ( ENTITIES = (
"prebrew_off_time", "prebrew_off_time",
"prebrew_on_time", "prebrew_on_time",
@ -269,21 +352,22 @@ async def test_disabled_entites(
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
for entity_name in ENTITIES: 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}") state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}")
assert state is None assert state is None
@pytest.mark.parametrize( @pytest.mark.parametrize(
"device_fixture", "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, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Assert not existing key entities.""" """Assert not existing key entities."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
for entity in ( for entity in (
@ -292,42 +376,6 @@ async def test_not_existing_key_entites(
"preinfusion_time", "preinfusion_time",
"set_dose", "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}") state = hass.states.get(f"number.{serial_number}_{entity}_key_{key}")
assert state is None 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

View File

@ -2,7 +2,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.const import LaMarzoccoModel from lmcloud.const import MachineModel, PrebrewMode, SteamLevel
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -18,7 +18,7 @@ from homeassistant.helpers import entity_registry as er
pytestmark = pytest.mark.usefixtures("init_integration") 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( async def test_steam_boiler_level(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
@ -44,18 +44,17 @@ async def test_steam_boiler_level(
SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION,
{ {
ATTR_ENTITY_ID: f"select.{serial_number}_steam_level", ATTR_ENTITY_ID: f"select.{serial_number}_steam_level",
ATTR_OPTION: "1", ATTR_OPTION: "2",
}, },
blocking=True, blocking=True,
) )
assert len(mock_lamarzocco.set_steam_level.mock_calls) == 1 mock_lamarzocco.set_steam_level.assert_called_once_with(level=SteamLevel.LEVEL_2)
mock_lamarzocco.set_steam_level.assert_called_once_with(1, None)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"device_fixture", "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( async def test_steam_boiler_level_none(
hass: HomeAssistant, hass: HomeAssistant,
@ -70,7 +69,7 @@ async def test_steam_boiler_level_none(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"device_fixture", "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( async def test_pre_brew_infusion_select(
hass: HomeAssistant, hass: HomeAssistant,
@ -97,20 +96,17 @@ async def test_pre_brew_infusion_select(
SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION,
{ {
ATTR_ENTITY_ID: f"select.{serial_number}_prebrew_infusion_mode", ATTR_ENTITY_ID: f"select.{serial_number}_prebrew_infusion_mode",
ATTR_OPTION: "preinfusion", ATTR_OPTION: "prebrew",
}, },
blocking=True, blocking=True,
) )
assert len(mock_lamarzocco.select_pre_brew_infusion_mode.mock_calls) == 1 mock_lamarzocco.set_prebrew_mode.assert_called_once_with(mode=PrebrewMode.PREBREW)
mock_lamarzocco.select_pre_brew_infusion_mode.assert_called_once_with(
mode="Preinfusion"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"device_fixture", "device_fixture",
[LaMarzoccoModel.GS3_MP], [MachineModel.GS3_MP],
) )
async def test_pre_brew_infusion_select_none( async def test_pre_brew_infusion_select_none(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -5,7 +5,6 @@ from unittest.mock import MagicMock
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.components.switch import ( from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN, DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@ -15,35 +14,39 @@ from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er 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( @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", "set_power",
(True,), ),
(False,), (
"_steam_boiler",
"set_steam",
), ),
("_steam_boiler", "set_steam", (True, None), (False, None)),
], ],
) )
async def test_switches( async def test_switches(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_name: str, entity_name: str,
method_name: str, method_name: str,
args_on: tuple,
args_off: tuple,
) -> None: ) -> None:
"""Test the La Marzocco switches.""" """Test the La Marzocco switches."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
control_fn = getattr(mock_lamarzocco, method_name) control_fn = getattr(mock_lamarzocco, method_name)
@ -66,7 +69,7 @@ async def test_switches(
) )
assert len(control_fn.mock_calls) == 1 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( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
@ -78,18 +81,21 @@ async def test_switches(
) )
assert len(control_fn.mock_calls) == 2 assert len(control_fn.mock_calls) == 2
control_fn.assert_called_with(*args_on) control_fn.assert_called_with(True)
async def test_device( async def test_device(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test the device for one switch.""" """Test the device for one switch."""
await async_init_integration(hass, mock_config_entry)
state = hass.states.get(f"switch.{mock_lamarzocco.serial_number}") state = hass.states.get(f"switch.{mock_lamarzocco.serial_number}")
assert state assert state
@ -100,26 +106,3 @@ async def test_device(
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device assert device
assert device == snapshot 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)

View File

@ -2,7 +2,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.const import LaMarzoccoUpdateableComponent from lmcloud.const import FirmwareType
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -18,8 +18,8 @@ pytestmark = pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_name", "component"), ("entity_name", "component"),
[ [
("machine_firmware", LaMarzoccoUpdateableComponent.MACHINE), ("machine_firmware", FirmwareType.MACHINE),
("gateway_firmware", LaMarzoccoUpdateableComponent.GATEWAY), ("gateway_firmware", FirmwareType.GATEWAY),
], ],
) )
async def test_update_entites( async def test_update_entites(
@ -28,7 +28,7 @@ async def test_update_entites(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_name: str, entity_name: str,
component: LaMarzoccoUpdateableComponent, component: FirmwareType,
) -> None: ) -> None:
"""Test the La Marzocco update entities.""" """Test the La Marzocco update entities."""