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."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
import logging
from .const import DOMAIN
from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient
from lmcloud.client_cloud import LaMarzoccoCloudClient
from lmcloud.client_local import LaMarzoccoLocalClient
from lmcloud.const import BT_MODEL_PREFIXES, FirmwareType
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
from packaging import version
from homeassistant.components.bluetooth import async_discovered_service_info
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PASSWORD,
CONF_TOKEN,
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.httpx_client import get_async_client
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import LaMarzoccoUpdateCoordinator
PLATFORMS = [
@ -18,15 +39,89 @@ PLATFORMS = [
Platform.UPDATE,
]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up La Marzocco as config entry."""
coordinator = LaMarzoccoUpdateCoordinator(hass)
assert entry.unique_id
serial = entry.unique_id
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
# initialize local API
local_client: LaMarzoccoLocalClient | None = None
if (host := entry.data.get(CONF_HOST)) is not None:
_LOGGER.debug("Initializing local API")
local_client = LaMarzoccoLocalClient(
host=host,
local_bearer=entry.data[CONF_TOKEN],
client=get_async_client(hass),
)
# initialize Bluetooth
bluetooth_client: LaMarzoccoBluetoothClient | None = None
if entry.options.get(CONF_USE_BLUETOOTH, True):
def bluetooth_configured() -> bool:
return entry.data.get(CONF_MAC, "") and entry.data.get(CONF_NAME, "")
if not bluetooth_configured():
for discovery_info in async_discovered_service_info(hass):
if (
(name := discovery_info.name)
and name.startswith(BT_MODEL_PREFIXES)
and name.split("_")[1] == serial
):
_LOGGER.debug("Found Bluetooth device, configuring with Bluetooth")
# found a device, add MAC address to config entry
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_MAC: discovery_info.address,
CONF_NAME: discovery_info.name,
},
)
break
if bluetooth_configured():
_LOGGER.debug("Initializing Bluetooth device")
bluetooth_client = LaMarzoccoBluetoothClient(
username=entry.data[CONF_USERNAME],
serial_number=serial,
token=entry.data[CONF_TOKEN],
address_or_ble_device=entry.data[CONF_MAC],
)
coordinator = LaMarzoccoUpdateCoordinator(
hass=hass,
local_client=local_client,
cloud_client=cloud_client,
bluetooth_client=bluetooth_client,
)
await coordinator.async_setup()
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version
if version.parse(gateway_version) < version.parse("v3.5-rc5"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(
hass,
DOMAIN,
"unsupported_gateway_firmware",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="unsupported_gateway_firmware",
translation_placeholders={"gateway_version": gateway_version},
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
@ -39,10 +134,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate config entry."""
if entry.version > 2:
# guard against downgrade from a future version
return False
if entry.version == 1:
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
try:
fleet = await cloud_client.get_customer_fleet()
except (AuthFail, RequestNotSuccessful) as exc:
_LOGGER.error("Migration failed with error %s", exc)
return False
assert entry.unique_id is not None
device = fleet[entry.unique_id]
v2_data = {
CONF_USERNAME: entry.data[CONF_USERNAME],
CONF_PASSWORD: entry.data[CONF_PASSWORD],
CONF_MODEL: device.model,
CONF_NAME: device.name,
CONF_TOKEN: device.communication_key,
}
if CONF_HOST in entry.data:
v2_data[CONF_HOST] = entry.data[CONF_HOST]
if CONF_MAC in entry.data:
v2_data[CONF_MAC] = entry.data[CONF_MAC]
hass.config_entries.async_update_entry(
entry,
data=v2_data,
version=2,
)
_LOGGER.debug("Migrated La Marzocco config entry to version 2")
return True

View File

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

View File

@ -4,7 +4,7 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from lmcloud import LMCloud as LaMarzoccoClient
from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
@ -22,14 +22,14 @@ class LaMarzoccoButtonEntityDescription(
):
"""Description of a La Marzocco button."""
press_fn: Callable[[LaMarzoccoClient], Coroutine[Any, Any, None]]
press_fn: Callable[[LaMarzoccoMachine], Coroutine[Any, Any, None]]
ENTITIES: tuple[LaMarzoccoButtonEntityDescription, ...] = (
LaMarzoccoButtonEntityDescription(
key="start_backflush",
translation_key="start_backflush",
press_fn=lambda lm: lm.start_backflush(),
press_fn=lambda machine: machine.start_backflush(),
),
)
@ -56,4 +56,4 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity):
async def async_press(self) -> None:
"""Press button."""
await self.entity_description.press_fn(self.coordinator.lm)
await self.entity_description.press_fn(self.coordinator.device)

View File

@ -3,6 +3,8 @@
from collections.abc import Iterator
from datetime import datetime, timedelta
from lmcloud.models import LaMarzoccoWakeUpSleepEntry
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@ -10,10 +12,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoBaseEntity
CALENDAR_KEY = "auto_on_off_schedule"
DAY_OF_WEEK = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
async def async_setup_entry(
hass: HomeAssistant,
@ -23,7 +36,10 @@ async def async_setup_entry(
"""Set up switch entities and services."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities([LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY)])
async_add_entities(
LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY, wake_up_sleep_entry)
for wake_up_sleep_entry in coordinator.device.config.wake_up_sleep_entries.values()
)
class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
@ -31,6 +47,17 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
_attr_translation_key = CALENDAR_KEY
def __init__(
self,
coordinator: LaMarzoccoUpdateCoordinator,
key: str,
wake_up_sleep_entry: LaMarzoccoWakeUpSleepEntry,
) -> None:
"""Set up calendar."""
super().__init__(coordinator, f"{key}_{wake_up_sleep_entry.entry_id}")
self.wake_up_sleep_entry = wake_up_sleep_entry
self._attr_translation_placeholders = {"id": wake_up_sleep_entry.entry_id}
@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
@ -85,29 +112,36 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
"""Return calendar event for a given weekday."""
# check first if auto/on off is turned on in general
# because could still be on for that day but disabled
if self.coordinator.lm.current_status["global_auto"] != "Enabled":
if not self.wake_up_sleep_entry.enabled:
return None
# parse the schedule for the day
schedule_day = self.coordinator.lm.schedule[date.weekday()]
if schedule_day["enable"] == "Disabled":
if DAY_OF_WEEK[date.weekday()] not in self.wake_up_sleep_entry.days:
return None
hour_on, minute_on = schedule_day["on"].split(":")
hour_off, minute_off = schedule_day["off"].split(":")
hour_on, minute_on = self.wake_up_sleep_entry.time_on.split(":")
hour_off, minute_off = self.wake_up_sleep_entry.time_off.split(":")
# if off time is 24:00, then it means the off time is the next day
# only for legacy schedules
day_offset = 0
if hour_off == "24":
day_offset = 1
hour_off = "0"
end_date = date.replace(
hour=int(hour_off),
minute=int(minute_off),
)
end_date += timedelta(days=day_offset)
return CalendarEvent(
start=date.replace(
hour=int(hour_on),
minute=int(minute_on),
second=0,
microsecond=0,
),
end=date.replace(
hour=int(hour_off),
minute=int(minute_off),
second=0,
microsecond=0,
),
end=end_date,
summary=f"Machine {self.coordinator.config_entry.title} on",
description="Machine is scheduled to turn on at the start time and off at the end time",
)

View File

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

View File

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

View File

@ -3,133 +3,108 @@
from collections.abc import Callable, Coroutine
from datetime import timedelta
import logging
from time import time
from typing import Any
from bleak.backends.device import BLEDevice
from lmcloud import LMCloud as LaMarzoccoClient
from lmcloud.const import BT_MODEL_NAMES
from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient
from lmcloud.client_cloud import LaMarzoccoCloudClient
from lmcloud.client_local import LaMarzoccoLocalClient
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.components.bluetooth import (
async_ble_device_from_address,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_USERNAME
from homeassistant.const import CONF_MODEL, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
FIRMWARE_UPDATE_INTERVAL = 3600
STATISTICS_UPDATE_INTERVAL = 300
_LOGGER = logging.getLogger(__name__)
NAME_PREFIXES = tuple(BT_MODEL_NAMES)
class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to handle fetching data from the La Marzocco API centrally."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant) -> None:
def __init__(
self,
hass: HomeAssistant,
cloud_client: LaMarzoccoCloudClient,
local_client: LaMarzoccoLocalClient | None,
bluetooth_client: LaMarzoccoBluetoothClient | None,
) -> None:
"""Initialize coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
self.lm = LaMarzoccoClient(
callback_websocket_notify=self.async_update_listeners,
)
self.local_connection_configured = (
self.config_entry.data.get(CONF_HOST) is not None
)
self._use_bluetooth = False
self.local_connection_configured = local_client is not None
async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
if not self.lm.initialized:
await self._async_init_client()
await self._async_handle_request(
self.lm.update_local_machine_status, force_update=True
assert self.config_entry.unique_id
self.device = LaMarzoccoMachine(
model=self.config_entry.data[CONF_MODEL],
serial_number=self.config_entry.unique_id,
name=self.config_entry.data[CONF_NAME],
cloud_client=cloud_client,
local_client=local_client,
bluetooth_client=bluetooth_client,
)
_LOGGER.debug("Current status: %s", str(self.lm.current_status))
self._last_firmware_data_update: float | None = None
self._last_statistics_data_update: float | None = None
self._local_client = local_client
async def _async_init_client(self) -> None:
"""Initialize the La Marzocco Client."""
# Initialize cloud API
_LOGGER.debug("Initializing Cloud API")
await self._async_handle_request(
self.lm.init_cloud_api,
credentials=self.config_entry.data,
machine_serial=self.config_entry.data[CONF_MACHINE],
)
_LOGGER.debug("Model name: %s", self.lm.model_name)
# initialize local API
if (host := self.config_entry.data.get(CONF_HOST)) is not None:
_LOGGER.debug("Initializing local API")
await self.lm.init_local_api(
host=host,
client=get_async_client(self.hass),
)
_LOGGER.debug("Init WebSocket in Background Task")
async def async_setup(self) -> None:
"""Set up the coordinator."""
if self._local_client is not None:
_LOGGER.debug("Init WebSocket in background task")
self.config_entry.async_create_background_task(
hass=self.hass,
target=self.lm.lm_local_api.websocket_connect(
callback=self.lm.on_websocket_message_received,
use_sigterm_handler=False,
target=self.device.websocket_connect(
notify_callback=lambda: self.async_set_updated_data(None)
),
name="lm_websocket_task",
)
# initialize Bluetooth
if self.config_entry.options.get(CONF_USE_BLUETOOTH, True):
async def websocket_close(_: Any | None = None) -> None:
if (
self._local_client is not None
and self._local_client.websocket is not None
and self._local_client.websocket.open
):
self._local_client.terminating = True
await self._local_client.websocket.close()
def bluetooth_configured() -> bool:
return self.config_entry.data.get(
CONF_MAC, ""
) and self.config_entry.data.get(CONF_NAME, "")
if not bluetooth_configured():
machine = self.config_entry.data[CONF_MACHINE]
for discovery_info in async_discovered_service_info(self.hass):
if (
(name := discovery_info.name)
and name.startswith(NAME_PREFIXES)
and name.split("_")[1] == machine
):
_LOGGER.debug(
"Found Bluetooth device, configuring with Bluetooth"
)
# found a device, add MAC address to config entry
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
CONF_MAC: discovery_info.address,
CONF_NAME: discovery_info.name,
},
)
break
if bluetooth_configured():
# config entry contains BT config
_LOGGER.debug("Initializing with known Bluetooth device")
await self.lm.init_bluetooth_with_known_device(
self.config_entry.data[CONF_USERNAME],
self.config_entry.data.get(CONF_MAC, ""),
self.config_entry.data.get(CONF_NAME, ""),
self.config_entry.async_on_unload(
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, websocket_close
)
self._use_bluetooth = True
)
self.config_entry.async_on_unload(websocket_close)
self.lm.initialized = True
async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self._async_handle_request(self.device.get_config)
if (
self._last_firmware_data_update is None
or (self._last_firmware_data_update + FIRMWARE_UPDATE_INTERVAL) < time()
):
await self._async_handle_request(self.device.get_firmware)
self._last_firmware_data_update = time()
if (
self._last_statistics_data_update is None
or (self._last_statistics_data_update + STATISTICS_UPDATE_INTERVAL) < time()
):
await self._async_handle_request(self.device.get_statistics)
self._last_statistics_data_update = time()
_LOGGER.debug("Current status: %s", str(self.device.config))
async def _async_handle_request[**_P](
self,
@ -137,9 +112,8 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
*args: _P.args,
**kwargs: _P.kwargs,
) -> None:
"""Handle a request to the API."""
try:
await func(*args, **kwargs)
await func()
except AuthFail as ex:
msg = "Authentication failed."
_LOGGER.debug(msg, exc_info=True)
@ -147,15 +121,3 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
except RequestNotSuccessful as ex:
_LOGGER.debug(ex, exc_info=True)
raise UpdateFailed(f"Querying API failed. Error: {ex}") from ex
def async_get_ble_device(self) -> BLEDevice | None:
"""Get a Bleak Client for the machine."""
# according to HA best practices, we should not reuse the same client
# get a new BLE device from hass and init a new Bleak Client with it
if not self._use_bluetooth:
return None
return async_ble_device_from_address(
self.hass,
self.lm.lm_bluetooth.address,
)

View File

@ -2,7 +2,10 @@
from __future__ import annotations
from typing import Any
from dataclasses import asdict
from typing import Any, TypedDict
from lmcloud.const import FirmwareType
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
@ -13,31 +16,30 @@ from .coordinator import LaMarzoccoUpdateCoordinator
TO_REDACT = {
"serial_number",
"machine_sn",
}
class DiagnosticsData(TypedDict):
"""Diagnostic data for La Marzocco."""
model: str
config: dict[str, Any]
firmware: list[dict[FirmwareType, dict[str, Any]]]
statistics: dict[str, Any]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: LaMarzoccoUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: LaMarzoccoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
device = coordinator.device
# collect all data sources
data = {}
data["current_status"] = coordinator.lm.current_status
data["machine_info"] = coordinator.lm.machine_info
data["config"] = coordinator.lm.config
data["statistics"] = {"stats": coordinator.lm.statistics} # wrap to satisfy mypy
diagnostics_data = DiagnosticsData(
model=device.model,
config=asdict(device.config),
firmware=[{key: asdict(firmware)} for key, firmware in device.firmware.items()],
statistics=asdict(device.statistics),
)
# build a firmware section
data["firmware"] = {
"machine": {
"version": coordinator.lm.firmware_version,
"latest_version": coordinator.lm.latest_firmware_version,
},
"gateway": {
"version": coordinator.lm.gateway_version,
"latest_version": coordinator.lm.latest_gateway_version,
},
}
return async_redact_data(data, TO_REDACT)
return async_redact_data(diagnostics_data, TO_REDACT)

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,8 @@
from collections.abc import Callable
from dataclasses import dataclass
from lmcloud import LMCloud as LaMarzoccoClient
from lmcloud.const import BoilerType, PhysicalKey
from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -22,12 +23,11 @@ from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
@dataclass(frozen=True, kw_only=True)
class LaMarzoccoSensorEntityDescription(
LaMarzoccoEntityDescription,
SensorEntityDescription,
LaMarzoccoEntityDescription, SensorEntityDescription
):
"""Description of a La Marzocco sensor."""
value_fn: Callable[[LaMarzoccoClient], float | int]
value_fn: Callable[[LaMarzoccoMachine], float | int]
ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
@ -36,7 +36,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
translation_key="drink_stats_coffee",
native_unit_of_measurement="drinks",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda lm: lm.current_status.get("drinks_k1", 0),
value_fn=lambda device: device.statistics.drink_stats.get(PhysicalKey.A, 0),
available_fn=lambda device: len(device.statistics.drink_stats) > 0,
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoSensorEntityDescription(
@ -44,7 +45,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
translation_key="drink_stats_flushing",
native_unit_of_measurement="drinks",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda lm: lm.current_status.get("total_flushing", 0),
value_fn=lambda device: device.statistics.total_flushes,
available_fn=lambda device: len(device.statistics.drink_stats) > 0,
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoSensorEntityDescription(
@ -53,8 +55,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
value_fn=lambda lm: lm.current_status.get("brew_active_duration", 0),
available_fn=lambda lm: lm.websocket_connected,
value_fn=lambda device: device.config.brew_active_duration,
available_fn=lambda device: device.websocket_connected,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda coordinator: coordinator.local_connection_configured,
),
@ -65,7 +67,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
value_fn=lambda lm: lm.current_status.get("coffee_temp", 0),
value_fn=lambda device: device.config.boilers[
BoilerType.COFFEE
].current_temperature,
),
LaMarzoccoSensorEntityDescription(
key="current_temp_steam",
@ -74,7 +78,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
value_fn=lambda lm: lm.current_status.get("steam_temp", 0),
value_fn=lambda device: device.config.boilers[
BoilerType.STEAM
].current_temperature,
),
)
@ -102,4 +108,4 @@ class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity):
@property
def native_value(self) -> int | float:
"""State of the sensor."""
return self.entity_description.value_fn(self.coordinator.lm)
return self.entity_description.value_fn(self.coordinator.device)

View File

@ -68,7 +68,7 @@
},
"calendar": {
"auto_on_off_schedule": {
"name": "Auto on/off schedule"
"name": "Auto on/off schedule ({id})"
}
},
"number": {
@ -139,9 +139,6 @@
}
},
"switch": {
"auto_on_off": {
"name": "Auto on/off"
},
"steam_boiler": {
"name": "Steam boiler"
}
@ -154,5 +151,11 @@
"name": "Gateway firmware"
}
}
},
"issues": {
"unsupported_gateway_firmware": {
"title": "Unsupported gateway firmware",
"description": "Gateway firmware {gateway_version} is no longer supported by this integration, please update."
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,23 @@
"""Lamarzocco session fixtures."""
from collections.abc import Callable
import json
from unittest.mock import MagicMock, patch
from lmcloud.const import LaMarzoccoModel
from bleak.backends.device import BLEDevice
from lmcloud.const import FirmwareType, MachineModel, SteamLevel
from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoDeviceInfo
import pytest
from typing_extensions import Generator
from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN
from homeassistant.core import HomeAssistant
from . import MODEL_DICT, USER_INPUT, async_init_integration
from . import SERIAL_DICT, USER_INPUT, async_init_integration
from tests.common import (
MockConfigEntry,
load_json_array_fixture,
load_json_object_fixture,
)
from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture
@pytest.fixture
@ -27,12 +28,13 @@ def mock_config_entry(
entry = MockConfigEntry(
title="My LaMarzocco",
domain=DOMAIN,
version=2,
data=USER_INPUT
| {
CONF_MACHINE: mock_lamarzocco.serial_number,
CONF_MODEL: mock_lamarzocco.model,
CONF_HOST: "host",
CONF_NAME: "name",
CONF_MAC: "mac",
CONF_TOKEN: "token",
CONF_NAME: "GS3",
},
unique_id=mock_lamarzocco.serial_number,
)
@ -44,77 +46,96 @@ def mock_config_entry(
async def init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock
) -> MockConfigEntry:
"""Set up the LaMetric integration for testing."""
"""Set up the La Marzocco integration for testing."""
await async_init_integration(hass, mock_config_entry)
return mock_config_entry
@pytest.fixture
def device_fixture() -> LaMarzoccoModel:
def device_fixture() -> MachineModel:
"""Return the device fixture for a specific device."""
return LaMarzoccoModel.GS3_AV
return MachineModel.GS3_AV
@pytest.fixture
def mock_lamarzocco(device_fixture: LaMarzoccoModel) -> Generator[MagicMock]:
"""Return a mocked LM client."""
model_name = device_fixture
def mock_device_info() -> LaMarzoccoDeviceInfo:
"""Return a mocked La Marzocco device info."""
return LaMarzoccoDeviceInfo(
model=MachineModel.GS3_AV,
serial_number="GS01234",
name="GS3",
communication_key="token",
)
(serial_number, true_model_name) = MODEL_DICT[model_name]
@pytest.fixture
def mock_cloud_client(
mock_device_info: LaMarzoccoDeviceInfo,
) -> Generator[MagicMock]:
"""Return a mocked LM cloud client."""
with (
patch(
"homeassistant.components.lamarzocco.config_flow.LaMarzoccoCloudClient",
autospec=True,
) as cloud_client,
patch(
"homeassistant.components.lamarzocco.LaMarzoccoCloudClient",
new=cloud_client,
),
):
client = cloud_client.return_value
client.get_customer_fleet.return_value = {
mock_device_info.serial_number: mock_device_info
}
yield client
@pytest.fixture
def mock_lamarzocco(device_fixture: MachineModel) -> Generator[MagicMock]:
"""Return a mocked LM client."""
model = device_fixture
serial_number = SERIAL_DICT[model]
dummy_machine = LaMarzoccoMachine(
model=model,
serial_number=serial_number,
name=serial_number,
)
config = load_json_object_fixture("config.json", DOMAIN)
statistics = json.loads(load_fixture("statistics.json", DOMAIN))
dummy_machine.parse_config(config)
dummy_machine.parse_statistics(statistics)
with (
patch(
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient",
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine",
autospec=True,
) as lamarzocco_mock,
patch(
"homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient",
new=lamarzocco_mock,
),
):
lamarzocco = lamarzocco_mock.return_value
lamarzocco.machine_info = {
"machine_name": serial_number,
"serial_number": serial_number,
}
lamarzocco.name = dummy_machine.name
lamarzocco.model = dummy_machine.model
lamarzocco.serial_number = dummy_machine.serial_number
lamarzocco.full_model_name = dummy_machine.full_model_name
lamarzocco.config = dummy_machine.config
lamarzocco.statistics = dummy_machine.statistics
lamarzocco.firmware = dummy_machine.firmware
lamarzocco.steam_level = SteamLevel.LEVEL_1
lamarzocco.model_name = model_name
lamarzocco.true_model_name = true_model_name
lamarzocco.machine_name = serial_number
lamarzocco.serial_number = serial_number
lamarzocco.firmware_version = "1.1"
lamarzocco.latest_firmware_version = "1.2"
lamarzocco.gateway_version = "v2.2-rc0"
lamarzocco.latest_gateway_version = "v3.1-rc4"
lamarzocco.update_firmware.return_value = True
lamarzocco.current_status = load_json_object_fixture(
"current_status.json", DOMAIN
)
lamarzocco.config = load_json_object_fixture("config.json", DOMAIN)
lamarzocco.statistics = load_json_array_fixture("statistics.json", DOMAIN)
lamarzocco.schedule = load_json_array_fixture("schedule.json", DOMAIN)
lamarzocco.get_all_machines.return_value = [
(serial_number, model_name),
]
lamarzocco.check_local_connection.return_value = True
lamarzocco.initialized = False
lamarzocco.websocket_connected = True
lamarzocco.firmware[FirmwareType.GATEWAY].latest_version = "v3.5-rc3"
lamarzocco.firmware[FirmwareType.MACHINE].latest_version = "1.55"
async def websocket_connect_mock(
callback: MagicMock, use_sigterm_handler: MagicMock
notify_callback: Callable | None,
) -> None:
"""Mock the websocket connect method."""
return None
lamarzocco.lm_local_api.websocket_connect = websocket_connect_mock
lamarzocco.lm_bluetooth = MagicMock()
lamarzocco.lm_bluetooth.address = "AA:BB:CC:DD:EE:FF"
lamarzocco.websocket_connect = websocket_connect_mock
yield lamarzocco
@ -133,3 +154,11 @@ def remove_local_connection(
@pytest.fixture(autouse=True)
def mock_bluetooth(enable_bluetooth: None) -> None:
"""Auto mock bluetooth."""
@pytest.fixture
def mock_ble_device() -> BLEDevice:
"""Return a mock BLE device."""
return BLEDevice(
"00:00:00:00:00:00", "GS_GS01234", details={"path": "path"}, rssi=50
)

View File

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

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
# name: test_calendar_edge_cases[start_date0-end_date0]
dict({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
@ -15,7 +15,7 @@
# ---
# name: test_calendar_edge_cases[start_date1-end_date1]
dict({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
@ -29,7 +29,7 @@
# ---
# name: test_calendar_edge_cases[start_date2-end_date2]
dict({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
@ -43,7 +43,7 @@
# ---
# name: test_calendar_edge_cases[start_date3-end_date3]
dict({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
@ -57,7 +57,7 @@
# ---
# name: test_calendar_edge_cases[start_date4-end_date4]
dict({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([
]),
}),
@ -65,7 +65,7 @@
# ---
# name: test_calendar_edge_cases[start_date5-end_date5]
dict({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
@ -83,26 +83,7 @@
}),
})
# ---
# name: test_calendar_events
StateSnapshot({
'attributes': ReadOnlyDict({
'all_day': False,
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end_time': '2024-01-13 23:00:00',
'friendly_name': 'GS01234 Auto on/off schedule',
'location': '',
'message': 'Machine My LaMarzocco on',
'start_time': '2024-01-13 10:00:00',
}),
'context': <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
# name: test_calendar_events[entry.GS01234_auto_on_off_schedule_axfz5bj]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@ -114,7 +95,7 @@
'disabled_by': None,
'domain': 'calendar',
'entity_category': None,
'entity_id': 'calendar.gs01234_auto_on_off_schedule',
'entity_id': 'calendar.gs01234_auto_on_off_schedule_axfz5bj',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@ -126,86 +107,267 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Auto on/off schedule',
'original_name': 'Auto on/off schedule (aXFz5bJ)',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'auto_on_off_schedule',
'unique_id': 'GS01234_auto_on_off_schedule',
'unique_id': 'GS01234_auto_on_off_schedule_aXFz5bJ',
'unit_of_measurement': None,
})
# ---
# name: test_calendar_events.2
# name: test_calendar_events[entry.GS01234_auto_on_off_schedule_os2oswx]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <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({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({
'events': list([
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-13T23:00:00-08:00',
'start': '2024-01-13T10:00:00-08:00',
'end': '2024-01-14T07:30:00-08:00',
'start': '2024-01-14T07:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-17T13:00:00-08:00',
'start': '2024-01-17T08:00:00-08:00',
'end': '2024-01-21T07:30:00-08:00',
'start': '2024-01-21T07:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-19T09:00:00-08:00',
'start': '2024-01-19T06:00:00-08:00',
'end': '2024-01-28T07:30:00-08:00',
'start': '2024-01-28T07:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-20T23:00:00-08:00',
'start': '2024-01-20T10:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-24T13:00:00-08:00',
'start': '2024-01-24T08:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-26T09:00:00-08:00',
'start': '2024-01-26T06:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-27T23:00:00-08:00',
'start': '2024-01-27T10:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-31T13:00:00-08:00',
'start': '2024-01-31T08:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-02-02T09:00:00-08:00',
'start': '2024-02-02T06:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-02-03T23:00:00-08:00',
'start': '2024-02-03T10:00:00-08:00',
'end': '2024-02-04T07:30:00-08:00',
'start': '2024-02-04T07:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
]),
}),
})
# ---
# name: test_calendar_events[events.GS01234_auto_on_off_schedule_os2oswx]
dict({
'calendar.gs01234_auto_on_off_schedule_os2oswx': dict({
'events': list([
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-13T00:00:00-08:00',
'start': '2024-01-12T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-14T00:00:00-08:00',
'start': '2024-01-13T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-15T00:00:00-08:00',
'start': '2024-01-14T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-16T00:00:00-08:00',
'start': '2024-01-15T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-17T00:00:00-08:00',
'start': '2024-01-16T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-18T00:00:00-08:00',
'start': '2024-01-17T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-19T00:00:00-08:00',
'start': '2024-01-18T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-20T00:00:00-08:00',
'start': '2024-01-19T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-21T00:00:00-08:00',
'start': '2024-01-20T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-22T00:00:00-08:00',
'start': '2024-01-21T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-23T00:00:00-08:00',
'start': '2024-01-22T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-24T00:00:00-08:00',
'start': '2024-01-23T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-25T00:00:00-08:00',
'start': '2024-01-24T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-26T00:00:00-08:00',
'start': '2024-01-25T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-27T00:00:00-08:00',
'start': '2024-01-26T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-28T00:00:00-08:00',
'start': '2024-01-27T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-29T00:00:00-08:00',
'start': '2024-01-28T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-30T00:00:00-08:00',
'start': '2024-01-29T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-01-31T00:00:00-08:00',
'start': '2024-01-30T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-02-01T00:00:00-08:00',
'start': '2024-01-31T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-02-02T00:00:00-08:00',
'start': '2024-02-01T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-02-03T00:00:00-08:00',
'start': '2024-02-02T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
dict({
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end': '2024-02-04T00:00:00-08:00',
'start': '2024-02-03T22:00:00-08:00',
'summary': 'Machine My LaMarzocco on',
}),
]),
}),
})
# ---
# name: test_calendar_events[state.GS01234_auto_on_off_schedule_axfz5bj]
StateSnapshot({
'attributes': ReadOnlyDict({
'all_day': False,
'description': 'Machine is scheduled to turn on at the start time and off at the end time',
'end_time': '2024-01-14 07:30:00',
'friendly_name': 'GS01234 Auto on/off schedule (aXFz5bJ)',
'location': '',
'message': 'Machine My LaMarzocco on',
'start_time': '2024-01-14 07:00:00',
}),
'context': <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
dict({
'calendar.gs01234_auto_on_off_schedule': dict({
'calendar.gs01234_auto_on_off_schedule_os2oswx': dict({
'events': list([
]),
}),

View File

@ -2,297 +2,107 @@
# name: test_diagnostics
dict({
'config': dict({
'boilerTargetTemperature': dict({
'CoffeeBoiler1': 95,
'SteamBoiler': 123.9000015258789,
}),
'boilers': list([
dict({
'current': 123.80000305175781,
'id': 'SteamBoiler',
'isEnabled': True,
'target': 123.9000015258789,
'boilers': dict({
'CoffeeBoiler1': dict({
'current_temperature': 96.5,
'enabled': True,
'target_temperature': 95,
}),
dict({
'current': 96.5,
'id': 'CoffeeBoiler1',
'isEnabled': True,
'target': 95,
}),
]),
'clock': '1901-07-08T10:29:00',
'firmwareVersions': list([
dict({
'fw_version': '1.40',
'name': 'machine_firmware',
}),
dict({
'fw_version': 'v3.1-rc4',
'name': 'gateway_firmware',
}),
]),
'groupCapabilities': list([
dict({
'capabilities': dict({
'boilerId': 'CoffeeBoiler1',
'groupNumber': 'Group1',
'groupType': 'AV_Group',
'hasFlowmeter': True,
'hasScale': False,
'numberOfDoses': 4,
}),
'doseMode': dict({
'brewingType': 'PulsesType',
'groupNumber': 'Group1',
}),
'doses': list([
dict({
'doseIndex': 'DoseA',
'doseType': 'PulsesType',
'groupNumber': 'Group1',
'stopTarget': 135,
}),
dict({
'doseIndex': 'DoseB',
'doseType': 'PulsesType',
'groupNumber': 'Group1',
'stopTarget': 97,
}),
dict({
'doseIndex': 'DoseC',
'doseType': 'PulsesType',
'groupNumber': 'Group1',
'stopTarget': 108,
}),
dict({
'doseIndex': 'DoseD',
'doseType': 'PulsesType',
'groupNumber': 'Group1',
'stopTarget': 121,
}),
]),
}),
]),
'isBackFlushEnabled': False,
'isPlumbedIn': True,
'machineCapabilities': list([
dict({
'coffeeBoilersNumber': 1,
'family': 'GS3AV',
'groupsNumber': 1,
'hasCupWarmer': False,
'machineModes': list([
'BrewingMode',
'StandBy',
]),
'schedulingType': 'weeklyScheduling',
'steamBoilersNumber': 1,
'teaDosesNumber': 1,
}),
]),
'machineMode': 'BrewingMode',
'machine_hw': '2',
'machine_sn': '**REDACTED**',
'preinfusionMode': dict({
'Group1': dict({
'groupNumber': 'Group1',
'preinfusionStyle': 'PreinfusionByDoseType',
'SteamBoiler': dict({
'current_temperature': 123.80000305175781,
'enabled': True,
'target_temperature': 123.9000015258789,
}),
}),
'preinfusionModesAvailable': list([
'ByDoseType',
]),
'preinfusionSettings': dict({
'Group1': list([
dict({
'doseType': 'DoseA',
'groupNumber': 'Group1',
'preWetHoldTime': 1,
'preWetTime': 0.5,
}),
dict({
'doseType': 'DoseB',
'groupNumber': 'Group1',
'preWetHoldTime': 1,
'preWetTime': 0.5,
}),
dict({
'doseType': 'DoseC',
'groupNumber': 'Group1',
'preWetHoldTime': 3.299999952316284,
'preWetTime': 3.299999952316284,
}),
dict({
'doseType': 'DoseD',
'groupNumber': 'Group1',
'preWetHoldTime': 2,
'preWetTime': 2,
}),
]),
'mode': 'TypeB',
}),
'standByTime': 0,
'tankStatus': True,
'teaDoses': dict({
'DoseA': dict({
'doseIndex': 'DoseA',
'stopTarget': 8,
}),
}),
'version': 'v1',
'weeklySchedulingConfig': dict({
'enabled': True,
'friday': dict({
'enabled': True,
'h_off': 16,
'h_on': 6,
'm_off': 0,
'm_on': 0,
}),
'monday': dict({
'enabled': True,
'h_off': 16,
'h_on': 6,
'm_off': 0,
'm_on': 0,
}),
'saturday': dict({
'enabled': True,
'h_off': 16,
'h_on': 6,
'm_off': 0,
'm_on': 0,
}),
'sunday': dict({
'enabled': True,
'h_off': 16,
'h_on': 6,
'm_off': 0,
'm_on': 0,
}),
'thursday': dict({
'enabled': True,
'h_off': 16,
'h_on': 6,
'm_off': 0,
'm_on': 0,
}),
'tuesday': dict({
'enabled': True,
'h_off': 16,
'h_on': 6,
'm_off': 0,
'm_on': 0,
}),
'wednesday': dict({
'enabled': True,
'h_off': 16,
'h_on': 6,
'm_off': 0,
'm_on': 0,
}),
}),
}),
'current_status': dict({
'brew_active': False,
'coffee_boiler_on': True,
'coffee_set_temp': 95,
'coffee_temp': 93,
'dose_hot_water': 1023,
'dose_k1': 1023,
'dose_k2': 1023,
'dose_k3': 1023,
'dose_k4': 1023,
'drinks_k1': 13,
'drinks_k2': 2,
'drinks_k3': 42,
'drinks_k4': 34,
'enable_prebrewing': True,
'enable_preinfusion': False,
'fri_auto': 'Disabled',
'fri_off_time': '00:00',
'fri_on_time': '00:00',
'global_auto': 'Enabled',
'mon_auto': 'Disabled',
'mon_off_time': '00:00',
'mon_on_time': '00:00',
'power': True,
'prebrewing_toff_k1': 5,
'prebrewing_toff_k2': 5,
'prebrewing_toff_k3': 5,
'prebrewing_toff_k4': 5,
'prebrewing_ton_k1': 3,
'prebrewing_ton_k2': 3,
'prebrewing_ton_k3': 3,
'prebrewing_ton_k4': 3,
'preinfusion_k1': 4,
'preinfusion_k2': 4,
'preinfusion_k3': 4,
'preinfusion_k4': 4,
'sat_auto': 'Disabled',
'sat_off_time': '00:00',
'sat_on_time': '00:00',
'steam_boiler_enable': True,
'steam_boiler_on': True,
'steam_level_set': 3,
'steam_set_temp': 128,
'steam_temp': 113,
'sun_auto': 'Disabled',
'sun_off_time': '00:00',
'sun_on_time': '00:00',
'thu_auto': 'Disabled',
'thu_off_time': '00:00',
'thu_on_time': '00:00',
'total_flushing': 69,
'tue_auto': 'Disabled',
'tue_off_time': '00:00',
'tue_on_time': '00:00',
'water_reservoir_contact': True,
'wed_auto': 'Disabled',
'wed_off_time': '00:00',
'wed_on_time': '00:00',
}),
'firmware': dict({
'gateway': dict({
'latest_version': 'v3.1-rc4',
'version': 'v2.2-rc0',
'brew_active_duration': 0,
'dose_hot_water': 8,
'doses': dict({
'1': 135,
'2': 97,
'3': 108,
'4': 121,
}),
'machine': dict({
'latest_version': '1.2',
'version': '1.1',
'plumbed_in': True,
'prebrew_configuration': dict({
'1': dict({
'off_time': 1,
'on_time': 0.5,
}),
'2': dict({
'off_time': 1,
'on_time': 0.5,
}),
'3': dict({
'off_time': 3.299999952316284,
'on_time': 3.299999952316284,
}),
'4': dict({
'off_time': 2,
'on_time': 2,
}),
}),
'prebrew_mode': 'TypeB',
'smart_standby': dict({
'enabled': True,
'minutes': 10,
'mode': 'LastBrewing',
}),
'turned_on': True,
'wake_up_sleep_entries': dict({
'Os2OswX': dict({
'days': list([
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
]),
'enabled': True,
'entry_id': 'Os2OswX',
'steam': True,
'time_off': '24:0',
'time_on': '22:0',
}),
'aXFz5bJ': dict({
'days': list([
'sunday',
]),
'enabled': True,
'entry_id': 'aXFz5bJ',
'steam': True,
'time_off': '7:30',
'time_on': '7:0',
}),
}),
'water_contact': True,
}),
'machine_info': dict({
'machine_name': 'GS01234',
'serial_number': '**REDACTED**',
}),
'firmware': list([
dict({
'machine': dict({
'current_version': '1.40',
'latest_version': '1.55',
}),
}),
dict({
'gateway': dict({
'current_version': 'v3.1-rc4',
'latest_version': 'v3.5-rc3',
}),
}),
]),
'model': 'GS3 AV',
'statistics': dict({
'stats': list([
dict({
'coffeeType': 0,
'count': 1047,
}),
dict({
'coffeeType': 1,
'count': 560,
}),
dict({
'coffeeType': 2,
'count': 468,
}),
dict({
'coffeeType': 3,
'count': 312,
}),
dict({
'coffeeType': 4,
'count': 2252,
}),
dict({
'coffeeType': -1,
'count': 1740,
}),
]),
'continous': 2252,
'drink_stats': dict({
'1': 1047,
'2': 560,
'3': 468,
'4': 312,
}),
'total_flushes': 1740,
}),
})
# ---

View File

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

View File

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

View File

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

View File

@ -20,16 +20,16 @@
'labels': set({
}),
'manufacturer': 'La Marzocco',
'model': 'GS3 AV',
'model': <MachineModel.GS3_AV: 'GS3 AV'>,
'name': 'GS01234',
'name_by_user': None,
'serial_number': 'GS01234',
'suggested_area': None,
'sw_version': '1.1',
'sw_version': '1.40',
'via_device_id': None,
})
# ---
# name: test_switches[-set_power-args_on0-args_off0]
# name: test_switches[-set_power]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS01234',
@ -42,7 +42,7 @@
'state': 'on',
})
# ---
# name: test_switches[-set_power-args_on0-args_off0].1
# name: test_switches[-set_power].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@ -75,141 +75,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switches[-set_power-kwargs_on0-kwargs_off0]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS01234',
'icon': 'mdi:power',
}),
'context': <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]
# name: test_switches[_steam_boiler-set_steam]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Steam boiler',
@ -222,53 +88,7 @@
'state': 'on',
})
# ---
# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <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
# name: test_switches[_steam_boiler-set_steam].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),

View File

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

View File

@ -1,7 +1,10 @@
"""Tests for La Marzocco binary sensors."""
from datetime import timedelta
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from lmcloud.exceptions import RequestNotSuccessful
import pytest
from syrupy import SnapshotAssertion
@ -11,7 +14,7 @@ from homeassistant.helpers import entity_registry as er
from . import async_init_integration
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
BINARY_SENSORS = (
"brewing_active",
@ -70,3 +73,29 @@ async def test_brew_active_unavailable(
)
assert state
assert state.state == STATE_UNAVAILABLE
async def test_sensor_going_unavailable(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test sensor is going unavailable after an unsuccessful update."""
brewing_active_sensor = (
f"binary_sensor.{mock_lamarzocco.serial_number}_brewing_active"
)
await async_init_integration(hass, mock_config_entry)
state = hass.states.get(brewing_active_sensor)
assert state
assert state.state != STATE_UNAVAILABLE
mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("")
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(brewing_active_sensor)
assert state
assert state.state == STATE_UNAVAILABLE

View File

@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import async_init_integration
from . import WAKE_UP_SLEEP_ENTRY_IDS, async_init_integration
from tests.common import MockConfigEntry
@ -40,27 +40,37 @@ async def test_calendar_events(
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"calendar.{serial_number}_auto_on_off_schedule")
assert state
assert state == snapshot
for identifier in WAKE_UP_SLEEP_ENTRY_IDS:
identifier = identifier.lower()
state = hass.states.get(
f"calendar.{serial_number}_auto_on_off_schedule_{identifier}"
)
assert state
assert state == snapshot(
name=f"state.{serial_number}_auto_on_off_schedule_{identifier}"
)
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot(
name=f"entry.{serial_number}_auto_on_off_schedule_{identifier}"
)
events = await hass.services.async_call(
CALENDAR_DOMAIN,
SERVICE_GET_EVENTS,
{
ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule",
EVENT_START_DATETIME: test_time,
EVENT_END_DATETIME: test_time + timedelta(days=23),
},
blocking=True,
return_response=True,
)
events = await hass.services.async_call(
CALENDAR_DOMAIN,
SERVICE_GET_EVENTS,
{
ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule_{identifier}",
EVENT_START_DATETIME: test_time,
EVENT_END_DATETIME: test_time + timedelta(days=23),
},
blocking=True,
return_response=True,
)
assert events == snapshot
assert events == snapshot(
name=f"events.{serial_number}_auto_on_off_schedule_{identifier}"
)
@pytest.mark.parametrize(
@ -89,21 +99,13 @@ async def test_calendar_edge_cases(
start_date = start_date.replace(tzinfo=dt_util.get_default_time_zone())
end_date = end_date.replace(tzinfo=dt_util.get_default_time_zone())
# set schedule to be only on Sunday, 07:00 - 07:30
mock_lamarzocco.schedule[2]["enable"] = "Disabled"
mock_lamarzocco.schedule[4]["enable"] = "Disabled"
mock_lamarzocco.schedule[5]["enable"] = "Disabled"
mock_lamarzocco.schedule[6]["enable"] = "Enabled"
mock_lamarzocco.schedule[6]["on"] = "07:00"
mock_lamarzocco.schedule[6]["off"] = "07:30"
await async_init_integration(hass, mock_config_entry)
events = await hass.services.async_call(
CALENDAR_DOMAIN,
SERVICE_GET_EVENTS,
{
ATTR_ENTITY_ID: f"calendar.{mock_lamarzocco.serial_number}_auto_on_off_schedule",
ATTR_ENTITY_ID: f"calendar.{mock_lamarzocco.serial_number}_auto_on_off_schedule_{WAKE_UP_SLEEP_ENTRY_IDS[1].lower()}",
EVENT_START_DATETIME: start_date,
EVENT_END_DATETIME: end_date,
},
@ -123,7 +125,9 @@ async def test_no_calendar_events_global_disable(
) -> None:
"""Assert no events when global auto on/off is disabled."""
mock_lamarzocco.current_status["global_auto"] = "Disabled"
wake_up_sleep_entry_id = WAKE_UP_SLEEP_ENTRY_IDS[0]
mock_lamarzocco.config.wake_up_sleep_entries[wake_up_sleep_entry_id].enabled = False
test_time = datetime(2024, 1, 12, 11, tzinfo=dt_util.get_default_time_zone())
freezer.move_to(test_time)
@ -131,14 +135,16 @@ async def test_no_calendar_events_global_disable(
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"calendar.{serial_number}_auto_on_off_schedule")
state = hass.states.get(
f"calendar.{serial_number}_auto_on_off_schedule_{wake_up_sleep_entry_id.lower()}"
)
assert state
events = await hass.services.async_call(
CALENDAR_DOMAIN,
SERVICE_GET_EVENTS,
{
ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule",
ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule_{wake_up_sleep_entry_id.lower()}",
EVENT_START_DATETIME: test_time,
EVENT_END_DATETIME: test_time + timedelta(days=23),
},

View File

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

View File

@ -1,15 +1,19 @@
"""Test initialization of lamarzocco."""
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
from lmcloud.const import FirmwareType
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
import pytest
from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE
from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_MAC, CONF_NAME
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from . import async_init_integration, get_bluetooth_service_info
from . import USER_INPUT, async_init_integration, get_bluetooth_service_info
from tests.common import MockConfigEntry
@ -20,7 +24,9 @@ async def test_load_unload_config_entry(
mock_lamarzocco: MagicMock,
) -> None:
"""Test loading and unloading the integration."""
await async_init_integration(hass, mock_config_entry)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
@ -36,11 +42,13 @@ async def test_config_entry_not_ready(
mock_lamarzocco: MagicMock,
) -> None:
"""Test the La Marzocco configuration entry not ready."""
mock_lamarzocco.update_local_machine_status.side_effect = RequestNotSuccessful("")
mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("")
await async_init_integration(hass, mock_config_entry)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1
assert len(mock_lamarzocco.get_config.mock_calls) == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@ -50,11 +58,13 @@ async def test_invalid_auth(
mock_lamarzocco: MagicMock,
) -> None:
"""Test auth error during setup."""
mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("")
await async_init_integration(hass, mock_config_entry)
mock_lamarzocco.get_config.side_effect = AuthFail("")
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1
assert len(mock_lamarzocco.get_config.mock_calls) == 1
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
@ -68,27 +78,132 @@ async def test_invalid_auth(
assert flow["context"].get("entry_id") == mock_config_entry.entry_id
async def test_v1_migration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_cloud_client: MagicMock,
mock_lamarzocco: MagicMock,
) -> None:
"""Test v1 -> v2 Migration."""
entry_v1 = MockConfigEntry(
domain=DOMAIN,
version=1,
unique_id=mock_lamarzocco.serial_number,
data={
**USER_INPUT,
CONF_HOST: "host",
CONF_MACHINE: mock_lamarzocco.serial_number,
CONF_MAC: "aa:bb:cc:dd:ee:ff",
},
)
entry_v1.add_to_hass(hass)
await hass.config_entries.async_setup(entry_v1.entry_id)
await hass.async_block_till_done()
assert entry_v1.version == 2
assert dict(entry_v1.data) == dict(mock_config_entry.data) | {
CONF_MAC: "aa:bb:cc:dd:ee:ff"
}
async def test_migration_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_cloud_client: MagicMock,
mock_lamarzocco: MagicMock,
) -> None:
"""Test errors during migration."""
mock_cloud_client.get_customer_fleet.side_effect = RequestNotSuccessful("Error")
entry_v1 = MockConfigEntry(
domain=DOMAIN,
version=1,
unique_id=mock_lamarzocco.serial_number,
data={
**USER_INPUT,
CONF_MACHINE: mock_lamarzocco.serial_number,
},
)
entry_v1.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry_v1.entry_id)
assert entry_v1.state is ConfigEntryState.MIGRATION_ERROR
async def test_config_flow_entry_migration_downgrade(
hass: HomeAssistant,
) -> None:
"""Test that config entry fails setup if the version is from the future."""
entry = MockConfigEntry(domain=DOMAIN, version=3)
entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry.entry_id)
async def test_bluetooth_is_set_from_discovery(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
) -> None:
"""Assert we're not searching for a new BT device when we already found one previously."""
# remove the bluetooth configuration from entry
data = mock_config_entry.data.copy()
del data[CONF_NAME]
del data[CONF_MAC]
hass.config_entries.async_update_entry(mock_config_entry, data=data)
"""Check we can fill a device from discovery info."""
service_info = get_bluetooth_service_info(
mock_lamarzocco.model_name, mock_lamarzocco.serial_number
mock_lamarzocco.model, mock_lamarzocco.serial_number
)
with patch(
"homeassistant.components.lamarzocco.coordinator.async_discovered_service_info",
return_value=[service_info],
with (
patch(
"homeassistant.components.lamarzocco.async_discovered_service_info",
return_value=[service_info],
) as discovery,
patch(
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine"
) as init_device,
):
await async_init_integration(hass, mock_config_entry)
mock_lamarzocco.init_bluetooth_with_known_device.assert_called_once()
discovery.assert_called_once()
init_device.assert_called_once()
_, kwargs = init_device.call_args
assert kwargs["bluetooth_client"] is not None
assert mock_config_entry.data[CONF_NAME] == service_info.name
assert mock_config_entry.data[CONF_MAC] == service_info.address
async def test_websocket_closed_on_unload(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
) -> None:
"""Test the websocket is closed on unload."""
with patch(
"homeassistant.components.lamarzocco.LaMarzoccoLocalClient",
autospec=True,
) as local_client:
client = local_client.return_value
client.websocket = AsyncMock()
client.websocket.connected = True
await async_init_integration(hass, mock_config_entry)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
client.websocket.close.assert_called_once()
@pytest.mark.parametrize(
("version", "issue_exists"), [("v3.5-rc6", False), ("v3.3-rc4", True)]
)
async def test_gateway_version_issue(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
version: str,
issue_exists: bool,
) -> None:
"""Make sure we get the issue for certain gateway firmware versions."""
mock_lamarzocco.firmware[FirmwareType.GATEWAY].current_version = version
await async_init_integration(hass, mock_config_entry)
issue_registry = ir.async_get(hass)
issue = issue_registry.async_get_issue(DOMAIN, "unsupported_gateway_firmware")
assert (issue is not None) == issue_exists

View File

@ -2,7 +2,13 @@
from unittest.mock import MagicMock
from lmcloud.const import KEYS_PER_MODEL, LaMarzoccoModel
from lmcloud.const import (
KEYS_PER_MODEL,
BoilerType,
MachineModel,
PhysicalKey,
PrebrewMode,
)
import pytest
from syrupy import SnapshotAssertion
@ -15,17 +21,22 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
pytestmark = pytest.mark.usefixtures("init_integration")
from . import async_init_integration
from tests.common import MockConfigEntry
async def test_coffee_boiler(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the La Marzocco coffee temperature Number."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"number.{serial_number}_coffee_target_temperature")
@ -47,35 +58,34 @@ async def test_coffee_boiler(
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature",
ATTR_VALUE: 95,
ATTR_VALUE: 94,
},
blocking=True,
)
assert len(mock_lamarzocco.set_coffee_temp.mock_calls) == 1
mock_lamarzocco.set_coffee_temp.assert_called_once_with(
temperature=95, ble_device=None
assert len(mock_lamarzocco.set_temp.mock_calls) == 1
mock_lamarzocco.set_temp.assert_called_once_with(
boiler=BoilerType.COFFEE, temperature=94
)
@pytest.mark.parametrize(
"device_fixture", [LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP]
)
@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV, MachineModel.GS3_MP])
@pytest.mark.parametrize(
("entity_name", "value", "func_name", "kwargs"),
[
(
"steam_target_temperature",
131,
"set_steam_temp",
{"temperature": 131, "ble_device": None},
"set_temp",
{"boiler": BoilerType.STEAM, "temperature": 131},
),
("tea_water_duration", 15, "set_dose_hot_water", {"value": 15}),
("tea_water_duration", 15, "set_dose_tea_water", {"dose": 15}),
],
)
async def test_gs3_exclusive(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
@ -85,7 +95,7 @@ async def test_gs3_exclusive(
kwargs: dict[str, float],
) -> None:
"""Test exclusive entities for GS3 AV/MP."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
func = getattr(mock_lamarzocco, func_name)
@ -118,14 +128,15 @@ async def test_gs3_exclusive(
@pytest.mark.parametrize(
"device_fixture", [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI]
"device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI]
)
async def test_gs3_exclusive_none(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure GS3 exclusive is None for unsupported models."""
await async_init_integration(hass, mock_config_entry)
ENTITIES = ("steam_target_temperature", "tea_water_duration")
serial_number = mock_lamarzocco.serial_number
@ -135,29 +146,50 @@ async def test_gs3_exclusive_none(
@pytest.mark.parametrize(
"device_fixture", [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI]
"device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI]
)
@pytest.mark.parametrize(
("entity_name", "value", "kwargs"),
("entity_name", "function_name", "prebrew_mode", "value", "kwargs"),
[
("prebrew_off_time", 6, {"on_time": 3000, "off_time": 6000, "key": 1}),
("prebrew_on_time", 6, {"on_time": 6000, "off_time": 5000, "key": 1}),
("preinfusion_time", 7, {"off_time": 7000, "key": 1}),
(
"prebrew_off_time",
"set_prebrew_time",
PrebrewMode.PREBREW,
6,
{"prebrew_off_time": 6.0, "key": PhysicalKey.A},
),
(
"prebrew_on_time",
"set_prebrew_time",
PrebrewMode.PREBREW,
6,
{"prebrew_on_time": 6.0, "key": PhysicalKey.A},
),
(
"preinfusion_time",
"set_preinfusion_time",
PrebrewMode.PREINFUSION,
7,
{"preinfusion_time": 7.0, "key": PhysicalKey.A},
),
],
)
async def test_pre_brew_infusion_numbers(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
entity_name: str,
function_name: str,
prebrew_mode: PrebrewMode,
value: float,
kwargs: dict[str, float],
) -> None:
"""Test the La Marzocco prebrew/-infusion sensors."""
mock_lamarzocco.current_status["enable_preinfusion"] = True
mock_lamarzocco.config.prebrew_mode = prebrew_mode
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
@ -168,12 +200,8 @@ async def test_pre_brew_infusion_numbers(
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry.device_id
assert entry == snapshot
device = device_registry.async_get(entry.device_id)
assert device
# service call
await hass.services.async_call(
NUMBER_DOMAIN,
@ -185,43 +213,97 @@ async def test_pre_brew_infusion_numbers(
blocking=True,
)
assert len(mock_lamarzocco.configure_prebrew.mock_calls) == 1
mock_lamarzocco.configure_prebrew.assert_called_once_with(**kwargs)
function = getattr(mock_lamarzocco, function_name)
function.assert_called_once_with(**kwargs)
@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.GS3_AV])
@pytest.mark.parametrize(
"device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI]
)
@pytest.mark.parametrize(
("prebrew_mode", "entity", "unavailable"),
[
(
PrebrewMode.PREBREW,
("prebrew_off_time", "prebrew_on_time"),
("preinfusion_time",),
),
(
PrebrewMode.PREINFUSION,
("preinfusion_time",),
("prebrew_off_time", "prebrew_on_time"),
),
],
)
async def test_pre_brew_infusion_numbers_unavailable(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
prebrew_mode: PrebrewMode,
entity: tuple[str, ...],
unavailable: tuple[str, ...],
) -> None:
"""Test entities are unavailable depending on selected state."""
mock_lamarzocco.config.prebrew_mode = prebrew_mode
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
for entity_name in entity:
state = hass.states.get(f"number.{serial_number}_{entity_name}")
assert state
assert state.state != STATE_UNAVAILABLE
for entity_name in unavailable:
state = hass.states.get(f"number.{serial_number}_{entity_name}")
assert state
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV])
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("entity_name", "value", "function_name", "kwargs"),
("entity_name", "value", "prebrew_mode", "function_name", "kwargs"),
[
(
"prebrew_off_time",
6,
"configure_prebrew",
{"on_time": 3000, "off_time": 6000},
PrebrewMode.PREBREW,
"set_prebrew_time",
{"prebrew_off_time": 6.0},
),
(
"prebrew_on_time",
6,
"configure_prebrew",
{"on_time": 6000, "off_time": 5000},
PrebrewMode.PREBREW,
"set_prebrew_time",
{"prebrew_on_time": 6.0},
),
("preinfusion_time", 7, "configure_prebrew", {"off_time": 7000}),
("dose", 6, "set_dose", {"value": 6}),
(
"preinfusion_time",
7,
PrebrewMode.PREINFUSION,
"set_preinfusion_time",
{"preinfusion_time": 7.0},
),
("dose", 6, PrebrewMode.DISABLED, "set_dose", {"dose": 6}),
],
)
async def test_pre_brew_infusion_key_numbers(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_name: str,
value: float,
prebrew_mode: PrebrewMode,
function_name: str,
kwargs: dict[str, float],
) -> None:
"""Test the La Marzocco number sensors for GS3AV model."""
mock_lamarzocco.current_status["enable_preinfusion"] = True
mock_lamarzocco.config.prebrew_mode = prebrew_mode
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
@ -230,7 +312,7 @@ async def test_pre_brew_infusion_key_numbers(
state = hass.states.get(f"number.{serial_number}_{entity_name}")
assert state is None
for key in range(1, KEYS_PER_MODEL[mock_lamarzocco.model_name] + 1):
for key in PhysicalKey:
state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}")
assert state
assert state == snapshot(name=f"{serial_number}_{entity_name}_key_{key}-state")
@ -248,17 +330,18 @@ async def test_pre_brew_infusion_key_numbers(
kwargs["key"] = key
assert len(func.mock_calls) == key
assert len(func.mock_calls) == key.value
func.assert_called_with(**kwargs)
@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.GS3_AV])
@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV])
async def test_disabled_entites(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the La Marzocco prebrew/-infusion sensors for GS3AV model."""
await async_init_integration(hass, mock_config_entry)
ENTITIES = (
"prebrew_off_time",
"prebrew_on_time",
@ -269,21 +352,22 @@ async def test_disabled_entites(
serial_number = mock_lamarzocco.serial_number
for entity_name in ENTITIES:
for key in range(1, KEYS_PER_MODEL[mock_lamarzocco.model_name] + 1):
for key in PhysicalKey:
state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}")
assert state is None
@pytest.mark.parametrize(
"device_fixture",
[LaMarzoccoModel.GS3_MP, LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI],
[MachineModel.GS3_MP, MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI],
)
async def test_not_existing_key_entites(
async def test_not_existing_key_entities(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Assert not existing key entities."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
for entity in (
@ -292,42 +376,6 @@ async def test_not_existing_key_entites(
"preinfusion_time",
"set_dose",
):
for key in range(1, KEYS_PER_MODEL[LaMarzoccoModel.GS3_AV] + 1):
for key in range(1, KEYS_PER_MODEL[MachineModel.GS3_AV] + 1):
state = hass.states.get(f"number.{serial_number}_{entity}_key_{key}")
assert state is None
@pytest.mark.parametrize(
"device_fixture",
[LaMarzoccoModel.GS3_MP],
)
async def test_not_existing_entites(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
) -> None:
"""Assert not existing entities."""
serial_number = mock_lamarzocco.serial_number
for entity in (
"prebrew_off_time",
"prebrew_on_time",
"preinfusion_time",
"set_dose",
):
state = hass.states.get(f"number.{serial_number}_{entity}")
assert state is None
@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.LINEA_MICRA])
async def test_not_settable_entites(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
) -> None:
"""Assert not settable causes error."""
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"number.{serial_number}_preinfusion_time")
assert state
assert state.state == STATE_UNAVAILABLE

View File

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

View File

@ -5,7 +5,6 @@ from unittest.mock import MagicMock
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
@ -15,35 +14,39 @@ from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
from . import async_init_integration
pytestmark = pytest.mark.usefixtures("init_integration")
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
("entity_name", "method_name", "args_on", "args_off"),
(
"entity_name",
"method_name",
),
[
("", "set_power", (True, None), (False, None)),
(
"_auto_on_off",
"set_auto_on_off_global",
(True,),
(False,),
"",
"set_power",
),
(
"_steam_boiler",
"set_steam",
),
("_steam_boiler", "set_steam", (True, None), (False, None)),
],
)
async def test_switches(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
entity_name: str,
method_name: str,
args_on: tuple,
args_off: tuple,
) -> None:
"""Test the La Marzocco switches."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
control_fn = getattr(mock_lamarzocco, method_name)
@ -66,7 +69,7 @@ async def test_switches(
)
assert len(control_fn.mock_calls) == 1
control_fn.assert_called_once_with(*args_off)
control_fn.assert_called_once_with(False)
await hass.services.async_call(
SWITCH_DOMAIN,
@ -78,18 +81,21 @@ async def test_switches(
)
assert len(control_fn.mock_calls) == 2
control_fn.assert_called_with(*args_on)
control_fn.assert_called_with(True)
async def test_device(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the device for one switch."""
await async_init_integration(hass, mock_config_entry)
state = hass.states.get(f"switch.{mock_lamarzocco.serial_number}")
assert state
@ -100,26 +106,3 @@ async def test_device(
device = device_registry.async_get(entry.device_id)
assert device
assert device == snapshot
async def test_call_without_bluetooth_works(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that if not using bluetooth, the switch still works."""
serial_number = mock_lamarzocco.serial_number
coordinator = hass.data[DOMAIN][mock_config_entry.entry_id]
coordinator._use_bluetooth = False
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: f"switch.{serial_number}_steam_boiler",
},
blocking=True,
)
assert len(mock_lamarzocco.set_steam.mock_calls) == 1
mock_lamarzocco.set_steam.assert_called_once_with(False, None)

View File

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