Migrate lamarzocco to pylamarzocco 2.0.0 (#142098)

* Migrate lamarzocco to pylamarzocco 2.0.0

* bump manifest

* Remove CONF_TOKEN

* remove icons

* Rename coordiantor

* use none for token

* Bump version

* Move first get settings

* remove sensor snapshots

* Change iot_class from cloud_polling to cloud_push

* Update integrations.json

* Re-add release url

* Remove extra icon, fix native step

* fomat

* Rename const

* review comments

* Update tests/components/lamarzocco/test_config_flow.py

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

* add unique id check

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Josef Zweck 2025-04-17 13:34:06 +02:00 committed by GitHub
parent 0aaa4fa79b
commit bbb8a1bacc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 2442 additions and 4411 deletions

View File

@ -1,27 +1,27 @@
"""The La Marzocco integration."""
import asyncio
import logging
from packaging import version
from pylamarzocco.clients.bluetooth import LaMarzoccoBluetoothClient
from pylamarzocco.clients.cloud import LaMarzoccoCloudClient
from pylamarzocco.clients.local import LaMarzoccoLocalClient
from pylamarzocco.const import BT_MODEL_PREFIXES, FirmwareType
from pylamarzocco.devices.machine import LaMarzoccoMachine
from pylamarzocco import (
LaMarzoccoBluetoothClient,
LaMarzoccoCloudClient,
LaMarzoccoMachine,
)
from pylamarzocco.const import FirmwareType
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from homeassistant.components.bluetooth import async_discovered_service_info
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.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_create_clientsession
@ -29,9 +29,9 @@ from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import (
LaMarzoccoConfigEntry,
LaMarzoccoConfigUpdateCoordinator,
LaMarzoccoFirmwareUpdateCoordinator,
LaMarzoccoRuntimeData,
LaMarzoccoStatisticsUpdateCoordinator,
LaMarzoccoScheduleUpdateCoordinator,
LaMarzoccoSettingsUpdateCoordinator,
)
PLATFORMS = [
@ -40,11 +40,12 @@ PLATFORMS = [
Platform.CALENDAR,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
BT_MODEL_PREFIXES = ("MICRA", "MINI", "GS3")
_LOGGER = logging.getLogger(__name__)
@ -61,31 +62,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
client=client,
)
# initialize the firmware update coordinator early to check the firmware version
firmware_device = LaMarzoccoMachine(
model=entry.data[CONF_MODEL],
serial_number=entry.unique_id,
name=entry.data[CONF_NAME],
cloud_client=cloud_client,
)
try:
settings = await cloud_client.get_thing_settings(serial)
except AuthFail as ex:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from ex
except RequestNotSuccessful as ex:
_LOGGER.debug(ex, exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="api_error"
) from ex
firmware_coordinator = LaMarzoccoFirmwareUpdateCoordinator(
hass, entry, firmware_device
)
await firmware_coordinator.async_config_entry_first_refresh()
gateway_version = version.parse(
firmware_device.firmware[FirmwareType.GATEWAY].current_version
settings.firmwares[FirmwareType.GATEWAY].build_version
)
if gateway_version >= version.parse("v5.0.9"):
# remove host from config entry, it is not supported anymore
data = {k: v for k, v in entry.data.items() if k != CONF_HOST}
hass.config_entries.async_update_entry(
entry,
data=data,
)
elif gateway_version < version.parse("v3.4-rc5"):
if gateway_version < version.parse("v5.0.9"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(
hass,
@ -97,24 +90,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
translation_placeholders={"gateway_version": str(gateway_version)},
)
# 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=client,
)
# 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():
if entry.options.get(CONF_USE_BLUETOOTH, True) and (
token := settings.ble_auth_token
):
if CONF_MAC not in entry.data:
for discovery_info in async_discovered_service_info(hass):
if (
(name := discovery_info.name)
@ -128,38 +109,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
data={
**entry.data,
CONF_MAC: discovery_info.address,
CONF_NAME: discovery_info.name,
},
)
break
if bluetooth_configured():
if not entry.data[CONF_TOKEN]:
# update the token in the config entry
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_TOKEN: token,
},
)
if CONF_MAC in entry.data:
_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],
ble_token=token,
)
device = LaMarzoccoMachine(
model=entry.data[CONF_MODEL],
serial_number=entry.unique_id,
name=entry.data[CONF_NAME],
cloud_client=cloud_client,
local_client=local_client,
bluetooth_client=bluetooth_client,
)
coordinators = LaMarzoccoRuntimeData(
LaMarzoccoConfigUpdateCoordinator(hass, entry, device, local_client),
firmware_coordinator,
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
LaMarzoccoConfigUpdateCoordinator(hass, entry, device),
LaMarzoccoSettingsUpdateCoordinator(hass, entry, device),
LaMarzoccoScheduleUpdateCoordinator(hass, entry, device),
)
# API does not like concurrent requests, so no asyncio.gather here
await coordinators.config_coordinator.async_config_entry_first_refresh()
await coordinators.statistics_coordinator.async_config_entry_first_refresh()
await asyncio.gather(
coordinators.config_coordinator.async_config_entry_first_refresh(),
coordinators.settings_coordinator.async_config_entry_first_refresh(),
coordinators.schedule_coordinator.async_config_entry_first_refresh(),
)
entry.runtime_data = coordinators
@ -184,41 +170,45 @@ async def async_migrate_entry(
hass: HomeAssistant, entry: LaMarzoccoConfigEntry
) -> bool:
"""Migrate config entry."""
if entry.version > 2:
if entry.version > 3:
# guard against downgrade from a future version
return False
if entry.version == 1:
_LOGGER.error(
"Migration from version 1 is no longer supported, please remove and re-add the integration"
)
return False
if entry.version == 2:
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
try:
fleet = await cloud_client.get_customer_fleet()
things = await cloud_client.list_things()
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 = {
v3_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,
CONF_TOKEN: next(
(
thing.ble_auth_token
for thing in things
if thing.serial_number == entry.unique_id
),
None,
),
}
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]
v3_data[CONF_MAC] = entry.data[CONF_MAC]
hass.config_entries.async_update_entry(
entry,
data=v2_data,
version=2,
data=v3_data,
version=3,
)
_LOGGER.debug("Migrated La Marzocco config entry to version 2")
return True

View File

@ -2,9 +2,10 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import cast
from pylamarzocco.const import MachineModel
from pylamarzocco.models import LaMarzoccoMachineConfig
from pylamarzocco.const import BackFlushStatus, MachineState, WidgetType
from pylamarzocco.models import BackFlush, BaseWidgetOutput, MachineStatus
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@ -16,7 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import LaMarzoccoConfigEntry
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription, LaMarzoccScaleEntity
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@ -29,7 +30,7 @@ class LaMarzoccoBinarySensorEntityDescription(
):
"""Description of a La Marzocco binary sensor."""
is_on_fn: Callable[[LaMarzoccoMachineConfig], bool | None]
is_on_fn: Callable[[dict[WidgetType, BaseWidgetOutput]], bool | None]
ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
@ -37,32 +38,30 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
key="water_tank",
translation_key="water_tank",
device_class=BinarySensorDeviceClass.PROBLEM,
is_on_fn=lambda config: not config.water_contact,
is_on_fn=lambda config: WidgetType.CM_NO_WATER in config,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda coordinator: coordinator.local_connection_configured,
),
LaMarzoccoBinarySensorEntityDescription(
key="brew_active",
translation_key="brew_active",
device_class=BinarySensorDeviceClass.RUNNING,
is_on_fn=lambda config: config.brew_active,
available_fn=lambda device: device.websocket_connected,
is_on_fn=(
lambda config: cast(
MachineStatus, config[WidgetType.CM_MACHINE_STATUS]
).status
is MachineState.BREWING
),
available_fn=lambda device: device.websocket.connected,
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoBinarySensorEntityDescription(
key="backflush_enabled",
translation_key="backflush_enabled",
device_class=BinarySensorDeviceClass.RUNNING,
is_on_fn=lambda config: config.backflush_enabled,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
SCALE_ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
LaMarzoccoBinarySensorEntityDescription(
key="connected",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on_fn=lambda config: config.scale.connected if config.scale else None,
is_on_fn=(
lambda config: cast(BackFlush, config[WidgetType.CM_BACK_FLUSH]).status
is BackFlushStatus.REQUESTED
),
entity_category=EntityCategory.DIAGNOSTIC,
),
)
@ -76,30 +75,11 @@ async def async_setup_entry(
"""Set up binary sensor entities."""
coordinator = entry.runtime_data.config_coordinator
entities = [
async_add_entities(
LaMarzoccoBinarySensorEntity(coordinator, description)
for description in ENTITIES
if description.supported_fn(coordinator)
]
if (
coordinator.device.model in (MachineModel.LINEA_MINI, MachineModel.LINEA_MINI_R)
and coordinator.device.config.scale
):
entities.extend(
LaMarzoccoScaleBinarySensorEntity(coordinator, description)
for description in SCALE_ENTITIES
)
def _async_add_new_scale() -> None:
async_add_entities(
LaMarzoccoScaleBinarySensorEntity(coordinator, description)
for description in SCALE_ENTITIES
)
coordinator.new_device_callback.append(_async_add_new_scale)
async_add_entities(entities)
)
class LaMarzoccoBinarySensorEntity(LaMarzoccoEntity, BinarySensorEntity):
@ -110,12 +90,6 @@ class LaMarzoccoBinarySensorEntity(LaMarzoccoEntity, BinarySensorEntity):
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self.entity_description.is_on_fn(self.coordinator.device.config)
class LaMarzoccoScaleBinarySensorEntity(
LaMarzoccoBinarySensorEntity, LaMarzoccScaleEntity
):
"""Binary sensor for La Marzocco scales."""
entity_description: LaMarzoccoBinarySensorEntityDescription
return self.entity_description.is_on_fn(
self.coordinator.device.dashboard.config
)

View File

@ -3,7 +3,7 @@
from collections.abc import Iterator
from datetime import datetime, timedelta
from pylamarzocco.models import LaMarzoccoWakeUpSleepEntry
from pylamarzocco.const import WeekDay
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant
@ -18,15 +18,15 @@ PARALLEL_UPDATES = 0
CALENDAR_KEY = "auto_on_off_schedule"
DAY_OF_WEEK = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
WEEKDAY_TO_ENUM = {
0: WeekDay.MONDAY,
1: WeekDay.TUESDAY,
2: WeekDay.WEDNESDAY,
3: WeekDay.THURSDAY,
4: WeekDay.FRIDAY,
5: WeekDay.SATURDAY,
6: WeekDay.SUNDAY,
}
async def async_setup_entry(
@ -36,10 +36,12 @@ async def async_setup_entry(
) -> None:
"""Set up switch entities and services."""
coordinator = entry.runtime_data.config_coordinator
coordinator = entry.runtime_data.schedule_coordinator
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()
LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY, schedule.identifier)
for schedule in coordinator.device.schedule.smart_wake_up_sleep.schedules
if schedule.identifier
)
@ -52,12 +54,12 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
self,
coordinator: LaMarzoccoUpdateCoordinator,
key: str,
wake_up_sleep_entry: LaMarzoccoWakeUpSleepEntry,
identifier: str,
) -> 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}
super().__init__(coordinator, f"{key}_{identifier}")
self._identifier = identifier
self._attr_translation_placeholders = {"id": identifier}
@property
def event(self) -> CalendarEvent | None:
@ -112,24 +114,31 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity):
def _async_get_calendar_event(self, date: datetime) -> CalendarEvent | None:
"""Return calendar event for a given weekday."""
schedule_entry = (
self.coordinator.device.schedule.smart_wake_up_sleep.schedules_dict[
self._identifier
]
)
# check first if auto/on off is turned on in general
if not self.wake_up_sleep_entry.enabled:
if not schedule_entry.enabled:
return None
# parse the schedule for the day
if DAY_OF_WEEK[date.weekday()] not in self.wake_up_sleep_entry.days:
if WEEKDAY_TO_ENUM[date.weekday()] not in schedule_entry.days:
return None
hour_on, minute_on = self.wake_up_sleep_entry.time_on.split(":")
hour_off, minute_off = self.wake_up_sleep_entry.time_off.split(":")
hour_on = schedule_entry.on_time_minutes // 60
minute_on = schedule_entry.on_time_minutes % 60
hour_off = schedule_entry.off_time_minutes // 60
minute_off = schedule_entry.off_time_minutes % 60
# 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":
if hour_off == 24:
# if the machine is scheduled to turn off at midnight, we need to
# set the end date to the next day
day_offset = 1
hour_off = "0"
hour_off = 0
end_date = date.replace(
hour=int(hour_off),

View File

@ -7,10 +7,9 @@ import logging
from typing import Any
from aiohttp import ClientSession
from pylamarzocco.clients.cloud import LaMarzoccoCloudClient
from pylamarzocco.clients.local import LaMarzoccoLocalClient
from pylamarzocco import LaMarzoccoCloudClient
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoDeviceInfo
from pylamarzocco.models import Thing
import voluptuous as vol
from homeassistant.components.bluetooth import (
@ -26,9 +25,7 @@ from homeassistant.config_entries import (
)
from homeassistant.const import (
CONF_ADDRESS,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PASSWORD,
CONF_TOKEN,
@ -59,14 +56,14 @@ _LOGGER = logging.getLogger(__name__)
class LmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for La Marzocco."""
VERSION = 2
VERSION = 3
_client: ClientSession
def __init__(self) -> None:
"""Initialize the config flow."""
self._config: dict[str, Any] = {}
self._fleet: dict[str, LaMarzoccoDeviceInfo] = {}
self._things: dict[str, Thing] = {}
self._discovered: dict[str, str] = {}
async def async_step_user(
@ -83,7 +80,6 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
data = {
**data,
**user_input,
**self._discovered,
}
self._client = async_create_clientsession(self.hass)
@ -93,7 +89,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
client=self._client,
)
try:
self._fleet = await cloud_client.get_customer_fleet()
things = await cloud_client.list_things()
except AuthFail:
_LOGGER.debug("Server rejected login credentials")
errors["base"] = "invalid_auth"
@ -101,37 +97,30 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("Error connecting to server: %s", exc)
errors["base"] = "cannot_connect"
else:
if not self._fleet:
self._things = {thing.serial_number: thing for thing in things}
if not self._things:
errors["base"] = "no_machines"
if not errors:
self._config = data
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=data
)
if self._discovered:
if self._discovered[CONF_MACHINE] not in self._fleet:
if self._discovered[CONF_MACHINE] not in self._things:
errors["base"] = "machine_not_found"
else:
self._config = data
# if DHCP discovery was used, auto fill machine selection
if CONF_HOST in self._discovered:
return await self.async_step_machine_selection(
user_input={
CONF_HOST: self._discovered[CONF_HOST],
CONF_MACHINE: self._discovered[CONF_MACHINE],
}
)
# if Bluetooth discovery was used, only select host
return self.async_show_form(
step_id="machine_selection",
data_schema=vol.Schema(
{vol.Optional(CONF_HOST): cv.string}
),
)
# store discovered connection address
if CONF_MAC in self._discovered:
self._config[CONF_MAC] = self._discovered[CONF_MAC]
if CONF_ADDRESS in self._discovered:
self._config[CONF_ADDRESS] = self._discovered[CONF_ADDRESS]
return await self.async_step_machine_selection(
user_input={CONF_MACHINE: self._discovered[CONF_MACHINE]}
)
if not errors:
self._config = data
return await self.async_step_machine_selection()
placeholders: dict[str, str] | None = None
@ -175,18 +164,7 @@ 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):
if not await LaMarzoccoLocalClient.validate_connection(
client=self._client,
host=user_input[CONF_HOST],
token=selected_device.communication_key,
):
errors[CONF_HOST] = "cannot_connect"
else:
self._config[CONF_HOST] = user_input[CONF_HOST]
selected_device = self._things[serial_number]
if not errors:
if self.source == SOURCE_RECONFIGURE:
@ -200,18 +178,16 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
title=selected_device.name,
data={
**self._config,
CONF_NAME: selected_device.name,
CONF_MODEL: selected_device.model,
CONF_TOKEN: selected_device.communication_key,
CONF_TOKEN: self._things[serial_number].ble_auth_token,
},
)
machine_options = [
SelectOptionDict(
value=device.serial_number,
label=f"{device.model} ({device.serial_number})",
value=thing.serial_number,
label=f"{thing.name} ({thing.serial_number})",
)
for device in self._fleet.values()
for thing in self._things.values()
]
machine_selection_schema = vol.Schema(
@ -224,7 +200,6 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_HOST): cv.string,
}
)
@ -304,7 +279,6 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured(
updates={
CONF_HOST: discovery_info.ip,
CONF_ADDRESS: discovery_info.macaddress,
}
)
@ -316,8 +290,8 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
discovery_info.ip,
)
self._discovered[CONF_NAME] = discovery_info.hostname
self._discovered[CONF_MACHINE] = serial
self._discovered[CONF_HOST] = discovery_info.ip
self._discovered[CONF_ADDRESS] = discovery_info.macaddress
return await self.async_step_user()

View File

@ -3,28 +3,25 @@
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Callable
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
from pylamarzocco.clients.local import LaMarzoccoLocalClient
from pylamarzocco.devices.machine import LaMarzoccoMachine
from pylamarzocco import LaMarzoccoMachine
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=1)
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=5)
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
_LOGGER = logging.getLogger(__name__)
@ -33,8 +30,8 @@ class LaMarzoccoRuntimeData:
"""Runtime data for La Marzocco."""
config_coordinator: LaMarzoccoConfigUpdateCoordinator
firmware_coordinator: LaMarzoccoFirmwareUpdateCoordinator
statistics_coordinator: LaMarzoccoStatisticsUpdateCoordinator
settings_coordinator: LaMarzoccoSettingsUpdateCoordinator
schedule_coordinator: LaMarzoccoScheduleUpdateCoordinator
type LaMarzoccoConfigEntry = ConfigEntry[LaMarzoccoRuntimeData]
@ -51,7 +48,6 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
hass: HomeAssistant,
entry: LaMarzoccoConfigEntry,
device: LaMarzoccoMachine,
local_client: LaMarzoccoLocalClient | None = None,
) -> None:
"""Initialize coordinator."""
super().__init__(
@ -62,9 +58,6 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
update_interval=self._default_update_interval,
)
self.device = device
self.local_connection_configured = local_client is not None
self._local_client = local_client
self.new_device_callback: list[Callable] = []
async def _async_update_data(self) -> None:
"""Do the data update."""
@ -89,30 +82,22 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Class to handle fetching data from the La Marzocco API centrally."""
_scale_address: str | None = None
async def _async_connect_websocket(self) -> None:
"""Set up the coordinator."""
if self._local_client is not None and (
self._local_client.websocket is None or self._local_client.websocket.closed
):
if not self.device.websocket.connected:
_LOGGER.debug("Init WebSocket in background task")
self.config_entry.async_create_background_task(
hass=self.hass,
target=self.device.websocket_connect(
notify_callback=lambda: self.async_set_updated_data(None)
target=self.device.connect_dashboard_websocket(
update_callback=lambda _: self.async_set_updated_data(None)
),
name="lm_websocket_task",
)
async def websocket_close(_: Any | None = None) -> None:
if (
self._local_client is not None
and self._local_client.websocket is not None
and not self._local_client.websocket.closed
):
await self._local_client.websocket.close()
if self.device.websocket.connected:
await self.device.websocket.disconnect()
self.config_entry.async_on_unload(
self.hass.bus.async_listen_once(
@ -123,47 +108,28 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
async def _internal_async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.device.get_config()
_LOGGER.debug("Current status: %s", str(self.device.config))
await self.device.get_dashboard()
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
await self._async_connect_websocket()
self._async_add_remove_scale()
@callback
def _async_add_remove_scale(self) -> None:
"""Add or remove a scale when added or removed."""
if self.device.config.scale and not self._scale_address:
self._scale_address = self.device.config.scale.address
for scale_callback in self.new_device_callback:
scale_callback()
elif not self.device.config.scale and self._scale_address:
device_registry = dr.async_get(self.hass)
if device := device_registry.async_get_device(
identifiers={(DOMAIN, self._scale_address)}
):
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
self._scale_address = None
class LaMarzoccoFirmwareUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco firmware."""
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco settings."""
_default_update_interval = FIRMWARE_UPDATE_INTERVAL
_default_update_interval = SETTINGS_UPDATE_INTERVAL
async def _internal_async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.device.get_firmware()
_LOGGER.debug("Current firmware: %s", str(self.device.firmware))
await self.device.get_settings()
_LOGGER.debug("Current settings: %s", self.device.settings.to_dict())
class LaMarzoccoStatisticsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco statistics."""
class LaMarzoccoScheduleUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco schedule."""
_default_update_interval = STATISTICS_UPDATE_INTERVAL
_default_update_interval = SCHEDULE_UPDATE_INTERVAL
async def _internal_async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.device.get_statistics()
_LOGGER.debug("Current statistics: %s", str(self.device.statistics))
await self.device.get_schedule()
_LOGGER.debug("Current schedule: %s", self.device.schedule.to_dict())

View File

@ -2,10 +2,7 @@
from __future__ import annotations
from dataclasses import asdict
from typing import Any, TypedDict
from pylamarzocco.const import FirmwareType
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
@ -17,15 +14,6 @@ TO_REDACT = {
}
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: LaMarzoccoConfigEntry,
@ -33,12 +21,4 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data.config_coordinator
device = coordinator.device
# collect all data sources
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),
)
return async_redact_data(diagnostics_data, TO_REDACT)
return async_redact_data(device.to_dict(), TO_REDACT)

View File

@ -2,10 +2,9 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from pylamarzocco import LaMarzoccoMachine
from pylamarzocco.const import FirmwareType
from pylamarzocco.devices.machine import LaMarzoccoMachine
from homeassistant.const import CONF_ADDRESS, CONF_MAC
from homeassistant.helpers.device_registry import (
@ -46,12 +45,12 @@ class LaMarzoccoBaseEntity(
self._attr_unique_id = f"{device.serial_number}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.serial_number)},
name=device.name,
name=device.dashboard.name,
manufacturer="La Marzocco",
model=device.full_model_name,
model_id=device.model,
model=device.dashboard.model_name.value,
model_id=device.dashboard.model_code.value,
serial_number=device.serial_number,
sw_version=device.firmware[FirmwareType.MACHINE].current_version,
sw_version=device.settings.firmwares[FirmwareType.MACHINE].build_version,
)
connections: set[tuple[str, str]] = set()
if coordinator.config_entry.data.get(CONF_ADDRESS):
@ -86,26 +85,3 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity):
"""Initialize the entity."""
super().__init__(coordinator, entity_description.key)
self.entity_description = entity_description
class LaMarzoccScaleEntity(LaMarzoccoEntity):
"""Common class for scale."""
def __init__(
self,
coordinator: LaMarzoccoUpdateCoordinator,
entity_description: LaMarzoccoEntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator, entity_description)
scale = coordinator.device.config.scale
if TYPE_CHECKING:
assert scale
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, scale.address)},
name=scale.name,
manufacturer="Acaia",
model="Lunar",
model_id="Y.301",
via_device=(DOMAIN, coordinator.device.serial_number),
)

View File

@ -34,36 +34,11 @@
"dose": {
"default": "mdi:cup-water"
},
"prebrew_off": {
"default": "mdi:water-off"
},
"prebrew_on": {
"default": "mdi:water"
},
"preinfusion_off": {
"default": "mdi:water"
},
"scale_target": {
"default": "mdi:scale-balance"
},
"smart_standby_time": {
"default": "mdi:timer"
},
"steam_temp": {
"default": "mdi:thermometer-water"
},
"tea_water_duration": {
"default": "mdi:timer-sand"
}
},
"select": {
"active_bbw": {
"default": "mdi:alpha-u",
"state": {
"a": "mdi:alpha-a",
"b": "mdi:alpha-b"
}
},
"smart_standby_mode": {
"default": "mdi:power",
"state": {
@ -88,26 +63,6 @@
}
}
},
"sensor": {
"drink_stats_coffee": {
"default": "mdi:chart-line"
},
"drink_stats_flushing": {
"default": "mdi:chart-line"
},
"drink_stats_coffee_key": {
"default": "mdi:chart-scatter-plot"
},
"shot_timer": {
"default": "mdi:timer"
},
"current_temp_coffee": {
"default": "mdi:thermometer"
},
"current_temp_steam": {
"default": "mdi:thermometer"
}
},
"switch": {
"main": {
"default": "mdi:power",

View File

@ -34,8 +34,8 @@
],
"documentation": "https://www.home-assistant.io/integrations/lamarzocco",
"integration_type": "device",
"iot_class": "cloud_polling",
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==1.4.9"]
"requirements": ["pylamarzocco==2.0.0b1"]
}

View File

@ -2,18 +2,12 @@
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from typing import Any, cast
from pylamarzocco.const import (
KEYS_PER_MODEL,
BoilerType,
MachineModel,
PhysicalKey,
PrebrewMode,
)
from pylamarzocco.devices.machine import LaMarzoccoMachine
from pylamarzocco import LaMarzoccoMachine
from pylamarzocco.const import WidgetType
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoMachineConfig
from pylamarzocco.models import CoffeeBoiler
from homeassistant.components.number import (
NumberDeviceClass,
@ -32,8 +26,8 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription, LaMarzoccScaleEntity
from .coordinator import LaMarzoccoConfigEntry
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
PARALLEL_UPDATES = 1
@ -45,25 +39,10 @@ class LaMarzoccoNumberEntityDescription(
):
"""Description of a La Marzocco number entity."""
native_value_fn: Callable[[LaMarzoccoMachineConfig], float | int]
native_value_fn: Callable[[LaMarzoccoMachine], float | int]
set_value_fn: Callable[[LaMarzoccoMachine, float | int], Coroutine[Any, Any, bool]]
@dataclass(frozen=True, kw_only=True)
class LaMarzoccoKeyNumberEntityDescription(
LaMarzoccoEntityDescription,
NumberEntityDescription,
):
"""Description of an La Marzocco number entity with keys."""
native_value_fn: Callable[
[LaMarzoccoMachineConfig, PhysicalKey], float | int | None
]
set_value_fn: Callable[
[LaMarzoccoMachine, float | int, PhysicalKey], Coroutine[Any, Any, bool]
]
ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
LaMarzoccoNumberEntityDescription(
key="coffee_temp",
@ -73,43 +52,11 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_step=PRECISION_TENTHS,
native_min_value=85,
native_max_value=104,
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",
translation_key="steam_temp",
device_class=NumberDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
native_step=PRECISION_WHOLE,
native_min_value=126,
native_max_value=131,
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 (
MachineModel.GS3_AV,
MachineModel.GS3_MP,
),
),
LaMarzoccoNumberEntityDescription(
key="tea_water_duration",
translation_key="tea_water_duration",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_WHOLE,
native_min_value=0,
native_max_value=30,
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 (
MachineModel.GS3_AV,
MachineModel.GS3_MP,
set_value_fn=lambda machine, temp: machine.set_coffee_target_temperature(temp),
native_value_fn=(
lambda machine: cast(
CoffeeBoiler, machine.dashboard.config[WidgetType.CM_COFFEE_BOILER]
).target_temperature
),
),
LaMarzoccoNumberEntityDescription(
@ -117,119 +64,18 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
translation_key="smart_standby_time",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
native_step=10,
native_min_value=10,
native_max_value=240,
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda machine, value: machine.set_smart_standby(
enabled=machine.config.smart_standby.enabled,
mode=machine.config.smart_standby.mode,
minutes=int(value),
),
native_value_fn=lambda config: config.smart_standby.minutes,
),
)
KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
LaMarzoccoKeyNumberEntityDescription(
key="prebrew_off",
translation_key="prebrew_off",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=1,
native_max_value=10,
entity_category=EntityCategory.CONFIG,
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][
0
].off_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
supported_fn=lambda coordinator: coordinator.device.model
!= MachineModel.GS3_MP,
),
LaMarzoccoKeyNumberEntityDescription(
key="prebrew_on",
translation_key="prebrew_on",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=2,
native_max_value=10,
entity_category=EntityCategory.CONFIG,
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][
0
].off_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
supported_fn=lambda coordinator: coordinator.device.model
!= MachineModel.GS3_MP,
),
LaMarzoccoKeyNumberEntityDescription(
key="preinfusion_off",
translation_key="preinfusion_off",
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS,
native_min_value=2,
native_max_value=29,
entity_category=EntityCategory.CONFIG,
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][
1
].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",
translation_key="dose",
native_unit_of_measurement="ticks",
native_step=PRECISION_WHOLE,
native_min_value=0,
native_max_value=999,
native_max_value=240,
entity_category=EntityCategory.CONFIG,
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,
),
)
SCALE_KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
LaMarzoccoKeyNumberEntityDescription(
key="scale_target",
translation_key="scale_target",
native_step=PRECISION_WHOLE,
native_min_value=1,
native_max_value=100,
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda machine, weight, key: machine.set_bbw_recipe_target(
key, int(weight)
),
native_value_fn=lambda config, key: (
config.bbw_settings.doses[key] if config.bbw_settings else None
),
supported_fn=(
lambda coordinator: coordinator.device.model
in (MachineModel.LINEA_MINI, MachineModel.LINEA_MINI_R)
and coordinator.device.config.scale is not None
set_value_fn=(
lambda machine, value: machine.set_smart_standby(
enabled=machine.schedule.smart_wake_up_sleep.smart_stand_by_enabled,
mode=machine.schedule.smart_wake_up_sleep.smart_stand_by_after,
minutes=int(value),
)
),
native_value_fn=lambda machine: machine.schedule.smart_wake_up_sleep.smart_stand_by_minutes,
),
)
@ -247,34 +93,6 @@ async def async_setup_entry(
if description.supported_fn(coordinator)
]
for description in KEY_ENTITIES:
if description.supported_fn(coordinator):
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)
)
for description in SCALE_KEY_ENTITIES:
if description.supported_fn(coordinator):
if bbw_settings := coordinator.device.config.bbw_settings:
entities.extend(
LaMarzoccoScaleTargetNumberEntity(
coordinator, description, int(key)
)
for key in bbw_settings.doses
)
def _async_add_new_scale() -> None:
if bbw_settings := coordinator.device.config.bbw_settings:
async_add_entities(
LaMarzoccoScaleTargetNumberEntity(coordinator, description, int(key))
for description in SCALE_KEY_ENTITIES
for key in bbw_settings.doses
)
coordinator.new_device_callback.append(_async_add_new_scale)
async_add_entities(entities)
@ -286,7 +104,7 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
@property
def native_value(self) -> float:
"""Return the current value."""
return self.entity_description.native_value_fn(self.coordinator.device.config)
return self.entity_description.native_value_fn(self.coordinator.device)
async def async_set_native_value(self, value: float) -> None:
"""Set the value."""
@ -305,62 +123,3 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
},
) from exc
self.async_write_ha_state()
class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity):
"""Number representing espresso machine with key support."""
entity_description: LaMarzoccoKeyNumberEntityDescription
def __init__(
self,
coordinator: LaMarzoccoUpdateCoordinator,
description: LaMarzoccoKeyNumberEntityDescription,
pyhsical_key: int,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator, description)
# Physical Key on the machine the entity represents.
if pyhsical_key == 0:
pyhsical_key = 1
else:
self._attr_translation_key = f"{description.translation_key}_key"
self._attr_translation_placeholders = {"key": str(pyhsical_key)}
self._attr_unique_id = f"{super()._attr_unique_id}_key{pyhsical_key}"
self._attr_entity_registry_enabled_default = False
self.pyhsical_key = pyhsical_key
@property
def native_value(self) -> float | None:
"""Return the current value."""
return self.entity_description.native_value_fn(
self.coordinator.device.config, PhysicalKey(self.pyhsical_key)
)
async def async_set_native_value(self, value: float) -> None:
"""Set the value."""
if value != self.native_value:
try:
await self.entity_description.set_value_fn(
self.coordinator.device, value, PhysicalKey(self.pyhsical_key)
)
except RequestNotSuccessful as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="number_exception_key",
translation_placeholders={
"key": self.entity_description.key,
"value": str(value),
"physical_key": str(self.pyhsical_key),
},
) from exc
self.async_write_ha_state()
class LaMarzoccoScaleTargetNumberEntity(
LaMarzoccoKeyNumberEntity, LaMarzoccScaleEntity
):
"""Entity representing a key number on the scale."""
entity_description: LaMarzoccoKeyNumberEntityDescription

View File

@ -2,18 +2,18 @@
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from typing import Any, cast
from pylamarzocco.const import (
MachineModel,
PhysicalKey,
PrebrewMode,
SmartStandbyMode,
SteamLevel,
ModelName,
PreExtractionMode,
SmartStandByType,
SteamTargetLevel,
WidgetType,
)
from pylamarzocco.devices.machine import LaMarzoccoMachine
from pylamarzocco.devices import LaMarzoccoMachine
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoMachineConfig
from pylamarzocco.models import PreBrewing, SteamBoilerLevel
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
@ -23,30 +23,29 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import LaMarzoccoConfigEntry
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription, LaMarzoccScaleEntity
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
PARALLEL_UPDATES = 1
STEAM_LEVEL_HA_TO_LM = {
"1": SteamLevel.LEVEL_1,
"2": SteamLevel.LEVEL_2,
"3": SteamLevel.LEVEL_3,
"1": SteamTargetLevel.LEVEL_1,
"2": SteamTargetLevel.LEVEL_2,
"3": SteamTargetLevel.LEVEL_3,
}
STEAM_LEVEL_LM_TO_HA = {value: key for key, value in STEAM_LEVEL_HA_TO_LM.items()}
PREBREW_MODE_HA_TO_LM = {
"disabled": PrebrewMode.DISABLED,
"prebrew": PrebrewMode.PREBREW,
"prebrew_enabled": PrebrewMode.PREBREW_ENABLED,
"preinfusion": PrebrewMode.PREINFUSION,
"disabled": PreExtractionMode.DISABLED,
"prebrew": PreExtractionMode.PREBREWING,
"preinfusion": PreExtractionMode.PREINFUSION,
}
PREBREW_MODE_LM_TO_HA = {value: key for key, value in PREBREW_MODE_HA_TO_LM.items()}
STANDBY_MODE_HA_TO_LM = {
"power_on": SmartStandbyMode.POWER_ON,
"last_brewing": SmartStandbyMode.LAST_BREWING,
"power_on": SmartStandByType.POWER_ON,
"last_brewing": SmartStandByType.LAST_BREW,
}
STANDBY_MODE_LM_TO_HA = {value: key for key, value in STANDBY_MODE_HA_TO_LM.items()}
@ -59,7 +58,7 @@ class LaMarzoccoSelectEntityDescription(
):
"""Description of a La Marzocco select entity."""
current_option_fn: Callable[[LaMarzoccoMachineConfig], str | None]
current_option_fn: Callable[[LaMarzoccoMachine], str | None]
select_option_fn: Callable[[LaMarzoccoMachine, str], Coroutine[Any, Any, bool]]
@ -71,25 +70,36 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
select_option_fn=lambda machine, option: machine.set_steam_level(
STEAM_LEVEL_HA_TO_LM[option]
),
current_option_fn=lambda config: STEAM_LEVEL_LM_TO_HA[config.steam_level],
supported_fn=lambda coordinator: coordinator.device.model
== MachineModel.LINEA_MICRA,
current_option_fn=lambda machine: STEAM_LEVEL_LM_TO_HA[
cast(
SteamBoilerLevel,
machine.dashboard.config[WidgetType.CM_STEAM_BOILER_LEVEL],
).target_level
],
supported_fn=(
lambda coordinator: coordinator.device.dashboard.model_name
in (ModelName.LINEA_MINI_R, ModelName.LINEA_MICRA)
),
),
LaMarzoccoSelectEntityDescription(
key="prebrew_infusion_select",
translation_key="prebrew_infusion_select",
entity_category=EntityCategory.CONFIG,
options=["disabled", "prebrew", "preinfusion"],
select_option_fn=lambda machine, option: machine.set_prebrew_mode(
select_option_fn=lambda machine, option: machine.set_pre_extraction_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 (
MachineModel.GS3_AV,
MachineModel.LINEA_MICRA,
MachineModel.LINEA_MINI,
MachineModel.LINEA_MINI_R,
current_option_fn=lambda machine: PREBREW_MODE_LM_TO_HA[
cast(PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]).mode
],
supported_fn=(
lambda coordinator: coordinator.device.dashboard.model_name
in (
ModelName.LINEA_MICRA,
ModelName.LINEA_MINI,
ModelName.LINEA_MINI_R,
ModelName.GS3_AV,
)
),
),
LaMarzoccoSelectEntityDescription(
@ -98,32 +108,16 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
options=["power_on", "last_brewing"],
select_option_fn=lambda machine, option: machine.set_smart_standby(
enabled=machine.config.smart_standby.enabled,
enabled=machine.schedule.smart_wake_up_sleep.smart_stand_by_enabled,
mode=STANDBY_MODE_HA_TO_LM[option],
minutes=machine.config.smart_standby.minutes,
minutes=machine.schedule.smart_wake_up_sleep.smart_stand_by_minutes,
),
current_option_fn=lambda config: STANDBY_MODE_LM_TO_HA[
config.smart_standby.mode
current_option_fn=lambda machine: STANDBY_MODE_LM_TO_HA[
machine.schedule.smart_wake_up_sleep.smart_stand_by_after
],
),
)
SCALE_ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
LaMarzoccoSelectEntityDescription(
key="active_bbw",
translation_key="active_bbw",
options=["a", "b"],
select_option_fn=lambda machine, option: machine.set_active_bbw_recipe(
PhysicalKey[option.upper()]
),
current_option_fn=lambda config: (
config.bbw_settings.active_dose.name.lower()
if config.bbw_settings
else None
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
@ -133,30 +127,11 @@ async def async_setup_entry(
"""Set up select entities."""
coordinator = entry.runtime_data.config_coordinator
entities = [
async_add_entities(
LaMarzoccoSelectEntity(coordinator, description)
for description in ENTITIES
if description.supported_fn(coordinator)
]
if (
coordinator.device.model in (MachineModel.LINEA_MINI, MachineModel.LINEA_MINI_R)
and coordinator.device.config.scale
):
entities.extend(
LaMarzoccoScaleSelectEntity(coordinator, description)
for description in SCALE_ENTITIES
)
def _async_add_new_scale() -> None:
async_add_entities(
LaMarzoccoScaleSelectEntity(coordinator, description)
for description in SCALE_ENTITIES
)
coordinator.new_device_callback.append(_async_add_new_scale)
async_add_entities(entities)
)
class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity):
@ -167,9 +142,7 @@ class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity):
@property
def current_option(self) -> str | None:
"""Return the current selected option."""
return str(
self.entity_description.current_option_fn(self.coordinator.device.config)
)
return self.entity_description.current_option_fn(self.coordinator.device)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
@ -188,9 +161,3 @@ class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity):
},
) from exc
self.async_write_ha_state()
class LaMarzoccoScaleSelectEntity(LaMarzoccoSelectEntity, LaMarzoccScaleEntity):
"""Select entity for La Marzocco scales."""
entity_description: LaMarzoccoSelectEntityDescription

View File

@ -1,226 +0,0 @@
"""Sensor platform for La Marzocco espresso machines."""
from collections.abc import Callable
from dataclasses import dataclass
from pylamarzocco.const import KEYS_PER_MODEL, BoilerType, MachineModel, PhysicalKey
from pylamarzocco.devices.machine import LaMarzoccoMachine
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription, LaMarzoccScaleEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class LaMarzoccoSensorEntityDescription(
LaMarzoccoEntityDescription, SensorEntityDescription
):
"""Description of a La Marzocco sensor."""
value_fn: Callable[[LaMarzoccoMachine], float | int]
@dataclass(frozen=True, kw_only=True)
class LaMarzoccoKeySensorEntityDescription(
LaMarzoccoEntityDescription, SensorEntityDescription
):
"""Description of a keyed La Marzocco sensor."""
value_fn: Callable[[LaMarzoccoMachine, PhysicalKey], int | None]
ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
LaMarzoccoSensorEntityDescription(
key="shot_timer",
translation_key="shot_timer",
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
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,
),
LaMarzoccoSensorEntityDescription(
key="current_temp_coffee",
translation_key="current_temp_coffee",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
value_fn=lambda device: device.config.boilers[
BoilerType.COFFEE
].current_temperature,
),
LaMarzoccoSensorEntityDescription(
key="current_temp_steam",
translation_key="current_temp_steam",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
value_fn=lambda device: device.config.boilers[
BoilerType.STEAM
].current_temperature,
supported_fn=lambda coordinator: coordinator.device.model
not in (MachineModel.LINEA_MINI, MachineModel.LINEA_MINI_R),
),
)
STATISTIC_ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
LaMarzoccoSensorEntityDescription(
key="drink_stats_coffee",
translation_key="drink_stats_coffee",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.statistics.total_coffee,
available_fn=lambda device: len(device.statistics.drink_stats) > 0,
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoSensorEntityDescription(
key="drink_stats_flushing",
translation_key="drink_stats_flushing",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.statistics.total_flushes,
available_fn=lambda device: len(device.statistics.drink_stats) > 0,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
KEY_STATISTIC_ENTITIES: tuple[LaMarzoccoKeySensorEntityDescription, ...] = (
LaMarzoccoKeySensorEntityDescription(
key="drink_stats_coffee_key",
translation_key="drink_stats_coffee_key",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device, key: device.statistics.drink_stats.get(key),
available_fn=lambda device: len(device.statistics.drink_stats) > 0,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
)
SCALE_ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
LaMarzoccoSensorEntityDescription(
key="scale_battery",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY,
value_fn=lambda device: (
device.config.scale.battery if device.config.scale else 0
),
supported_fn=(
lambda coordinator: coordinator.device.model
in (MachineModel.LINEA_MINI, MachineModel.LINEA_MINI_R)
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: LaMarzoccoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensor entities."""
config_coordinator = entry.runtime_data.config_coordinator
entities: list[LaMarzoccoSensorEntity | LaMarzoccoKeySensorEntity] = []
entities = [
LaMarzoccoSensorEntity(config_coordinator, description)
for description in ENTITIES
if description.supported_fn(config_coordinator)
]
if (
config_coordinator.device.model
in (MachineModel.LINEA_MINI, MachineModel.LINEA_MINI_R)
and config_coordinator.device.config.scale
):
entities.extend(
LaMarzoccoScaleSensorEntity(config_coordinator, description)
for description in SCALE_ENTITIES
)
statistics_coordinator = entry.runtime_data.statistics_coordinator
entities.extend(
LaMarzoccoSensorEntity(statistics_coordinator, description)
for description in STATISTIC_ENTITIES
if description.supported_fn(statistics_coordinator)
)
num_keys = KEYS_PER_MODEL[MachineModel(config_coordinator.device.model)]
if num_keys > 0:
entities.extend(
LaMarzoccoKeySensorEntity(statistics_coordinator, description, key)
for description in KEY_STATISTIC_ENTITIES
for key in range(1, num_keys + 1)
)
def _async_add_new_scale() -> None:
async_add_entities(
LaMarzoccoScaleSensorEntity(config_coordinator, description)
for description in SCALE_ENTITIES
)
config_coordinator.new_device_callback.append(_async_add_new_scale)
async_add_entities(entities)
class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity):
"""Sensor representing espresso machine temperature data."""
entity_description: LaMarzoccoSensorEntityDescription
@property
def native_value(self) -> int | float | None:
"""State of the sensor."""
return self.entity_description.value_fn(self.coordinator.device)
class LaMarzoccoKeySensorEntity(LaMarzoccoEntity, SensorEntity):
"""Sensor for a La Marzocco key."""
entity_description: LaMarzoccoKeySensorEntityDescription
def __init__(
self,
coordinator: LaMarzoccoUpdateCoordinator,
description: LaMarzoccoKeySensorEntityDescription,
key: int,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, description)
self.key = key
self._attr_translation_placeholders = {"key": str(key)}
self._attr_unique_id = f"{super()._attr_unique_id}_key{key}"
@property
def native_value(self) -> int | None:
"""State of the sensor."""
return self.entity_description.value_fn(
self.coordinator.device, PhysicalKey(self.key)
)
class LaMarzoccoScaleSensorEntity(LaMarzoccoSensorEntity, LaMarzoccScaleEntity):
"""Sensor for a La Marzocco scale."""
entity_description: LaMarzoccoSensorEntityDescription

View File

@ -32,13 +32,11 @@
}
},
"machine_selection": {
"description": "Select the machine you want to integrate. Set the \"IP\" to get access to shot time related sensors.",
"description": "Select the machine you want to integrate.",
"data": {
"host": "[%key:common::config_flow::data::ip%]",
"machine": "Machine"
},
"data_description": {
"host": "Local IP address of the machine",
"machine": "Select the machine you want to integrate"
}
},
@ -101,54 +99,16 @@
"coffee_temp": {
"name": "Coffee target temperature"
},
"dose_key": {
"name": "Dose Key {key}"
},
"prebrew_on": {
"name": "Prebrew on time"
},
"prebrew_on_key": {
"name": "Prebrew on time Key {key}"
},
"prebrew_off": {
"name": "Prebrew off time"
},
"prebrew_off_key": {
"name": "Prebrew off time Key {key}"
},
"preinfusion_off": {
"name": "Preinfusion time"
},
"preinfusion_off_key": {
"name": "Preinfusion time Key {key}"
},
"scale_target_key": {
"name": "Brew by weight target {key}"
},
"smart_standby_time": {
"name": "Smart standby time"
},
"steam_temp": {
"name": "Steam target temperature"
},
"tea_water_duration": {
"name": "Tea water duration"
}
},
"select": {
"active_bbw": {
"name": "Active brew by weight recipe",
"state": {
"a": "Recipe A",
"b": "Recipe B"
}
},
"prebrew_infusion_select": {
"name": "Prebrew/-infusion mode",
"state": {
"disabled": "[%key:common::state::disabled%]",
"prebrew": "Prebrew",
"prebrew_enabled": "Prebrew",
"preinfusion": "Preinfusion"
}
},
@ -168,29 +128,6 @@
}
}
},
"sensor": {
"current_temp_coffee": {
"name": "Current coffee temperature"
},
"current_temp_steam": {
"name": "Current steam temperature"
},
"drink_stats_coffee": {
"name": "Total coffees made",
"unit_of_measurement": "coffees"
},
"drink_stats_coffee_key": {
"name": "Coffees made Key {key}",
"unit_of_measurement": "coffees"
},
"drink_stats_flushing": {
"name": "Total flushes made",
"unit_of_measurement": "flushes"
},
"shot_timer": {
"name": "Shot timer"
}
},
"switch": {
"auto_on_off": {
"name": "Auto on/off ({id})"
@ -233,9 +170,6 @@
"number_exception": {
"message": "Error while setting value {value} for number {key}"
},
"number_exception_key": {
"message": "Error while setting value {value} for number {key}, key {physical_key}"
},
"select_option_error": {
"message": "Error while setting select option {option} for {key}"
},

View File

@ -2,12 +2,17 @@
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from typing import Any, cast
from pylamarzocco.const import BoilerType
from pylamarzocco.devices.machine import LaMarzoccoMachine
from pylamarzocco import LaMarzoccoMachine
from pylamarzocco.const import MachineMode, ModelName, WidgetType
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoMachineConfig
from pylamarzocco.models import (
MachineStatus,
SteamBoilerLevel,
SteamBoilerTemperature,
WakeUpScheduleSettings,
)
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
@ -30,7 +35,7 @@ class LaMarzoccoSwitchEntityDescription(
"""Description of a La Marzocco Switch."""
control_fn: Callable[[LaMarzoccoMachine, bool], Coroutine[Any, Any, bool]]
is_on_fn: Callable[[LaMarzoccoMachineConfig], bool]
is_on_fn: Callable[[LaMarzoccoMachine], bool]
ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
@ -39,13 +44,42 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
translation_key="main",
name=None,
control_fn=lambda machine, state: machine.set_power(state),
is_on_fn=lambda config: config.turned_on,
is_on_fn=(
lambda machine: cast(
MachineStatus, machine.dashboard.config[WidgetType.CM_MACHINE_STATUS]
).mode
is MachineMode.BREWING_MODE
),
),
LaMarzoccoSwitchEntityDescription(
key="steam_boiler_enable",
translation_key="steam_boiler",
control_fn=lambda machine, state: machine.set_steam(state),
is_on_fn=lambda config: config.boilers[BoilerType.STEAM].enabled,
is_on_fn=(
lambda machine: cast(
SteamBoilerLevel,
machine.dashboard.config[WidgetType.CM_STEAM_BOILER_LEVEL],
).enabled
),
supported_fn=(
lambda coordinator: coordinator.device.dashboard.model_name
in (ModelName.LINEA_MINI_R, ModelName.LINEA_MICRA)
),
),
LaMarzoccoSwitchEntityDescription(
key="steam_boiler_enable",
translation_key="steam_boiler",
control_fn=lambda machine, state: machine.set_steam(state),
is_on_fn=(
lambda machine: cast(
SteamBoilerTemperature,
machine.dashboard.config[WidgetType.CM_STEAM_BOILER_TEMPERATURE],
).enabled
),
supported_fn=(
lambda coordinator: coordinator.device.dashboard.model_name
not in (ModelName.LINEA_MINI_R, ModelName.LINEA_MICRA)
),
),
LaMarzoccoSwitchEntityDescription(
key="smart_standby_enabled",
@ -53,10 +87,10 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
control_fn=lambda machine, state: machine.set_smart_standby(
enabled=state,
mode=machine.config.smart_standby.mode,
minutes=machine.config.smart_standby.minutes,
mode=machine.schedule.smart_wake_up_sleep.smart_stand_by_after,
minutes=machine.schedule.smart_wake_up_sleep.smart_stand_by_minutes,
),
is_on_fn=lambda config: config.smart_standby.enabled,
is_on_fn=lambda machine: machine.schedule.smart_wake_up_sleep.smart_stand_by_enabled,
),
)
@ -78,8 +112,8 @@ async def async_setup_entry(
)
entities.extend(
LaMarzoccoAutoOnOffSwitchEntity(coordinator, wake_up_sleep_entry_id)
for wake_up_sleep_entry_id in coordinator.device.config.wake_up_sleep_entries
LaMarzoccoAutoOnOffSwitchEntity(coordinator, wake_up_sleep_entry)
for wake_up_sleep_entry in coordinator.device.schedule.smart_wake_up_sleep.schedules
)
async_add_entities(entities)
@ -117,7 +151,7 @@ class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity):
@property
def is_on(self) -> bool:
"""Return true if device is on."""
return self.entity_description.is_on_fn(self.coordinator.device.config)
return self.entity_description.is_on_fn(self.coordinator.device)
class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity):
@ -129,22 +163,21 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity):
def __init__(
self,
coordinator: LaMarzoccoUpdateCoordinator,
identifier: str,
schedule_entry: WakeUpScheduleSettings,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator, f"auto_on_off_{identifier}")
self._identifier = identifier
self._attr_translation_placeholders = {"id": identifier}
self.entity_category = EntityCategory.CONFIG
super().__init__(coordinator, f"auto_on_off_{schedule_entry.identifier}")
assert schedule_entry.identifier
self._schedule_entry = schedule_entry
self._identifier = schedule_entry.identifier
self._attr_translation_placeholders = {"id": schedule_entry.identifier}
self._attr_entity_category = EntityCategory.CONFIG
async def _async_enable(self, state: bool) -> None:
"""Enable or disable the auto on/off schedule."""
wake_up_sleep_entry = self.coordinator.device.config.wake_up_sleep_entries[
self._identifier
]
wake_up_sleep_entry.enabled = state
self._schedule_entry.enabled = state
try:
await self.coordinator.device.set_wake_up_sleep(wake_up_sleep_entry)
await self.coordinator.device.set_wakeup_schedule(self._schedule_entry)
except RequestNotSuccessful as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
@ -164,6 +197,4 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity):
@property
def is_on(self) -> bool:
"""Return true if switch is on."""
return self.coordinator.device.config.wake_up_sleep_entries[
self._identifier
].enabled
return self._schedule_entry.enabled

View File

@ -59,7 +59,7 @@ async def async_setup_entry(
) -> None:
"""Create update entities."""
coordinator = entry.runtime_data.firmware_coordinator
coordinator = entry.runtime_data.settings_coordinator
async_add_entities(
LaMarzoccoUpdateEntity(coordinator, description)
for description in ENTITIES
@ -74,18 +74,20 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
_attr_supported_features = UpdateEntityFeature.INSTALL
@property
def installed_version(self) -> str | None:
def installed_version(self) -> str:
"""Return the current firmware version."""
return self.coordinator.device.firmware[
return self.coordinator.device.settings.firmwares[
self.entity_description.component
].current_version
].build_version
@property
def latest_version(self) -> str:
"""Return the latest firmware version."""
return self.coordinator.device.firmware[
if available_update := self.coordinator.device.settings.firmwares[
self.entity_description.component
].latest_version
].available_update:
return available_update.build_version
return self.installed_version
@property
def release_url(self) -> str | None:
@ -99,9 +101,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
self._attr_in_progress = True
self.async_write_ha_state()
try:
success = await self.coordinator.device.update_firmware(
self.entity_description.component
)
await self.coordinator.device.update_firmware()
except RequestNotSuccessful as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
@ -110,13 +110,5 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
"key": self.entity_description.key,
},
) from exc
if not success:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={
"key": self.entity_description.key,
},
)
self._attr_in_progress = False
await self.coordinator.async_request_refresh()

View File

@ -3329,7 +3329,7 @@
"name": "La Marzocco",
"integration_type": "device",
"config_flow": true,
"iot_class": "cloud_polling"
"iot_class": "cloud_push"
},
"lametric": {
"name": "LaMetric",

2
requirements_all.txt generated
View File

@ -2089,7 +2089,7 @@ pykwb==0.0.8
pylacrosse==0.4
# homeassistant.components.lamarzocco
pylamarzocco==1.4.9
pylamarzocco==2.0.0b1
# homeassistant.components.lastfm
pylast==5.1.0

View File

@ -1704,7 +1704,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.8
# homeassistant.components.lamarzocco
pylamarzocco==1.4.9
pylamarzocco==2.0.0b1
# homeassistant.components.lastfm
pylast==5.1.0

View File

@ -1,6 +1,6 @@
"""Mock inputs for tests."""
from pylamarzocco.const import MachineModel
from pylamarzocco.const import ModelName
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
@ -19,10 +19,10 @@ PASSWORD_SELECTION = {
USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"}
SERIAL_DICT = {
MachineModel.GS3_AV: "GS012345",
MachineModel.GS3_MP: "GS012345",
MachineModel.LINEA_MICRA: "MR012345",
MachineModel.LINEA_MINI: "LM012345",
ModelName.GS3_AV: "GS012345",
ModelName.GS3_MP: "GS012345",
ModelName.LINEA_MICRA: "MR012345",
ModelName.LINEA_MINI: "LM012345",
}
WAKE_UP_SLEEP_ENTRY_IDS = ["Os2OswX", "aXFz5bJ"]
@ -37,15 +37,13 @@ async def async_init_integration(
await hass.async_block_till_done()
def get_bluetooth_service_info(
model: MachineModel, serial: str
) -> BluetoothServiceInfo:
def get_bluetooth_service_info(model: ModelName, serial: str) -> BluetoothServiceInfo:
"""Return a mocked BluetoothServiceInfo."""
if model in (MachineModel.GS3_AV, MachineModel.GS3_MP):
if model in (ModelName.GS3_AV, ModelName.GS3_MP):
name = f"GS3_{serial}"
elif model == MachineModel.LINEA_MINI:
elif model == ModelName.LINEA_MINI:
name = f"MINI_{serial}"
elif model == MachineModel.LINEA_MICRA:
elif model == ModelName.LINEA_MICRA:
name = f"MICRA_{serial}"
return BluetoothServiceInfo(
name=name,

View File

@ -1,28 +1,25 @@
"""Lamarzocco session fixtures."""
from collections.abc import Generator
import json
from unittest.mock import AsyncMock, MagicMock, patch
from bleak.backends.device import BLEDevice
from pylamarzocco.const import FirmwareType, MachineModel, SteamLevel
from pylamarzocco.devices.machine import LaMarzoccoMachine
from pylamarzocco.models import LaMarzoccoDeviceInfo
from pylamarzocco.const import ModelName
from pylamarzocco.models import (
Thing,
ThingDashboardConfig,
ThingSchedulingSettings,
ThingSettings,
)
import pytest
from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.const import (
CONF_ADDRESS,
CONF_HOST,
CONF_MODEL,
CONF_NAME,
CONF_TOKEN,
)
from homeassistant.const import CONF_ADDRESS, CONF_TOKEN
from homeassistant.core import HomeAssistant
from . import SERIAL_DICT, USER_INPUT, async_init_integration
from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture
from tests.common import MockConfigEntry, load_json_object_fixture
@pytest.fixture
@ -42,33 +39,11 @@ def mock_config_entry(
return MockConfigEntry(
title="My LaMarzocco",
domain=DOMAIN,
version=2,
version=3,
data=USER_INPUT
| {
CONF_MODEL: mock_lamarzocco.model,
CONF_ADDRESS: "00:00:00:00:00:00",
CONF_HOST: "host",
CONF_TOKEN: "token",
CONF_NAME: "GS3",
},
unique_id=mock_lamarzocco.serial_number,
)
@pytest.fixture
def mock_config_entry_no_local_connection(
hass: HomeAssistant, mock_lamarzocco: MagicMock
) -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="My LaMarzocco",
domain=DOMAIN,
version=2,
data=USER_INPUT
| {
CONF_MODEL: mock_lamarzocco.model,
CONF_TOKEN: "token",
CONF_NAME: "GS3",
},
unique_id=mock_lamarzocco.serial_number,
)
@ -85,26 +60,13 @@ async def init_integration(
@pytest.fixture
def device_fixture() -> MachineModel:
def device_fixture() -> ModelName:
"""Return the device fixture for a specific device."""
return MachineModel.GS3_AV
return ModelName.GS3_AV
@pytest.fixture
def mock_device_info(device_fixture: MachineModel) -> LaMarzoccoDeviceInfo:
"""Return a mocked La Marzocco device info."""
return LaMarzoccoDeviceInfo(
model=device_fixture,
serial_number=SERIAL_DICT[device_fixture],
name="GS3",
communication_key="token",
)
@pytest.fixture
def mock_cloud_client(
mock_device_info: LaMarzoccoDeviceInfo,
) -> Generator[MagicMock]:
@pytest.fixture(autouse=True)
def mock_cloud_client() -> Generator[MagicMock]:
"""Return a mocked LM cloud client."""
with (
patch(
@ -117,54 +79,48 @@ def mock_cloud_client(
),
):
client = cloud_client.return_value
client.get_customer_fleet.return_value = {
mock_device_info.serial_number: mock_device_info
}
client.list_things.return_value = [
Thing.from_dict(load_json_object_fixture("thing.json", DOMAIN))
]
client.get_thing_settings.return_value = ThingSettings.from_dict(
load_json_object_fixture("settings.json", DOMAIN)
)
yield client
@pytest.fixture
def mock_lamarzocco(device_fixture: MachineModel) -> Generator[MagicMock]:
def mock_lamarzocco(device_fixture: ModelName) -> 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,
)
if device_fixture == MachineModel.LINEA_MINI:
if device_fixture == ModelName.LINEA_MINI:
config = load_json_object_fixture("config_mini.json", DOMAIN)
elif device_fixture == ModelName.LINEA_MICRA:
config = load_json_object_fixture("config_micra.json", DOMAIN)
else:
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)
config = load_json_object_fixture("config_gs3.json", DOMAIN)
schedule = load_json_object_fixture("schedule.json", DOMAIN)
settings = load_json_object_fixture("settings.json", DOMAIN)
with (
patch(
"homeassistant.components.lamarzocco.LaMarzoccoMachine",
autospec=True,
) as lamarzocco_mock,
) as machine_mock_init,
):
lamarzocco = lamarzocco_mock.return_value
machine_mock = machine_mock_init.return_value
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.firmware[FirmwareType.GATEWAY].latest_version = "v3.5-rc3"
lamarzocco.firmware[FirmwareType.MACHINE].latest_version = "1.55"
yield lamarzocco
machine_mock.serial_number = SERIAL_DICT[device_fixture]
machine_mock.dashboard = ThingDashboardConfig.from_dict(config)
machine_mock.schedule = ThingSchedulingSettings.from_dict(schedule)
machine_mock.settings = ThingSettings.from_dict(settings)
machine_mock.dashboard.model_name = device_fixture
machine_mock.to_dict.return_value = {
"serial_number": machine_mock.serial_number,
"dashboard": machine_mock.dashboard.to_dict(),
"schedule": machine_mock.schedule.to_dict(),
"settings": machine_mock.settings.to_dict(),
}
yield machine_mock
@pytest.fixture(autouse=True)

View File

@ -1,198 +0,0 @@
{
"version": "v1",
"preinfusionModesAvailable": ["ByDoseType"],
"machineCapabilities": [
{
"family": "GS3AV",
"groupsNumber": 1,
"coffeeBoilersNumber": 1,
"hasCupWarmer": false,
"steamBoilersNumber": 1,
"teaDosesNumber": 1,
"machineModes": ["BrewingMode", "StandBy"],
"schedulingType": "weeklyScheduling"
}
],
"machine_sn": "Sn01239157",
"machine_hw": "2",
"isPlumbedIn": true,
"isBackFlushEnabled": false,
"standByTime": 0,
"smartStandBy": {
"enabled": true,
"minutes": 10,
"mode": "LastBrewing"
},
"tankStatus": true,
"groupCapabilities": [
{
"capabilities": {
"groupType": "AV_Group",
"groupNumber": "Group1",
"boilerId": "CoffeeBoiler1",
"hasScale": false,
"hasFlowmeter": true,
"numberOfDoses": 4
},
"doses": [
{
"groupNumber": "Group1",
"doseIndex": "DoseA",
"doseType": "PulsesType",
"stopTarget": 135
},
{
"groupNumber": "Group1",
"doseIndex": "DoseB",
"doseType": "PulsesType",
"stopTarget": 97
},
{
"groupNumber": "Group1",
"doseIndex": "DoseC",
"doseType": "PulsesType",
"stopTarget": 108
},
{
"groupNumber": "Group1",
"doseIndex": "DoseD",
"doseType": "PulsesType",
"stopTarget": 121
}
],
"doseMode": {
"groupNumber": "Group1",
"brewingType": "PulsesType"
}
}
],
"machineMode": "BrewingMode",
"teaDoses": {
"DoseA": {
"doseIndex": "DoseA",
"stopTarget": 8
}
},
"boilers": [
{
"id": "SteamBoiler",
"isEnabled": true,
"target": 123.90000152587891,
"current": 123.80000305175781
},
{
"id": "CoffeeBoiler1",
"isEnabled": true,
"target": 95,
"current": 96.5
}
],
"boilerTargetTemperature": {
"SteamBoiler": 123.90000152587891,
"CoffeeBoiler1": 95
},
"preinfusionMode": {
"Group1": {
"groupNumber": "Group1",
"preinfusionStyle": "PreinfusionByDoseType"
}
},
"preinfusionSettings": {
"mode": "TypeB",
"Group1": [
{
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "DoseA",
"preWetTime": 0.5,
"preWetHoldTime": 1
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseA",
"preWetTime": 0,
"preWetHoldTime": 4
},
{
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "DoseB",
"preWetTime": 0.5,
"preWetHoldTime": 1
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseB",
"preWetTime": 0,
"preWetHoldTime": 4
},
{
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "DoseC",
"preWetTime": 3.3,
"preWetHoldTime": 3.3
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseC",
"preWetTime": 0,
"preWetHoldTime": 4
},
{
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "DoseD",
"preWetTime": 2,
"preWetHoldTime": 2
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseD",
"preWetTime": 0,
"preWetHoldTime": 4
}
]
},
"wakeUpSleepEntries": [
{
"days": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday"
],
"enabled": true,
"id": "Os2OswX",
"steam": true,
"timeOff": "24:0",
"timeOn": "22:0"
},
{
"days": ["sunday"],
"enabled": true,
"id": "aXFz5bJ",
"steam": true,
"timeOff": "7:30",
"timeOn": "7:0"
}
],
"clock": "1901-07-08T10:29:00",
"firmwareVersions": [
{
"name": "machine_firmware",
"fw_version": "1.40"
},
{
"name": "gateway_firmware",
"fw_version": "v3.1-rc4"
}
]
}

View File

@ -0,0 +1,377 @@
{
"serialNumber": "GS012345",
"type": "CoffeeMachine",
"name": "GS012345",
"location": "HOME",
"modelCode": "GS3AV",
"modelName": "GS3AV",
"connected": true,
"connectionDate": 1742489087479,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": false,
"coffeeStation": null,
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/detail/gs3av/gs3av-1.png",
"bleAuthToken": null,
"widgets": [
{
"code": "CMMachineStatus",
"index": 1,
"output": {
"status": "PoweredOn",
"availableModes": ["BrewingMode", "StandBy"],
"mode": "BrewingMode",
"nextStatus": {
"status": "StandBy",
"startTime": 1742857195332
},
"brewingStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMCoffeeBoiler",
"index": 1,
"output": {
"status": "Ready",
"enabled": true,
"enabledSupported": false,
"targetTemperature": 95.0,
"targetTemperatureMin": 80,
"targetTemperatureMax": 110,
"targetTemperatureStep": 0.1,
"readyStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMSteamBoilerTemperature",
"index": 1,
"output": {
"status": "Off",
"enabled": true,
"enabledSupported": true,
"targetTemperature": 123.9,
"targetTemperatureSupported": true,
"targetTemperatureMin": 95,
"targetTemperatureMax": 140,
"targetTemperatureStep": 0.1,
"readyStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMGroupDoses",
"index": 1,
"output": {
"mirrorWithGroup1Supported": false,
"mirrorWithGroup1": null,
"mirrorWithGroup1NotEffective": false,
"availableModes": ["PulsesType"],
"mode": "PulsesType",
"profile": null,
"doses": {
"PulsesType": [
{
"doseIndex": "DoseA",
"dose": 126.0,
"doseMin": 0,
"doseMax": 9999,
"doseStep": 1
},
{
"doseIndex": "DoseB",
"dose": 126.0,
"doseMin": 0,
"doseMax": 9999,
"doseStep": 1
},
{
"doseIndex": "DoseC",
"dose": 160.0,
"doseMin": 0,
"doseMax": 9999,
"doseStep": 1
},
{
"doseIndex": "DoseD",
"dose": 77.0,
"doseMin": 0,
"doseMax": 9999,
"doseStep": 1
}
]
},
"continuousDoseSupported": false,
"continuousDose": null,
"brewingPressureSupported": false,
"brewingPressure": null
},
"tutorialUrl": null
},
{
"code": "CMPreBrewing",
"index": 1,
"output": {
"availableModes": ["PreBrewing", "PreInfusion", "Disabled"],
"mode": "PreInfusion",
"times": {
"PreBrewing": [
{
"doseIndex": "DoseA",
"seconds": {
"In": 0.5,
"Out": 1.0
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 10,
"Out": 10
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
},
{
"doseIndex": "DoseB",
"seconds": {
"In": 0.5,
"Out": 1.0
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 10,
"Out": 10
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
},
{
"doseIndex": "DoseC",
"seconds": {
"In": 3.3,
"Out": 3.3
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 10,
"Out": 10
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
},
{
"doseIndex": "DoseD",
"seconds": {
"In": 2.0,
"Out": 2.0
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 10,
"Out": 10
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
}
],
"PreInfusion": [
{
"doseIndex": "DoseA",
"seconds": {
"In": 0.0,
"Out": 4.0
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 25,
"Out": 25
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
},
{
"doseIndex": "DoseB",
"seconds": {
"In": 0.0,
"Out": 4.0
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 25,
"Out": 25
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
},
{
"doseIndex": "DoseC",
"seconds": {
"In": 0.0,
"Out": 4.0
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 25,
"Out": 25
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
},
{
"doseIndex": "DoseD",
"seconds": {
"In": 0.0,
"Out": 4.0
},
"secondsMin": {
"In": 0,
"Out": 0
},
"secondsMax": {
"In": 25,
"Out": 25
},
"secondsStep": {
"In": 0.1,
"Out": 0.1
}
}
]
},
"doseIndexSupported": true
},
"tutorialUrl": "https://www.lamarzocco.com/it/en/app/support/brewing-features/#gs3-av-linea-micra-linea-mini-home"
},
{
"code": "CMHotWaterDose",
"index": 1,
"output": {
"enabledSupported": false,
"enabled": true,
"doses": [
{
"doseIndex": "DoseA",
"dose": 8.0,
"doseMin": 0,
"doseMax": 90,
"doseStep": 1
}
]
},
"tutorialUrl": null
},
{
"code": "CMBackFlush",
"index": 1,
"output": {
"lastCleaningStartTime": null,
"status": "Off"
},
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/cleaning-and-backflush/#gs3-av"
}
],
"invalidWidgets": [
{
"code": "CMMachineGroupStatus",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMSteamBoilerLevel",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMPreExtraction",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMPreInfusionEnable",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMPreInfusion",
"index": 1,
"output": null,
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/brewing-features/#commercial"
},
{
"code": "CMBrewByWeightDoses",
"index": 1,
"output": null,
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/brew-by-weight"
},
{
"code": "CMCupWarmer",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMAutoFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMRinseFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMSteamFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMNoWater",
"index": 1,
"output": null,
"tutorialUrl": null
}
],
"runningCommands": []
}

View File

@ -0,0 +1,237 @@
{
"serialNumber": "MR012345",
"type": "CoffeeMachine",
"name": "MR012345",
"location": null,
"modelCode": "LINEAMICRA",
"modelName": "LINEA MICRA",
"connected": true,
"connectionDate": 1742526019892,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": false,
"coffeeStation": null,
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png",
"bleAuthToken": null,
"widgets": [
{
"code": "CMMachineStatus",
"index": 1,
"output": {
"status": "StandBy",
"availableModes": ["BrewingMode", "StandBy"],
"mode": "StandBy",
"nextStatus": null,
"brewingStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMCoffeeBoiler",
"index": 1,
"output": {
"status": "StandBy",
"enabled": true,
"enabledSupported": false,
"targetTemperature": 94.0,
"targetTemperatureMin": 80,
"targetTemperatureMax": 100,
"targetTemperatureStep": 0.1,
"readyStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMSteamBoilerLevel",
"index": 1,
"output": {
"status": "StandBy",
"enabled": true,
"enabledSupported": true,
"targetLevel": "Level3",
"targetLevelSupported": true,
"readyStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMPreExtraction",
"index": 1,
"output": {
"availableModes": ["PreBrewing", "PreInfusion", "Disabled"],
"mode": "PreInfusion",
"times": {
"In": {
"seconds": 0.0,
"secondsMin": {
"PreBrewing": 2,
"PreInfusion": 2
},
"secondsMax": {
"PreBrewing": 9,
"PreInfusion": 9
},
"secondsStep": {
"PreBrewing": 0.1,
"PreInfusion": 0.1
}
},
"Out": {
"seconds": 4.0,
"secondsMin": {
"PreBrewing": 1,
"PreInfusion": 1
},
"secondsMax": {
"PreBrewing": 9,
"PreInfusion": 25
},
"secondsStep": {
"PreBrewing": 0.1,
"PreInfusion": 0.1
}
}
}
},
"tutorialUrl": null
},
{
"code": "CMPreBrewing",
"index": 1,
"output": {
"availableModes": ["PreBrewing", "PreInfusion", "Disabled"],
"mode": "PreInfusion",
"times": {
"PreInfusion": [
{
"doseIndex": "ByGroup",
"seconds": {
"Out": 4.0,
"In": 0.0
},
"secondsMin": {
"Out": 1,
"In": 1
},
"secondsMax": {
"Out": 25,
"In": 25
},
"secondsStep": {
"Out": 0.1,
"In": 0.1
}
}
],
"PreBrewing": [
{
"doseIndex": "ByGroup",
"seconds": {
"Out": 5.0,
"In": 5.0
},
"secondsMin": {
"Out": 1,
"In": 1
},
"secondsMax": {
"Out": 9,
"In": 9
},
"secondsStep": {
"Out": 0.1,
"In": 0.1
}
}
]
},
"doseIndexSupported": false
},
"tutorialUrl": "https://www.lamarzocco.com/it/en/app/support/brewing-features/#gs3-av-linea-micra-linea-mini-home"
},
{
"code": "CMBackFlush",
"index": 1,
"output": {
"lastCleaningStartTime": null,
"status": "Off"
},
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/cleaning-and-backflush/#linea-micra"
}
],
"invalidWidgets": [
{
"code": "CMMachineGroupStatus",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMSteamBoilerTemperature",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMGroupDoses",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMPreInfusionEnable",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMPreInfusion",
"index": 1,
"output": null,
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/brewing-features/#commercial"
},
{
"code": "CMBrewByWeightDoses",
"index": 1,
"output": null,
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/brew-by-weight"
},
{
"code": "CMCupWarmer",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMHotWaterDose",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMAutoFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMRinseFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMSteamFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMNoWater",
"index": 1,
"output": null,
"tutorialUrl": null
}
],
"runningCommands": []
}

View File

@ -1,124 +1,284 @@
{
"version": "v1",
"preinfusionModesAvailable": ["ByDoseType"],
"machineCapabilities": [
{
"family": "LINEA",
"groupsNumber": 1,
"coffeeBoilersNumber": 1,
"hasCupWarmer": false,
"steamBoilersNumber": 1,
"teaDosesNumber": 1,
"machineModes": ["BrewingMode", "StandBy"],
"schedulingType": "smartWakeUpSleep"
}
],
"machine_sn": "Sn01239157",
"machine_hw": "0",
"isPlumbedIn": false,
"isBackFlushEnabled": false,
"standByTime": 0,
"tankStatus": true,
"settings": [],
"recipes": [
{
"id": "Recipe1",
"dose_mode": "Mass",
"recipe_doses": [
{ "id": "A", "target": 32 },
{ "id": "B", "target": 45 }
]
}
],
"recipeAssignment": [
{
"dose_index": "DoseA",
"recipe_id": "Recipe1",
"recipe_dose": "A",
"group": "Group1"
}
],
"groupCapabilities": [
{
"capabilities": {
"groupType": "AV_Group",
"groupNumber": "Group1",
"boilerId": "CoffeeBoiler1",
"hasScale": false,
"hasFlowmeter": false,
"numberOfDoses": 1
},
"doses": [
{
"groupNumber": "Group1",
"doseIndex": "DoseA",
"doseType": "MassType",
"stopTarget": 32
}
],
"doseMode": { "groupNumber": "Group1", "brewingType": "ManualType" }
}
],
"machineMode": "StandBy",
"teaDoses": { "DoseA": { "doseIndex": "DoseA", "stopTarget": 0 } },
"scale": {
"connected": true,
"address": "44:b7:d0:74:5f:90",
"name": "LMZ-123A45",
"battery": 64
},
"boilers": [
{ "id": "SteamBoiler", "isEnabled": false, "target": 0, "current": 0 },
{ "id": "CoffeeBoiler1", "isEnabled": true, "target": 89, "current": 42 }
],
"boilerTargetTemperature": { "SteamBoiler": 0, "CoffeeBoiler1": 89 },
"preinfusionMode": {
"Group1": {
"groupNumber": "Group1",
"preinfusionStyle": "PreinfusionByDoseType"
}
},
"preinfusionSettings": {
"mode": "TypeB",
"Group1": [
"serialNumber": "LM012345",
"type": "CoffeeMachine",
"name": "LM012345",
"location": null,
"modelCode": "LINEAMINI",
"modelName": "LINEA MINI",
"connected": true,
"connectionDate": 1742683649814,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": true,
"coffeeStation": {
"id": "a59cd870-dc75-428f-b73e-e5a247c6db73",
"name": "My coffee station",
"coffeeMachine": {
"serialNumber": "LM012345",
"type": "CoffeeMachine",
"name": null,
"location": null,
"modelCode": "LINEAMINI",
"modelName": "LINEA MINI",
"connected": true,
"connectionDate": 1742683649814,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": true,
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/list/lineamini/lineamini-1-c-nero_op.png",
"bleAuthToken": null
},
"grinders": [],
"accessories": [
{
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "Continuous",
"preWetTime": 2,
"preWetHoldTime": 3
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "Continuous",
"preWetTime": 0,
"preWetHoldTime": 3
"type": "ScaleAcaiaLunar",
"name": "LMZ-123A12",
"connected": false,
"batteryLevel": null,
"imageUrl": null
}
]
},
"wakeUpSleepEntries": [
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/detail/lineamini/lineamini-1-c-nero_op.png",
"bleAuthToken": null,
"widgets": [
{
"id": "T6aLl42",
"days": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday"
],
"steam": false,
"enabled": false,
"timeOn": "24:0",
"timeOff": "24:0"
"code": "CMMachineStatus",
"index": 1,
"output": {
"status": "StandBy",
"availableModes": ["BrewingMode", "StandBy"],
"mode": "StandBy",
"nextStatus": null,
"brewingStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMCoffeeBoiler",
"index": 1,
"output": {
"status": "StandBy",
"enabled": true,
"enabledSupported": false,
"targetTemperature": 90.0,
"targetTemperatureMin": 80,
"targetTemperatureMax": 100,
"targetTemperatureStep": 0.1,
"readyStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMSteamBoilerTemperature",
"index": 1,
"output": {
"status": "Off",
"enabled": false,
"enabledSupported": true,
"targetTemperature": 0.0,
"targetTemperatureSupported": false,
"targetTemperatureMin": 95,
"targetTemperatureMax": 140,
"targetTemperatureStep": 0.1,
"readyStartTime": null
},
"tutorialUrl": null
},
{
"code": "CMPreExtraction",
"index": 1,
"output": {
"availableModes": ["PreBrewing", "Disabled"],
"mode": "Disabled",
"times": {
"In": {
"seconds": 2.0,
"secondsMin": {
"PreBrewing": 2,
"PreInfusion": 2
},
"secondsMax": {
"PreBrewing": 9,
"PreInfusion": 9
},
"secondsStep": {
"PreBrewing": 0.1,
"PreInfusion": 0.1
}
},
"Out": {
"seconds": 3.0,
"secondsMin": {
"PreBrewing": 1,
"PreInfusion": 1
},
"secondsMax": {
"PreBrewing": 9,
"PreInfusion": 25
},
"secondsStep": {
"PreBrewing": 0.1,
"PreInfusion": 0.1
}
}
}
},
"tutorialUrl": null
},
{
"code": "CMPreBrewing",
"index": 1,
"output": {
"availableModes": ["PreBrewing", "Disabled"],
"mode": "Disabled",
"times": {
"PreBrewing": [
{
"doseIndex": "ByGroup",
"seconds": {
"Out": 3.0,
"In": 2.0
},
"secondsMin": {
"Out": 1,
"In": 1
},
"secondsMax": {
"Out": 9,
"In": 9
},
"secondsStep": {
"Out": 0.1,
"In": 0.1
}
}
]
},
"doseIndexSupported": false
},
"tutorialUrl": "https://www.lamarzocco.com/it/en/app/support/brewing-features/#gs3-av-linea-micra-linea-mini-home"
},
{
"code": "CMBrewByWeightDoses",
"index": 1,
"output": {
"scaleConnected": false,
"availableModes": ["Continuous"],
"mode": "Continuous",
"doses": {
"Dose1": {
"dose": 34.5,
"doseMin": 5,
"doseMax": 100,
"doseStep": 0.1
},
"Dose2": {
"dose": 17.5,
"doseMin": 5,
"doseMax": 100,
"doseStep": 0.1
}
}
},
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/brew-by-weight"
},
{
"code": "CMBackFlush",
"index": 1,
"output": {
"lastCleaningStartTime": 1742731776135,
"status": "Off"
},
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/cleaning-and-backflush/#linea-mini"
},
{
"code": "ThingScale",
"index": 2,
"output": {
"name": "LMZ-123A12",
"connected": false,
"batteryLevel": 0.0,
"calibrationRequired": false
},
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/training-scale-location/#linea-mini"
}
],
"smartStandBy": { "mode": "LastBrewing", "minutes": 10, "enabled": true },
"clock": "2024-08-31T14:47:45",
"firmwareVersions": [
{ "name": "machine_firmware", "fw_version": "2.12" },
{ "name": "gateway_firmware", "fw_version": "v3.6-rc4" }
]
"invalidWidgets": [
{
"code": "CMMachineGroupStatus",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMSteamBoilerLevel",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMGroupDoses",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMPreInfusionEnable",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMPreInfusion",
"index": 1,
"output": null,
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/brewing-features/#commercial"
},
{
"code": "CMCupWarmer",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMHotWaterDose",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMAutoFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMRinseFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMSteamFlush",
"index": 1,
"output": null,
"tutorialUrl": null
},
{
"code": "CMNoWater",
"index": 1,
"output": {
"allarm": false
},
"tutorialUrl": null
},
{
"code": "ThingScale",
"index": 1,
"output": null,
"tutorialUrl": "http://lamarzocco.com/it/en/app/support/training-scale-location/#linea-mini"
}
],
"runningCommands": []
}

View File

@ -0,0 +1,61 @@
{
"serialNumber": "MR123456",
"type": "CoffeeMachine",
"name": "MR123456",
"location": null,
"modelCode": "LINEAMICRA",
"modelName": "LINEA MICRA",
"connected": true,
"connectionDate": 1742526019892,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": false,
"coffeeStation": null,
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png",
"bleAuthToken": null,
"smartWakeUpSleepSupported": true,
"smartWakeUpSleep": {
"smartStandByEnabled": true,
"smartStandByMinutes": 10,
"smartStandByMinutesMin": 1,
"smartStandByMinutesMax": 30,
"smartStandByMinutesStep": 1,
"smartStandByAfter": "PowerOn",
"schedules": [
{
"id": "Os2OswX",
"enabled": true,
"onTimeMinutes": 1320,
"offTimeMinutes": 1440,
"days": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
],
"steamBoiler": true
},
{
"id": "aXFz5bJ",
"enabled": true,
"onTimeMinutes": 420,
"offTimeMinutes": 450,
"days": ["Sunday"],
"steamBoiler": false
}
]
},
"smartWakeUpSleepTutorialUrl": "https://www.lamarzocco.com/it/en/app/support/scheduling/#gs3-linea-micra-linea-mini-home",
"weeklySupported": false,
"weekly": null,
"weeklyTutorialUrl": "https://www.lamarzocco.com/it/en/app/support/scheduling/#linea-classic-s",
"autoOnOffSupported": false,
"autoOnOff": null,
"autoOnOffTutorialUrl": "https://www.lamarzocco.com/it/en/app/support/scheduling/#gb5-s-x-kb90-linea-pb-pbx-strada-s-x-commercial",
"autoStandBySupported": false,
"autoStandBy": null,
"autoStandByTutorialUrl": null
}

View File

@ -0,0 +1,50 @@
{
"serialNumber": "MR123456",
"type": "CoffeeMachine",
"name": "MR123456",
"location": null,
"modelCode": "LINEAMICRA",
"modelName": "LINEA MICRA",
"connected": true,
"connectionDate": 1742526019892,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": false,
"coffeeStation": null,
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png",
"bleAuthToken": null,
"actualFirmwares": [
{
"type": "Gateway",
"buildVersion": "v5.0.9",
"changeLog": "Whats new in this version:\n\n* New La Marzocco compatibility\n* Improved connectivity\n* Improved pairing process\n* Improved statistics\n* Boilers heating time\n* Last backflush date (GS3 MP excluded)\n* Automatic gateway updates option",
"thingModelCode": "LineaMicra",
"status": "ToUpdate",
"availableUpdate": {
"type": "Gateway",
"buildVersion": "v5.0.10",
"changeLog": "Whats new in this version:\n\n* fixed an issue that could cause the machine powers up outside scheduled time\n* minor improvements",
"thingModelCode": "LineaMicra"
}
},
{
"type": "Machine",
"buildVersion": "v1.17",
"changeLog": null,
"thingModelCode": "LineaMicra",
"status": "Updated",
"availableUpdate": null
}
],
"wifiSsid": "MyWifi",
"wifiRssi": -51,
"plumbInSupported": true,
"isPlumbedIn": true,
"cropsterSupported": false,
"cropsterActive": null,
"hemroSupported": false,
"hemroActive": null,
"factoryResetSupported": true,
"autoUpdateSupported": true,
"autoUpdate": false
}

View File

@ -1,26 +0,0 @@
[
{
"count": 1047,
"coffeeType": 0
},
{
"count": 560,
"coffeeType": 1
},
{
"count": 468,
"coffeeType": 2
},
{
"count": 312,
"coffeeType": 3
},
{
"count": 2252,
"coffeeType": 4
},
{
"coffeeType": -1,
"count": 1740
}
]

View File

@ -0,0 +1,16 @@
{
"serialNumber": "GS012345",
"type": "CoffeeMachine",
"name": "GS012345",
"location": "HOME",
"modelCode": "GS3AV",
"modelName": "GS3AV",
"connected": true,
"connectionDate": 1742489087479,
"offlineMode": false,
"requireFirmwareUpdate": false,
"availableFirmwareUpdate": false,
"coffeeStation": null,
"imageUrl": "https://lion.lamarzocco.io/img/thing-model/detail/gs3av/gs3av-1.png",
"bleAuthToken": null
}

View File

@ -143,51 +143,3 @@
'state': 'off',
})
# ---
# name: test_scale_connectivity[Linea Mini]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'LMZ-123A45 Connectivity',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.lmz_123a45_connectivity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_scale_connectivity[Linea Mini].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.lmz_123a45_connectivity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Connectivity',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'LM012345_connected',
'unit_of_measurement': None,
})
# ---

View File

@ -1,135 +1,766 @@
# serializer version: 1
# name: test_diagnostics
dict({
'config': dict({
'backflush_enabled': False,
'bbw_settings': None,
'boilers': dict({
'CoffeeBoiler1': dict({
'current_temperature': 96.5,
'enabled': True,
'target_temperature': 95,
'dashboard': dict({
'available_firmware_update': False,
'ble_auth_token': None,
'coffee_station': None,
'config': dict({
'CMBackFlush': dict({
'last_cleaning_start_time': None,
'status': 'Off',
}),
'SteamBoiler': dict({
'current_temperature': 123.80000305175781,
'CMCoffeeBoiler': dict({
'enabled': True,
'target_temperature': 123.9000015258789,
'enabled_supported': False,
'ready_start_time': None,
'status': 'Ready',
'target_temperature': 95.0,
'target_temperature_max': 110,
'target_temperature_min': 80,
'target_temperature_step': 0.1,
}),
}),
'brew_active': False,
'brew_active_duration': 0,
'dose_hot_water': 8,
'doses': dict({
'1': 135,
'2': 97,
'3': 108,
'4': 121,
}),
'plumbed_in': True,
'prebrew_configuration': dict({
'1': list([
dict({
'off_time': 1,
'on_time': 0.5,
'CMGroupDoses': dict({
'available_modes': list([
'PulsesType',
]),
'brewing_pressure': None,
'brewing_pressure_supported': False,
'continuous_dose': None,
'continuous_dose_supported': False,
'doses': dict({
'pulses_type': list([
dict({
'dose': 126.0,
'dose_index': 'DoseA',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
dict({
'dose': 126.0,
'dose_index': 'DoseB',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
dict({
'dose': 160.0,
'dose_index': 'DoseC',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
dict({
'dose': 77.0,
'dose_index': 'DoseD',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
]),
}),
dict({
'off_time': 4,
'on_time': 0,
}),
]),
'2': list([
dict({
'off_time': 1,
'on_time': 0.5,
}),
dict({
'off_time': 4,
'on_time': 0,
}),
]),
'3': list([
dict({
'off_time': 3.3,
'on_time': 3.3,
}),
dict({
'off_time': 4,
'on_time': 0,
}),
]),
'4': list([
dict({
'off_time': 2,
'on_time': 2,
}),
dict({
'off_time': 4,
'on_time': 0,
}),
]),
}),
'prebrew_mode': 'TypeB',
'scale': None,
'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',
'mirror_with_group_1': None,
'mirror_with_group_1_not_effective': False,
'mirror_with_group_1_supported': False,
'mode': 'PulsesType',
'profile': None,
}),
'CMHotWaterDose': dict({
'doses': list([
dict({
'dose': 8.0,
'dose_index': 'DoseA',
'dose_max': 90.0,
'dose_min': 0.0,
'dose_step': 1,
}),
]),
'enabled': True,
'entry_id': 'Os2OswX',
'steam': True,
'time_off': '24:0',
'time_on': '22:0',
'enabled_supported': False,
}),
'aXFz5bJ': dict({
'days': list([
'sunday',
'CMMachineStatus': dict({
'available_modes': list([
'BrewingMode',
'StandBy',
]),
'brewing_start_time': None,
'mode': 'BrewingMode',
'next_status': dict({
'start_time': '2025-03-24T22:59:55.332000+00:00',
'status': 'StandBy',
}),
'status': 'PoweredOn',
}),
'CMPreBrewing': dict({
'available_modes': list([
'PreBrewing',
'PreInfusion',
'Disabled',
]),
'dose_index_supported': True,
'mode': 'PreInfusion',
'times': dict({
'pre_brewing': list([
dict({
'dose_index': 'DoseA',
'seconds': dict({
'In': 0.5,
'Out': 1.0,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseB',
'seconds': dict({
'In': 0.5,
'Out': 1.0,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseC',
'seconds': dict({
'In': 3.3,
'Out': 3.3,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseD',
'seconds': dict({
'In': 2.0,
'Out': 2.0,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
]),
'pre_infusion': list([
dict({
'dose_index': 'DoseA',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseB',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseC',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseD',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
]),
}),
}),
'CMSteamBoilerTemperature': dict({
'enabled': True,
'entry_id': 'aXFz5bJ',
'steam': True,
'time_off': '7:30',
'time_on': '7:0',
'enabled_supported': True,
'ready_start_time': None,
'status': 'Off',
'target_temperature': 123.9,
'target_temperature_max': 140,
'target_temperature_min': 95,
'target_temperature_step': 0.1,
'target_temperature_supported': True,
}),
}),
'water_contact': True,
'connected': True,
'connection_date': '2025-03-20T16:44:47.479000+00:00',
'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/gs3av/gs3av-1.png',
'location': 'HOME',
'model_code': 'GS3AV',
'model_name': 'GS3 AV',
'name': 'GS012345',
'offline_mode': False,
'require_firmware_update': False,
'serial_number': '**REDACTED**',
'type': 'CoffeeMachine',
'widgets': list([
dict({
'code': 'CMMachineStatus',
'index': 1,
'output': dict({
'available_modes': list([
'BrewingMode',
'StandBy',
]),
'brewing_start_time': None,
'mode': 'BrewingMode',
'next_status': dict({
'start_time': '2025-03-24T22:59:55.332000+00:00',
'status': 'StandBy',
}),
'status': 'PoweredOn',
}),
}),
dict({
'code': 'CMCoffeeBoiler',
'index': 1,
'output': dict({
'enabled': True,
'enabled_supported': False,
'ready_start_time': None,
'status': 'Ready',
'target_temperature': 95.0,
'target_temperature_max': 110,
'target_temperature_min': 80,
'target_temperature_step': 0.1,
}),
}),
dict({
'code': 'CMSteamBoilerTemperature',
'index': 1,
'output': dict({
'enabled': True,
'enabled_supported': True,
'ready_start_time': None,
'status': 'Off',
'target_temperature': 123.9,
'target_temperature_max': 140,
'target_temperature_min': 95,
'target_temperature_step': 0.1,
'target_temperature_supported': True,
}),
}),
dict({
'code': 'CMGroupDoses',
'index': 1,
'output': dict({
'available_modes': list([
'PulsesType',
]),
'brewing_pressure': None,
'brewing_pressure_supported': False,
'continuous_dose': None,
'continuous_dose_supported': False,
'doses': dict({
'pulses_type': list([
dict({
'dose': 126.0,
'dose_index': 'DoseA',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
dict({
'dose': 126.0,
'dose_index': 'DoseB',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
dict({
'dose': 160.0,
'dose_index': 'DoseC',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
dict({
'dose': 77.0,
'dose_index': 'DoseD',
'dose_max': 9999.0,
'dose_min': 0.0,
'dose_step': 1,
}),
]),
}),
'mirror_with_group_1': None,
'mirror_with_group_1_not_effective': False,
'mirror_with_group_1_supported': False,
'mode': 'PulsesType',
'profile': None,
}),
}),
dict({
'code': 'CMPreBrewing',
'index': 1,
'output': dict({
'available_modes': list([
'PreBrewing',
'PreInfusion',
'Disabled',
]),
'dose_index_supported': True,
'mode': 'PreInfusion',
'times': dict({
'pre_brewing': list([
dict({
'dose_index': 'DoseA',
'seconds': dict({
'In': 0.5,
'Out': 1.0,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseB',
'seconds': dict({
'In': 0.5,
'Out': 1.0,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseC',
'seconds': dict({
'In': 3.3,
'Out': 3.3,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseD',
'seconds': dict({
'In': 2.0,
'Out': 2.0,
}),
'seconds_max': dict({
'In': 10.0,
'Out': 10.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
]),
'pre_infusion': list([
dict({
'dose_index': 'DoseA',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseB',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseC',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
dict({
'dose_index': 'DoseD',
'seconds': dict({
'In': 0.0,
'Out': 4.0,
}),
'seconds_max': dict({
'In': 25.0,
'Out': 25.0,
}),
'seconds_min': dict({
'In': 0.0,
'Out': 0.0,
}),
'seconds_step': dict({
'In': 0.1,
'Out': 0.1,
}),
}),
]),
}),
}),
}),
dict({
'code': 'CMHotWaterDose',
'index': 1,
'output': dict({
'doses': list([
dict({
'dose': 8.0,
'dose_index': 'DoseA',
'dose_max': 90.0,
'dose_min': 0.0,
'dose_step': 1,
}),
]),
'enabled': True,
'enabled_supported': False,
}),
}),
dict({
'code': 'CMBackFlush',
'index': 1,
'output': dict({
'last_cleaning_start_time': None,
'status': 'Off',
}),
}),
]),
}),
'firmware': list([
dict({
'machine': dict({
'current_version': '1.40',
'latest_version': '1.55',
'schedule': dict({
'available_firmware_update': False,
'ble_auth_token': None,
'coffee_station': None,
'connected': True,
'connection_date': '2025-03-21T03:00:19.892000+00:00',
'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png',
'location': None,
'model_code': 'LINEAMICRA',
'model_name': 'Linea Micra',
'name': 'MR123456',
'offline_mode': False,
'require_firmware_update': False,
'serial_number': '**REDACTED**',
'smart_wake_up_sleep': dict({
'schedules': list([
dict({
'days': list([
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
]),
'enabled': True,
'id': 'Os2OswX',
'offTimeMinutes': 1440,
'onTimeMinutes': 1320,
'steamBoiler': True,
}),
dict({
'days': list([
'Sunday',
]),
'enabled': True,
'id': 'aXFz5bJ',
'offTimeMinutes': 450,
'onTimeMinutes': 420,
'steamBoiler': False,
}),
]),
'schedules_dict': dict({
'Os2OswX': dict({
'days': list([
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
]),
'enabled': True,
'id': 'Os2OswX',
'offTimeMinutes': 1440,
'onTimeMinutes': 1320,
'steamBoiler': True,
}),
'aXFz5bJ': dict({
'days': list([
'Sunday',
]),
'enabled': True,
'id': 'aXFz5bJ',
'offTimeMinutes': 450,
'onTimeMinutes': 420,
'steamBoiler': False,
}),
}),
'smart_stand_by_after': 'PowerOn',
'smart_stand_by_enabled': True,
'smart_stand_by_minutes': 10,
'smart_stand_by_minutes_max': 30,
'smart_stand_by_minutes_min': 1,
'smart_stand_by_minutes_step': 1,
}),
'smart_wake_up_sleep_supported': True,
'type': 'CoffeeMachine',
}),
'serial_number': '**REDACTED**',
'settings': dict({
'actual_firmwares': list([
dict({
'available_update': dict({
'build_version': 'v5.0.10',
'change_log': '''
Whats new in this version:
* fixed an issue that could cause the machine powers up outside scheduled time
* minor improvements
''',
'thing_model_code': 'LineaMicra',
'type': 'Gateway',
}),
'build_version': 'v5.0.9',
'change_log': '''
Whats new in this version:
* New La Marzocco compatibility
* Improved connectivity
* Improved pairing process
* Improved statistics
* Boilers heating time
* Last backflush date (GS3 MP excluded)
* Automatic gateway updates option
''',
'status': 'ToUpdate',
'thing_model_code': 'LineaMicra',
'type': 'Gateway',
}),
dict({
'available_update': None,
'build_version': 'v1.17',
'change_log': 'None',
'status': 'Updated',
'thing_model_code': 'LineaMicra',
'type': 'Machine',
}),
]),
'auto_update': False,
'auto_update_supported': True,
'available_firmware_update': False,
'ble_auth_token': None,
'coffee_station': None,
'connected': True,
'connection_date': '2025-03-21T03:00:19.892000+00:00',
'cropster_active': False,
'cropster_supported': False,
'factory_reset_supported': True,
'firmwares': dict({
'Gateway': dict({
'available_update': dict({
'build_version': 'v5.0.10',
'change_log': '''
Whats new in this version:
* fixed an issue that could cause the machine powers up outside scheduled time
* minor improvements
''',
'thing_model_code': 'LineaMicra',
'type': 'Gateway',
}),
'build_version': 'v5.0.9',
'change_log': '''
Whats new in this version:
* New La Marzocco compatibility
* Improved connectivity
* Improved pairing process
* Improved statistics
* Boilers heating time
* Last backflush date (GS3 MP excluded)
* Automatic gateway updates option
''',
'status': 'ToUpdate',
'thing_model_code': 'LineaMicra',
'type': 'Gateway',
}),
'Machine': dict({
'available_update': None,
'build_version': 'v1.17',
'change_log': 'None',
'status': 'Updated',
'thing_model_code': 'LineaMicra',
'type': 'Machine',
}),
}),
dict({
'gateway': dict({
'current_version': 'v3.1-rc4',
'latest_version': 'v3.5-rc3',
}),
}),
]),
'model': 'GS3 AV',
'statistics': dict({
'continous': 2252,
'drink_stats': dict({
'1': 1047,
'2': 560,
'3': 468,
'4': 312,
}),
'total_flushes': 1740,
'hemro_active': False,
'hemro_supported': False,
'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png',
'is_plumbed_in': True,
'location': None,
'model_code': 'LINEAMICRA',
'model_name': 'Linea Micra',
'name': 'MR123456',
'offline_mode': False,
'plumb_in_supported': True,
'require_firmware_update': False,
'serial_number': '**REDACTED**',
'type': 'CoffeeMachine',
'wifi_rssi': -51,
'wifi_ssid': 'MyWifi',
}),
})
# ---

View File

@ -29,47 +29,14 @@
'labels': set({
}),
'manufacturer': 'La Marzocco',
'model': <MachineModel.GS3_AV: 'GS3 AV'>,
'model_id': <MachineModel.GS3_AV: 'GS3 AV'>,
'model': 'GS3 AV',
'model_id': 'GS3AV',
'name': 'GS012345',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': 'GS012345',
'suggested_area': None,
'sw_version': '1.40',
'sw_version': 'v1.17',
'via_device_id': None,
})
# ---
# name: test_scale_device[Linea Mini]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'lamarzocco',
'44:b7:d0:74:5f:90',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Acaia',
'model': 'Lunar',
'model_id': 'Y.301',
'name': 'LMZ-123A45',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': <ANY>,
})
# ---

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +1,4 @@
# serializer version: 1
# name: test_active_bbw_recipe[Linea Mini]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'LMZ-123A45 Active brew by weight recipe',
'options': list([
'a',
'b',
]),
}),
'context': <ANY>,
'entity_id': 'select.lmz_123a45_active_brew_by_weight_recipe',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'a',
})
# ---
# name: test_active_bbw_recipe[Linea Mini].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'a',
'b',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.lmz_123a45_active_brew_by_weight_recipe',
'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': 'Active brew by weight recipe',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'active_bbw',
'unique_id': 'LM012345_active_bbw',
'unit_of_measurement': None,
})
# ---
# name: test_pre_brew_infusion_select[GS3 AV]
StateSnapshot({
'attributes': ReadOnlyDict({
@ -113,6 +57,64 @@
'unit_of_measurement': None,
})
# ---
# name: test_pre_brew_infusion_select[Linea Micra]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MR012345 Prebrew/-infusion mode',
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'context': <ANY>,
'entity_id': 'select.mr012345_prebrew_infusion_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'preinfusion',
})
# ---
# name: test_pre_brew_infusion_select[Linea Micra].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.mr012345_prebrew_infusion_mode',
'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': 'Prebrew/-infusion mode',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'prebrew_infusion_select',
'unique_id': 'MR012345_prebrew_infusion_select',
'unit_of_measurement': None,
})
# ---
# name: test_pre_brew_infusion_select[Linea Mini]
StateSnapshot({
'attributes': ReadOnlyDict({
@ -128,7 +130,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'preinfusion',
'state': 'disabled',
})
# ---
# name: test_pre_brew_infusion_select[Linea Mini].1
@ -171,64 +173,6 @@
'unit_of_measurement': None,
})
# ---
# name: test_pre_brew_infusion_select[Micra]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MR012345 Prebrew/-infusion mode',
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'context': <ANY>,
'entity_id': 'select.mr012345_prebrew_infusion_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'preinfusion',
})
# ---
# name: test_pre_brew_infusion_select[Micra].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'disabled',
'prebrew',
'preinfusion',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.mr012345_prebrew_infusion_mode',
'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': 'Prebrew/-infusion mode',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'prebrew_infusion_select',
'unique_id': 'MR012345_prebrew_infusion_select',
'unit_of_measurement': None,
})
# ---
# name: test_smart_standby_mode
StateSnapshot({
'attributes': ReadOnlyDict({
@ -243,7 +187,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'last_brewing',
'state': 'power_on',
})
# ---
# name: test_smart_standby_mode.1
@ -285,7 +229,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_steam_boiler_level[Micra]
# name: test_steam_boiler_level[Linea Micra]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MR012345 Steam level',
@ -300,10 +244,10 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
'state': '3',
})
# ---
# name: test_steam_boiler_level[Micra].1
# name: test_steam_boiler_level[Linea Micra].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),

View File

@ -1,521 +0,0 @@
# serializer version: 1
# name: test_scale_battery[Linea Mini]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'LMZ-123A45 Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.lmz_123a45_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '64',
})
# ---
# name: test_scale_battery[Linea Mini].1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.lmz_123a45_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'LM012345_scale_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_coffees_made_key_1',
'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': 'Coffees made Key 1',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'drink_stats_coffee_key',
'unique_id': 'GS012345_drink_stats_coffee_key_key1',
'unit_of_measurement': 'coffees',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Coffees made Key 1',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'coffees',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_coffees_made_key_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1047',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_coffees_made_key_2',
'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': 'Coffees made Key 2',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'drink_stats_coffee_key',
'unique_id': 'GS012345_drink_stats_coffee_key_key2',
'unit_of_measurement': 'coffees',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Coffees made Key 2',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'coffees',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_coffees_made_key_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '560',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_3-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_coffees_made_key_3',
'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': 'Coffees made Key 3',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'drink_stats_coffee_key',
'unique_id': 'GS012345_drink_stats_coffee_key_key3',
'unit_of_measurement': 'coffees',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_3-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Coffees made Key 3',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'coffees',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_coffees_made_key_3',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '468',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_4-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_coffees_made_key_4',
'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': 'Coffees made Key 4',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'drink_stats_coffee_key',
'unique_id': 'GS012345_drink_stats_coffee_key_key4',
'unit_of_measurement': 'coffees',
})
# ---
# name: test_sensors[sensor.gs012345_coffees_made_key_4-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Coffees made Key 4',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'coffees',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_coffees_made_key_4',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '312',
})
# ---
# name: test_sensors[sensor.gs012345_current_coffee_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.gs012345_current_coffee_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Current coffee temperature',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'current_temp_coffee',
'unique_id': 'GS012345_current_temp_coffee',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.gs012345_current_coffee_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'GS012345 Current coffee temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_current_coffee_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '96.5',
})
# ---
# name: test_sensors[sensor.gs012345_current_steam_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.gs012345_current_steam_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Current steam temperature',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'current_temp_steam',
'unique_id': 'GS012345_current_temp_steam',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.gs012345_current_steam_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'GS012345 Current steam temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_current_steam_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123.800003051758',
})
# ---
# name: test_sensors[sensor.gs012345_shot_timer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_shot_timer',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Shot timer',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'shot_timer',
'unique_id': 'GS012345_shot_timer',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_sensors[sensor.gs012345_shot_timer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'GS012345 Shot timer',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_shot_timer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[sensor.gs012345_total_coffees_made-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_total_coffees_made',
'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': 'Total coffees made',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'drink_stats_coffee',
'unique_id': 'GS012345_drink_stats_coffee',
'unit_of_measurement': 'coffees',
})
# ---
# name: test_sensors[sensor.gs012345_total_coffees_made-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Total coffees made',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'coffees',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_total_coffees_made',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2387',
})
# ---
# name: test_sensors[sensor.gs012345_total_flushes_made-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gs012345_total_flushes_made',
'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': 'Total flushes made',
'platform': 'lamarzocco',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'drink_stats_flushing',
'unique_id': 'GS012345_drink_stats_flushing',
'unit_of_measurement': 'flushes',
})
# ---
# name: test_sensors[sensor.gs012345_total_flushes_made-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'GS012345 Total flushes made',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'flushes',
}),
'context': <ANY>,
'entity_id': 'sensor.gs012345_total_flushes_made',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1740',
})
# ---

View File

@ -42,8 +42,8 @@
'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png',
'friendly_name': 'GS012345 Gateway firmware',
'in_progress': False,
'installed_version': 'v3.1-rc4',
'latest_version': 'v3.5-rc3',
'installed_version': 'v5.0.9',
'latest_version': 'v5.0.10',
'release_summary': None,
'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/',
'skipped_version': None,
@ -102,8 +102,8 @@
'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png',
'friendly_name': 'GS012345 Machine firmware',
'in_progress': False,
'installed_version': '1.40',
'latest_version': '1.55',
'installed_version': 'v1.17',
'latest_version': 'v1.17',
'release_summary': None,
'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/',
'skipped_version': None,
@ -116,6 +116,6 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
'state': 'off',
})
# ---

View File

@ -4,10 +4,7 @@ from datetime import timedelta
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from pylamarzocco.const import MachineModel
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoScale
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import STATE_UNAVAILABLE, Platform
@ -35,26 +32,14 @@ async def test_binary_sensors(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_brew_active_does_not_exists(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry_no_local_connection: MockConfigEntry,
) -> None:
"""Test the La Marzocco currently_making_coffee doesn't exist if host not set."""
await async_init_integration(hass, mock_config_entry_no_local_connection)
state = hass.states.get(f"sensor.{mock_lamarzocco.serial_number}_brewing_active")
assert state is None
async def test_brew_active_unavailable(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the La Marzocco currently_making_coffee becomes unavailable."""
"""Test the La Marzocco brew active becomes unavailable."""
mock_lamarzocco.websocket_connected = False
mock_lamarzocco.websocket.connected = False
await async_init_integration(hass, mock_config_entry)
state = hass.states.get(
f"binary_sensor.{mock_lamarzocco.serial_number}_brewing_active"
@ -79,7 +64,7 @@ async def test_sensor_going_unavailable(
assert state
assert state.state != STATE_UNAVAILABLE
mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("")
mock_lamarzocco.get_dashboard.side_effect = RequestNotSuccessful("")
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
@ -87,68 +72,3 @@ async def test_sensor_going_unavailable(
state = hass.states.get(brewing_active_sensor)
assert state
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_scale_connectivity(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the scale binary sensors."""
await async_init_integration(hass, mock_config_entry)
state = hass.states.get("binary_sensor.lmz_123a45_connectivity")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry.device_id
assert entry == snapshot
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA],
)
async def test_other_models_no_scale_connectivity(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Ensure the other models don't have a connectivity sensor."""
await async_init_integration(hass, mock_config_entry)
state = hass.states.get("binary_sensor.lmz_123a45_connectivity")
assert state is None
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_connectivity_on_new_scale_added(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Ensure the connectivity binary sensor for a new scale is added automatically."""
mock_lamarzocco.config.scale = None
await async_init_integration(hass, mock_config_entry)
state = hass.states.get("binary_sensor.scale_123a45_connectivity")
assert state is None
mock_lamarzocco.config.scale = LaMarzoccoScale(
connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50
)
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.scale_123a45_connectivity")
assert state

View File

@ -127,7 +127,12 @@ async def test_no_calendar_events_global_disable(
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
wake_up_sleep_entry = mock_lamarzocco.schedule.smart_wake_up_sleep.schedules_dict[
wake_up_sleep_entry_id
]
assert wake_up_sleep_entry
wake_up_sleep_entry.enabled = False
test_time = datetime(2024, 1, 12, 11, tzinfo=dt_util.get_default_time_zone())
freezer.move_to(test_time)

View File

@ -1,11 +1,11 @@
"""Test the La Marzocco config flow."""
from collections.abc import Generator
from copy import deepcopy
from unittest.mock import AsyncMock, MagicMock, patch
from pylamarzocco.const import MachineModel
from pylamarzocco.const import ModelName
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoDeviceInfo
import pytest
from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE
@ -15,18 +15,11 @@ from homeassistant.config_entries import (
SOURCE_DHCP,
SOURCE_USER,
ConfigEntryState,
ConfigFlowResult,
)
from homeassistant.const import (
CONF_ADDRESS,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PASSWORD,
CONF_TOKEN,
)
from homeassistant.const import CONF_ADDRESS, CONF_MAC, CONF_PASSWORD, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult, FlowResultType
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from . import USER_INPUT, async_init_integration, get_bluetooth_service_info
@ -35,8 +28,8 @@ from tests.common import MockConfigEntry
async def __do_successful_user_step(
hass: HomeAssistant, result: FlowResult, mock_cloud_client: MagicMock
) -> FlowResult:
hass: HomeAssistant, result: ConfigFlowResult, mock_cloud_client: MagicMock
) -> ConfigFlowResult:
"""Successfully configure the user step."""
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -50,39 +43,28 @@ async def __do_successful_user_step(
async def __do_sucessful_machine_selection_step(
hass: HomeAssistant, result2: FlowResult, mock_device_info: LaMarzoccoDeviceInfo
hass: HomeAssistant, result2: ConfigFlowResult
) -> None:
"""Successfully configure the machine selection step."""
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()
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{CONF_MACHINE: "GS012345"},
)
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "GS3"
assert result3["title"] == "GS012345"
assert result3["data"] == {
**USER_INPUT,
CONF_HOST: "192.168.1.1",
CONF_MODEL: mock_device_info.model,
CONF_NAME: mock_device_info.name,
CONF_TOKEN: mock_device_info.communication_key,
CONF_TOKEN: None,
}
assert result3["result"].unique_id == "GS012345"
async def test_form(
hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
mock_setup_entry: Generator[AsyncMock],
) -> None:
"""Test we get the form."""
@ -94,13 +76,12 @@ async def test_form(
assert result["step_id"] == "user"
result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
await __do_sucessful_machine_selection_step(hass, result2)
async def test_form_abort_already_configured(
hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test we abort if already configured."""
@ -124,8 +105,7 @@ async def test_form_abort_already_configured(
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_HOST: "192.168.1.1",
CONF_MACHINE: mock_device_info.serial_number,
CONF_MACHINE: "GS012345",
},
)
await hass.async_block_till_done()
@ -134,15 +114,23 @@ async def test_form_abort_already_configured(
assert result3["reason"] == "already_configured"
@pytest.mark.parametrize(
("side_effect", "error"),
[
(AuthFail(""), "invalid_auth"),
(RequestNotSuccessful(""), "cannot_connect"),
],
)
async def test_form_invalid_auth(
hass: HomeAssistant,
mock_device_info: LaMarzoccoDeviceInfo,
mock_cloud_client: MagicMock,
mock_setup_entry: Generator[AsyncMock],
side_effect: Exception,
error: str,
) -> None:
"""Test invalid auth error."""
mock_cloud_client.get_customer_fleet.side_effect = AuthFail("")
mock_cloud_client.list_things.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@ -153,67 +141,24 @@ async def test_form_invalid_auth(
)
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_auth"}
assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1
assert result2["errors"] == {"base": error}
assert len(mock_cloud_client.list_things.mock_calls) == 1
# test recovery from failure
mock_cloud_client.get_customer_fleet.side_effect = None
mock_cloud_client.list_things.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)
await __do_sucessful_machine_selection_step(hass, result2)
async def test_form_invalid_host(
async def test_form_no_machines(
hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
mock_setup_entry: Generator[AsyncMock],
) -> None:
"""Test invalid auth error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
"""Test we don't have any devices."""
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "machine_selection"
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_cloud_client.get_customer_fleet.mock_calls) == 1
# test recovery from failure
await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
async def test_form_cannot_connect(
hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
mock_setup_entry: Generator[AsyncMock],
) -> None:
"""Test cannot connect error."""
mock_cloud_client.get_customer_fleet.return_value = {}
original_return = mock_cloud_client.list_things.return_value
mock_cloud_client.list_things.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@ -226,25 +171,13 @@ async def test_form_cannot_connect(
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "no_machines"}
assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1
mock_cloud_client.get_customer_fleet.side_effect = RequestNotSuccessful("")
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}
assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 2
assert len(mock_cloud_client.list_things.mock_calls) == 1
# test recovery from failure
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
}
mock_cloud_client.list_things.return_value = original_return
result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
await __do_sucessful_machine_selection_step(hass, result2)
async def test_reauth_flow(
@ -269,7 +202,7 @@ 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_cloud_client.get_customer_fleet.mock_calls) == 1
assert len(mock_cloud_client.list_things.mock_calls) == 1
assert mock_config_entry.data[CONF_PASSWORD] == "new_password"
@ -277,7 +210,6 @@ async def test_reconfigure_flow(
hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_config_entry: MockConfigEntry,
mock_device_info: LaMarzoccoDeviceInfo,
mock_setup_entry: Generator[AsyncMock],
) -> None:
"""Testing reconfgure flow."""
@ -289,15 +221,9 @@ async def test_reconfigure_flow(
assert result["step_id"] == "reconfigure"
result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
service_info = get_bluetooth_service_info(
mock_device_info.model, mock_device_info.serial_number
)
service_info = get_bluetooth_service_info(ModelName.GS3_MP, "GS012345")
with (
patch(
"homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection",
return_value=True,
),
patch(
"homeassistant.components.lamarzocco.config_flow.async_discovered_service_info",
return_value=[service_info],
@ -306,8 +232,7 @@ async def test_reconfigure_flow(
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_HOST: "192.168.1.1",
CONF_MACHINE: mock_device_info.serial_number,
CONF_MACHINE: "GS012345",
},
)
await hass.async_block_till_done()
@ -338,8 +263,10 @@ async def test_bluetooth_discovery(
) -> None:
"""Test bluetooth discovery."""
service_info = get_bluetooth_service_info(
mock_lamarzocco.model, mock_lamarzocco.serial_number
ModelName.GS3_MP, mock_lamarzocco.serial_number
)
mock_cloud_client.list_things.return_value[0].ble_auth_token = "dummyToken"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
)
@ -351,33 +278,13 @@ async def test_bluetooth_discovery(
result["flow_id"],
USER_INPUT,
)
assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "machine_selection"
assert result2["type"] is FlowResultType.CREATE_ENTRY
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"] == "GS3"
assert result3["data"] == {
assert result2["title"] == "GS012345"
assert result2["data"] == {
**USER_INPUT,
CONF_HOST: "192.168.1.1",
CONF_MACHINE: mock_lamarzocco.serial_number,
CONF_NAME: "GS3",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_MODEL: mock_lamarzocco.model,
CONF_TOKEN: "token",
CONF_TOKEN: "dummyToken",
}
@ -392,7 +299,7 @@ async def test_bluetooth_discovery_already_configured(
mock_config_entry.add_to_hass(hass)
service_info = get_bluetooth_service_info(
mock_lamarzocco.model, mock_lamarzocco.serial_number
ModelName.GS3_MP, mock_lamarzocco.serial_number
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
@ -405,12 +312,11 @@ async def test_bluetooth_discovery_errors(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
mock_setup_entry: Generator[AsyncMock],
) -> None:
"""Test bluetooth discovery errors."""
service_info = get_bluetooth_service_info(
mock_lamarzocco.model, mock_lamarzocco.serial_number
ModelName.GS3_MP, mock_lamarzocco.serial_number
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
@ -421,61 +327,37 @@ async def test_bluetooth_discovery_errors(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_cloud_client.get_customer_fleet.return_value = {"GS98765", ""}
original_return = deepcopy(mock_cloud_client.list_things.return_value)
mock_cloud_client.list_things.return_value[0].serial_number = "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_cloud_client.get_customer_fleet.mock_calls) == 1
assert len(mock_cloud_client.list_things.mock_calls) == 1
mock_cloud_client.get_customer_fleet.return_value = {
mock_device_info.serial_number: mock_device_info
}
mock_cloud_client.list_things.return_value = original_return
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_cloud_client.get_customer_fleet.mock_calls) == 2
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 result2["type"] is FlowResultType.CREATE_ENTRY
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "GS3"
assert result3["data"] == {
assert result2["title"] == "GS012345"
assert result2["data"] == {
**USER_INPUT,
CONF_HOST: "192.168.1.1",
CONF_MACHINE: mock_lamarzocco.serial_number,
CONF_NAME: "GS3",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_MODEL: mock_lamarzocco.model,
CONF_TOKEN: "token",
CONF_TOKEN: None,
}
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI, MachineModel.GS3_AV],
)
async def test_dhcp_discovery(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_cloud_client: MagicMock,
mock_device_info: LaMarzoccoDeviceInfo,
mock_setup_entry: Generator[AsyncMock],
) -> None:
"""Test dhcp discovery."""
@ -493,24 +375,16 @@ async def test_dhcp_discovery(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection",
return_value=True,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["data"] == {
**USER_INPUT,
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
CONF_HOST: "192.168.1.42",
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,
}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["data"] == {
**USER_INPUT,
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
CONF_TOKEN: None,
}
async def test_dhcp_discovery_abort_on_hostname_changed(
@ -541,7 +415,6 @@ async def test_dhcp_already_configured_and_update(
mock_config_entry: MockConfigEntry,
) -> None:
"""Test discovered IP address change."""
old_ip = mock_config_entry.data[CONF_HOST]
old_address = mock_config_entry.data[CONF_ADDRESS]
mock_config_entry.add_to_hass(hass)
@ -557,9 +430,6 @@ async def test_dhcp_already_configured_and_update(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert mock_config_entry.data[CONF_HOST] != old_ip
assert mock_config_entry.data[CONF_HOST] == "192.168.1.42"
assert mock_config_entry.data[CONF_ADDRESS] != old_address
assert mock_config_entry.data[CONF_ADDRESS] == "aa:bb:cc:dd:ee:ff"

View File

@ -1,11 +1,10 @@
"""Test initialization of lamarzocco."""
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from pylamarzocco.const import FirmwareType, MachineModel
from pylamarzocco.const import FirmwareType, ModelName
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.models import WebSocketDetails
import pytest
from syrupy import SnapshotAssertion
@ -13,6 +12,7 @@ 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_ADDRESS,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
@ -29,7 +29,7 @@ from homeassistant.helpers import (
from . import USER_INPUT, async_init_integration, get_bluetooth_service_info
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.common import MockConfigEntry
async def test_load_unload_config_entry(
@ -54,25 +54,48 @@ async def test_config_entry_not_ready(
mock_lamarzocco: MagicMock,
) -> None:
"""Test the La Marzocco configuration entry not ready."""
mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("")
mock_lamarzocco.get_dashboard.side_effect = RequestNotSuccessful("")
await async_init_integration(hass, mock_config_entry)
assert len(mock_lamarzocco.get_config.mock_calls) == 1
assert len(mock_lamarzocco.get_dashboard.mock_calls) == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize(
("side_effect", "expected_state"),
[
(AuthFail(""), ConfigEntryState.SETUP_ERROR),
(RequestNotSuccessful(""), ConfigEntryState.SETUP_RETRY),
],
)
async def test_get_settings_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_cloud_client: MagicMock,
side_effect: Exception,
expected_state: ConfigEntryState,
) -> None:
"""Test error during initial settings get."""
mock_cloud_client.get_thing_settings.side_effect = side_effect
await async_init_integration(hass, mock_config_entry)
assert len(mock_cloud_client.get_thing_settings.mock_calls) == 1
assert mock_config_entry.state is expected_state
async def test_invalid_auth(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
) -> None:
"""Test auth error during setup."""
mock_lamarzocco.get_config.side_effect = AuthFail("")
mock_lamarzocco.get_dashboard.side_effect = AuthFail("")
await async_init_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
assert len(mock_lamarzocco.get_config.mock_calls) == 1
assert len(mock_lamarzocco.get_dashboard.mock_calls) == 1
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
@ -86,37 +109,54 @@ async def test_invalid_auth(
assert flow["context"].get("entry_id") == mock_config_entry.entry_id
async def test_v1_migration(
async def test_v1_migration_fails(
hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_lamarzocco: MagicMock,
) -> None:
"""Test v1 -> v2 Migration."""
common_data = {
**USER_INPUT,
CONF_HOST: "host",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
}
entry_v1 = MockConfigEntry(
domain=DOMAIN,
version=1,
unique_id=mock_lamarzocco.serial_number,
data={
**common_data,
CONF_MACHINE: mock_lamarzocco.serial_number,
},
data={},
)
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) == {
**common_data,
CONF_NAME: "GS3",
CONF_MODEL: mock_lamarzocco.model,
CONF_TOKEN: "token",
assert entry_v1.state is ConfigEntryState.MIGRATION_ERROR
async def test_v2_migration(
hass: HomeAssistant,
mock_cloud_client: MagicMock,
mock_lamarzocco: MagicMock,
) -> None:
"""Test v2 -> v3 Migration."""
entry_v2 = MockConfigEntry(
domain=DOMAIN,
version=2,
unique_id=mock_lamarzocco.serial_number,
data={
**USER_INPUT,
CONF_HOST: "192.168.1.24",
CONF_NAME: "La Marzocco",
CONF_MODEL: ModelName.GS3_MP.value,
CONF_MAC: "aa:bb:cc:dd:ee:ff",
},
)
entry_v2.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry_v2.entry_id)
assert entry_v2.state is ConfigEntryState.LOADED
assert entry_v2.version == 3
assert dict(entry_v2.data) == {
**USER_INPUT,
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_TOKEN: None,
}
@ -128,28 +168,28 @@ async def test_migration_errors(
) -> None:
"""Test errors during migration."""
mock_cloud_client.get_customer_fleet.side_effect = RequestNotSuccessful("Error")
mock_cloud_client.list_things.side_effect = RequestNotSuccessful("Error")
entry_v1 = MockConfigEntry(
entry_v2 = MockConfigEntry(
domain=DOMAIN,
version=1,
version=2,
unique_id=mock_lamarzocco.serial_number,
data={
**USER_INPUT,
CONF_MACHINE: mock_lamarzocco.serial_number,
},
)
entry_v1.add_to_hass(hass)
entry_v2.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry_v1.entry_id)
assert entry_v1.state is ConfigEntryState.MIGRATION_ERROR
assert not await hass.config_entries.async_setup(entry_v2.entry_id)
assert entry_v2.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 = MockConfigEntry(domain=DOMAIN, version=4)
entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry.entry_id)
@ -159,12 +199,14 @@ async def test_bluetooth_is_set_from_discovery(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
mock_cloud_client: MagicMock,
) -> None:
"""Check we can fill a device from discovery info."""
service_info = get_bluetooth_service_info(
mock_lamarzocco.model, mock_lamarzocco.serial_number
ModelName.GS3_MP, mock_lamarzocco.serial_number
)
mock_cloud_client.get_thing_settings.return_value.ble_auth_token = "token"
with (
patch(
"homeassistant.components.lamarzocco.async_discovered_service_info",
@ -174,17 +216,15 @@ async def test_bluetooth_is_set_from_discovery(
"homeassistant.components.lamarzocco.LaMarzoccoMachine"
) as mock_machine_class,
):
mock_machine = MagicMock()
mock_machine.get_firmware = AsyncMock()
mock_machine.firmware = mock_lamarzocco.firmware
mock_machine_class.return_value = mock_machine
mock_machine_class.return_value = mock_lamarzocco
await async_init_integration(hass, mock_config_entry)
discovery.assert_called_once()
assert mock_machine_class.call_count == 2
assert mock_machine_class.call_count == 1
_, kwargs = mock_machine_class.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
assert mock_config_entry.data[CONF_TOKEN] == "token"
async def test_websocket_closed_on_unload(
@ -193,34 +233,38 @@ async def test_websocket_closed_on_unload(
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()
mock_disconnect_callback = AsyncMock()
mock_websocket = MagicMock()
mock_websocket.closed = True
await async_init_integration(hass, mock_config_entry)
mock_lamarzocco.websocket_connect.assert_called_once()
mock_lamarzocco.websocket = WebSocketDetails(
mock_websocket, mock_disconnect_callback
)
client.websocket.closed = False
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
client.websocket.close.assert_called_once()
await async_init_integration(hass, mock_config_entry)
mock_lamarzocco.connect_dashboard_websocket.assert_called_once()
mock_websocket.closed = False
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
mock_disconnect_callback.assert_called_once()
@pytest.mark.parametrize(
("version", "issue_exists"), [("v3.5-rc6", False), ("v3.3-rc4", True)]
("version", "issue_exists"), [("v3.5-rc6", True), ("v5.0.9", False)]
)
async def test_gateway_version_issue(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
mock_cloud_client: 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
mock_cloud_client.get_thing_settings.return_value.firmwares[
FirmwareType.GATEWAY
].build_version = version
await async_init_integration(hass, mock_config_entry)
@ -229,34 +273,33 @@ async def test_gateway_version_issue(
assert (issue is not None) == issue_exists
async def test_conf_host_removed_for_new_gateway(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
) -> None:
"""Make sure we get the issue for certain gateway firmware versions."""
mock_lamarzocco.firmware[FirmwareType.GATEWAY].current_version = "v5.0.9"
await async_init_integration(hass, mock_config_entry)
assert CONF_HOST not in mock_config_entry.data
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."""
mock_config_entry = MockConfigEntry(
title="My LaMarzocco",
domain=DOMAIN,
version=3,
data=USER_INPUT
| {
CONF_ADDRESS: "00:00:00:00:00:00",
CONF_TOKEN: "token",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
},
unique_id=mock_lamarzocco.serial_number,
)
await async_init_integration(hass, mock_config_entry)
hass.config_entries.async_update_entry(
mock_config_entry,
data={**mock_config_entry.data, CONF_MAC: "aa:bb:cc:dd:ee:ff"},
data={
**mock_config_entry.data,
},
)
state = hass.states.get(f"switch.{mock_lamarzocco.serial_number}")
@ -269,49 +312,3 @@ async def test_device(
device = device_registry.async_get(entry.device_id)
assert device
assert device == snapshot
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_scale_device(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the device."""
await async_init_integration(hass, mock_config_entry)
device = device_registry.async_get_device(
identifiers={(DOMAIN, mock_lamarzocco.config.scale.address)}
)
assert device
assert device == snapshot
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_remove_stale_scale(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Ensure stale scale is cleaned up."""
await async_init_integration(hass, mock_config_entry)
scale_address = mock_lamarzocco.config.scale.address
device = device_registry.async_get_device(identifiers={(DOMAIN, scale_address)})
assert device
mock_lamarzocco.config.scale = None
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
device = device_registry.async_get_device(identifiers={(DOMAIN, scale_address)})
assert device is None

View File

@ -1,19 +1,10 @@
"""Tests for the La Marzocco number entities."""
from datetime import timedelta
from typing import Any
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from pylamarzocco.const import (
KEYS_PER_MODEL,
BoilerType,
MachineModel,
PhysicalKey,
PrebrewMode,
)
from pylamarzocco.const import SmartStandByType
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoScale
import pytest
from syrupy import SnapshotAssertion
@ -22,14 +13,14 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import async_init_integration
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
@ -38,14 +29,14 @@ from tests.common import MockConfigEntry, async_fire_time_changed
(
"coffee_target_temperature",
94,
"set_temp",
{"boiler": BoilerType.COFFEE, "temperature": 94},
"set_coffee_target_temperature",
{"temperature": 94},
),
(
"smart_standby_time",
23,
"set_smart_standby",
{"enabled": True, "mode": "LastBrewing", "minutes": 23},
{"enabled": True, "mode": SmartStandByType.POWER_ON, "minutes": 23},
),
],
)
@ -94,318 +85,6 @@ async def test_general_numbers(
mock_func.assert_called_once_with(**kwargs)
@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_temp",
{"boiler": BoilerType.STEAM, "temperature": 131},
),
("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,
entity_name: str,
value: float,
func_name: str,
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)
state = hass.states.get(f"number.{serial_number}_{entity_name}")
assert state
assert state == snapshot
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,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: f"number.{serial_number}_{entity_name}",
ATTR_VALUE: value,
},
blocking=True,
)
assert len(func.mock_calls) == 1
func.assert_called_once_with(**kwargs)
@pytest.mark.parametrize(
"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
for entity in ENTITIES:
state = hass.states.get(f"number.{serial_number}_{entity}")
assert state is None
@pytest.mark.parametrize(
"device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI]
)
@pytest.mark.parametrize(
("entity_name", "function_name", "prebrew_mode", "value", "kwargs"),
[
(
"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,
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.config.prebrew_mode = prebrew_mode
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"number.{serial_number}_{entity_name}")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
# service call
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: f"number.{serial_number}_{entity_name}",
ATTR_VALUE: value,
},
blocking=True,
)
function = getattr(mock_lamarzocco, function_name)
function.assert_called_once_with(**kwargs)
@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", "prebrew_mode", "function_name", "kwargs"),
[
(
"prebrew_off_time",
6,
PrebrewMode.PREBREW,
"set_prebrew_time",
{"prebrew_off_time": 6.0},
),
(
"prebrew_on_time",
6,
PrebrewMode.PREBREW,
"set_prebrew_time",
{"prebrew_on_time": 6.0},
),
(
"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.config.prebrew_mode = prebrew_mode
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
func = getattr(mock_lamarzocco, function_name)
state = hass.states.get(f"number.{serial_number}_{entity_name}")
assert state is None
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")
# service call
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: f"number.{serial_number}_{entity_name}_key_{key}",
ATTR_VALUE: value,
},
blocking=True,
)
kwargs["key"] = key
assert len(func.mock_calls) == key.value
func.assert_called_with(**kwargs)
@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",
"preinfusion_time",
"set_dose",
)
serial_number = mock_lamarzocco.serial_number
for entity_name in ENTITIES:
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",
[MachineModel.GS3_MP, MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI],
)
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 (
"prebrew_off_time",
"prebrew_on_time",
"preinfusion_time",
"set_dose",
):
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.usefixtures("entity_registry_enabled_by_default")
async def test_number_error(
hass: HomeAssistant,
@ -419,7 +98,9 @@ async def test_number_error(
state = hass.states.get(f"number.{serial_number}_coffee_target_temperature")
assert state
mock_lamarzocco.set_temp.side_effect = RequestNotSuccessful("Boom")
mock_lamarzocco.set_coffee_target_temperature.side_effect = RequestNotSuccessful(
"Boom"
)
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
NUMBER_DOMAIN,
@ -431,107 +112,3 @@ async def test_number_error(
blocking=True,
)
assert exc_info.value.translation_key == "number_exception"
state = hass.states.get(f"number.{serial_number}_dose_key_1")
assert state
mock_lamarzocco.set_dose.side_effect = RequestNotSuccessful("Boom")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: f"number.{serial_number}_dose_key_1",
ATTR_VALUE: 99,
},
blocking=True,
)
assert exc_info.value.translation_key == "number_exception_key"
@pytest.mark.parametrize("physical_key", [PhysicalKey.A, PhysicalKey.B])
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_set_target(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
physical_key: PhysicalKey,
) -> None:
"""Test the La Marzocco set target sensors."""
await async_init_integration(hass, mock_config_entry)
entity_name = f"number.lmz_123a45_brew_by_weight_target_{int(physical_key)}"
state = hass.states.get(entity_name)
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
# service call
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: entity_name,
ATTR_VALUE: 42,
},
blocking=True,
)
mock_lamarzocco.set_bbw_recipe_target.assert_called_once_with(physical_key, 42)
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA],
)
async def test_other_models_no_scale_set_target(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Ensure the other models don't have a set target numbers."""
await async_init_integration(hass, mock_config_entry)
for i in range(1, 3):
state = hass.states.get(f"number.lmz_123a45_brew_by_weight_target_{i}")
assert state is None
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_set_target_on_new_scale_added(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Ensure the set target numbers for a new scale are added automatically."""
mock_lamarzocco.config.scale = None
await async_init_integration(hass, mock_config_entry)
for i in range(1, 3):
state = hass.states.get(f"number.scale_123a45_brew_by_weight_target_{i}")
assert state is None
mock_lamarzocco.config.scale = LaMarzoccoScale(
connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50
)
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
for i in range(1, 3):
state = hass.states.get(f"number.scale_123a45_brew_by_weight_target_{i}")
assert state

View File

@ -1,18 +1,14 @@
"""Tests for the La Marzocco select entities."""
from datetime import timedelta
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from pylamarzocco.const import (
MachineModel,
PhysicalKey,
PrebrewMode,
SmartStandbyMode,
SteamLevel,
ModelName,
PreExtractionMode,
SmartStandByType,
SteamTargetLevel,
)
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoScale
import pytest
from syrupy import SnapshotAssertion
@ -26,15 +22,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import async_init_integration
from tests.common import MockConfigEntry, async_fire_time_changed
pytest.mark.usefixtures("init_integration")
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MICRA])
@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MICRA])
async def test_steam_boiler_level(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@ -65,12 +57,14 @@ async def test_steam_boiler_level(
blocking=True,
)
mock_lamarzocco.set_steam_level.assert_called_once_with(level=SteamLevel.LEVEL_2)
mock_lamarzocco.set_steam_level.assert_called_once_with(
level=SteamTargetLevel.LEVEL_2
)
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MINI],
[ModelName.GS3_AV, ModelName.GS3_MP, ModelName.LINEA_MINI],
)
async def test_steam_boiler_level_none(
hass: HomeAssistant,
@ -86,7 +80,7 @@ async def test_steam_boiler_level_none(
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.LINEA_MICRA, MachineModel.GS3_AV, MachineModel.LINEA_MINI],
[ModelName.LINEA_MICRA, ModelName.GS3_AV, ModelName.LINEA_MINI],
)
async def test_pre_brew_infusion_select(
hass: HomeAssistant,
@ -118,19 +112,21 @@ async def test_pre_brew_infusion_select(
blocking=True,
)
mock_lamarzocco.set_prebrew_mode.assert_called_once_with(mode=PrebrewMode.PREBREW)
mock_lamarzocco.set_pre_extraction_mode.assert_called_once_with(
mode=PreExtractionMode.PREBREWING
)
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.GS3_MP],
[ModelName.GS3_MP],
)
async def test_pre_brew_infusion_select_none(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
) -> None:
"""Ensure the La Marzocco Steam Level Select is not created for non-Micra models."""
"""Ensure GS3 MP has no prebrew models."""
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"select.{serial_number}_prebrew_infusion_mode")
@ -162,13 +158,13 @@ async def test_smart_standby_mode(
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: f"select.{serial_number}_smart_standby_mode",
ATTR_OPTION: "power_on",
ATTR_OPTION: "last_brewing",
},
blocking=True,
)
mock_lamarzocco.set_smart_standby.assert_called_once_with(
enabled=True, mode=SmartStandbyMode.POWER_ON, minutes=10
enabled=True, mode=SmartStandByType.LAST_BREW, minutes=10
)
@ -183,7 +179,7 @@ async def test_select_errors(
state = hass.states.get(f"select.{serial_number}_prebrew_infusion_mode")
assert state
mock_lamarzocco.set_prebrew_mode.side_effect = RequestNotSuccessful("Boom")
mock_lamarzocco.set_pre_extraction_mode.side_effect = RequestNotSuccessful("Boom")
# Test setting invalid option
with pytest.raises(HomeAssistantError) as exc_info:
@ -197,77 +193,3 @@ async def test_select_errors(
blocking=True,
)
assert exc_info.value.translation_key == "select_option_error"
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_active_bbw_recipe(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_lamarzocco: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the La Marzocco active bbw recipe select."""
state = hass.states.get("select.lmz_123a45_active_brew_by_weight_recipe")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.lmz_123a45_active_brew_by_weight_recipe",
ATTR_OPTION: "b",
},
blocking=True,
)
mock_lamarzocco.set_active_bbw_recipe.assert_called_once_with(PhysicalKey.B)
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA],
)
async def test_other_models_no_active_bbw_select(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
) -> None:
"""Ensure the other models don't have a battery sensor."""
state = hass.states.get("select.lmz_123a45_active_brew_by_weight_recipe")
assert state is None
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_active_bbw_select_on_new_scale_added(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Ensure the active bbw select for a new scale is added automatically."""
mock_lamarzocco.config.scale = None
await async_init_integration(hass, mock_config_entry)
state = hass.states.get("select.scale_123a45_active_brew_by_weight_recipe")
assert state is None
mock_lamarzocco.config.scale = LaMarzoccoScale(
connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50
)
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("select.scale_123a45_active_brew_by_weight_recipe")
assert state

View File

@ -1,138 +0,0 @@
"""Tests for La Marzocco sensors."""
from datetime import timedelta
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from pylamarzocco.const import MachineModel
from pylamarzocco.models import LaMarzoccoScale
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import async_init_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensors(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the La Marzocco sensors."""
with patch("homeassistant.components.lamarzocco.PLATFORMS", [Platform.SENSOR]):
await async_init_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_shot_timer_not_exists(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry_no_local_connection: MockConfigEntry,
) -> None:
"""Test the La Marzocco shot timer doesn't exist if host not set."""
await async_init_integration(hass, mock_config_entry_no_local_connection)
state = hass.states.get(f"sensor.{mock_lamarzocco.serial_number}_shot_timer")
assert state is None
async def test_shot_timer_unavailable(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the La Marzocco brew_active becomes unavailable."""
mock_lamarzocco.websocket_connected = False
await async_init_integration(hass, mock_config_entry)
state = hass.states.get(f"sensor.{mock_lamarzocco.serial_number}_shot_timer")
assert state
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_no_steam_linea_mini(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure Linea Mini has no steam temp."""
await async_init_integration(hass, mock_config_entry)
serial_number = mock_lamarzocco.serial_number
state = hass.states.get(f"sensor.{serial_number}_current_temp_steam")
assert state is None
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_scale_battery(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the scale battery sensor."""
await async_init_integration(hass, mock_config_entry)
state = hass.states.get("sensor.lmz_123a45_battery")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry.device_id
assert entry == snapshot
@pytest.mark.parametrize(
"device_fixture",
[MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA],
)
async def test_other_models_no_scale_battery(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Ensure the other models don't have a battery sensor."""
await async_init_integration(hass, mock_config_entry)
state = hass.states.get("sensor.lmz_123a45_battery")
assert state is None
@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI])
async def test_battery_on_new_scale_added(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Ensure the battery sensor for a new scale is added automatically."""
mock_lamarzocco.config.scale = None
await async_init_integration(hass, mock_config_entry)
state = hass.states.get("sensor.lmz_123a45_battery")
assert state is None
mock_lamarzocco.config.scale = LaMarzoccoScale(
connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50
)
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.scale_123a45_battery")
assert state

View File

@ -3,6 +3,7 @@
from typing import Any
from unittest.mock import MagicMock, patch
from pylamarzocco.const import SmartStandByType
from pylamarzocco.exceptions import RequestNotSuccessful
import pytest
from syrupy import SnapshotAssertion
@ -47,7 +48,7 @@ async def test_switches(
(
"_smart_standby_enabled",
"set_smart_standby",
{"mode": "LastBrewing", "minutes": 10},
{"mode": SmartStandByType.POWER_ON, "minutes": 10},
),
],
)
@ -124,12 +125,15 @@ async def test_auto_on_off_switches(
blocking=True,
)
wake_up_sleep_entry = mock_lamarzocco.config.wake_up_sleep_entries[
wake_up_sleep_entry_id
]
wake_up_sleep_entry = (
mock_lamarzocco.schedule.smart_wake_up_sleep.schedules_dict[
wake_up_sleep_entry_id
]
)
assert wake_up_sleep_entry
wake_up_sleep_entry.enabled = False
mock_lamarzocco.set_wake_up_sleep.assert_called_with(wake_up_sleep_entry)
mock_lamarzocco.set_wakeup_schedule.assert_called_with(wake_up_sleep_entry)
await hass.services.async_call(
SWITCH_DOMAIN,
@ -140,7 +144,7 @@ async def test_auto_on_off_switches(
blocking=True,
)
wake_up_sleep_entry.enabled = True
mock_lamarzocco.set_wake_up_sleep.assert_called_with(wake_up_sleep_entry)
mock_lamarzocco.set_wakeup_schedule.assert_called_with(wake_up_sleep_entry)
async def test_switch_exceptions(
@ -183,7 +187,7 @@ async def test_switch_exceptions(
state = hass.states.get(f"switch.{serial_number}_auto_on_off_os2oswx")
assert state
mock_lamarzocco.set_wake_up_sleep.side_effect = RequestNotSuccessful("Boom")
mock_lamarzocco.set_wakeup_schedule.side_effect = RequestNotSuccessful("Boom")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
SWITCH_DOMAIN,

View File

@ -2,7 +2,6 @@
from unittest.mock import MagicMock, patch
from pylamarzocco.const import FirmwareType
from pylamarzocco.exceptions import RequestNotSuccessful
import pytest
from syrupy import SnapshotAssertion
@ -31,19 +30,10 @@ async def test_update(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("entity_name", "component"),
[
("machine_firmware", FirmwareType.MACHINE),
("gateway_firmware", FirmwareType.GATEWAY),
],
)
async def test_update_entites(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
entity_name: str,
component: FirmwareType,
) -> None:
"""Test the La Marzocco update entities."""
@ -55,43 +45,34 @@ async def test_update_entites(
UPDATE_DOMAIN,
SERVICE_INSTALL,
{
ATTR_ENTITY_ID: f"update.{serial_number}_{entity_name}",
ATTR_ENTITY_ID: f"update.{serial_number}_gateway_firmware",
},
blocking=True,
)
mock_lamarzocco.update_firmware.assert_called_once_with(component)
mock_lamarzocco.update_firmware.assert_called_once_with()
@pytest.mark.parametrize(
("attr", "value"),
[
("side_effect", RequestNotSuccessful("Boom")),
("return_value", False),
],
)
async def test_update_error(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
attr: str,
value: bool | Exception,
) -> None:
"""Test error during update."""
await async_init_integration(hass, mock_config_entry)
state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_machine_firmware")
state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_gateway_firmware")
assert state
setattr(mock_lamarzocco.update_firmware, attr, value)
mock_lamarzocco.update_firmware.side_effect = RequestNotSuccessful("Boom")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
UPDATE_DOMAIN,
SERVICE_INSTALL,
{
ATTR_ENTITY_ID: f"update.{mock_lamarzocco.serial_number}_machine_firmware",
ATTR_ENTITY_ID: f"update.{mock_lamarzocco.serial_number}_gateway_firmware",
},
blocking=True,
)