Split coordinator in lamarzocco (#133208)

This commit is contained in:
Josef Zweck 2024-12-15 21:31:18 +01:00 committed by GitHub
parent 89387760d3
commit 0030a970a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 138 additions and 104 deletions

View File

@ -7,6 +7,7 @@ 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.exceptions import AuthFail, RequestNotSuccessful
from homeassistant.components.bluetooth import async_discovered_service_info
@ -25,7 +26,13 @@ from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator
from .coordinator import (
LaMarzoccoConfigEntry,
LaMarzoccoConfigUpdateCoordinator,
LaMarzoccoFirmwareUpdateCoordinator,
LaMarzoccoRuntimeData,
LaMarzoccoStatisticsUpdateCoordinator,
)
PLATFORMS = [
Platform.BINARY_SENSOR,
@ -99,18 +106,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
address_or_ble_device=entry.data[CONF_MAC],
)
coordinator = LaMarzoccoUpdateCoordinator(
hass=hass,
entry=entry,
local_client=local_client,
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,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
coordinators = LaMarzoccoRuntimeData(
LaMarzoccoConfigUpdateCoordinator(hass, entry, device, local_client),
LaMarzoccoFirmwareUpdateCoordinator(hass, entry, device),
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
)
gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version
# API does not like concurrent requests, so no asyncio.gather here
await coordinators.config_coordinator.async_config_entry_first_refresh()
await coordinators.firmware_coordinator.async_config_entry_first_refresh()
await coordinators.statistics_coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinators
gateway_version = device.firmware[FirmwareType.GATEWAY].current_version
if version.parse(gateway_version) < version.parse("v3.4-rc5"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(

View File

@ -64,7 +64,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up binary sensor entities."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config_coordinator
async_add_entities(
LaMarzoccoBinarySensorEntity(coordinator, description)

View File

@ -57,7 +57,7 @@ async def async_setup_entry(
) -> None:
"""Set up button entities."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config_coordinator
async_add_entities(
LaMarzoccoButtonEntity(coordinator, description)
for description in ENTITIES

View File

@ -36,7 +36,7 @@ async def async_setup_entry(
) -> None:
"""Set up switch entities and services."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config_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()

View File

@ -2,20 +2,18 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine
from abc import abstractmethod
from dataclasses import dataclass
from datetime import timedelta
import logging
from time import time
from typing import Any
from pylamarzocco.clients.bluetooth import LaMarzoccoBluetoothClient
from pylamarzocco.clients.cloud import LaMarzoccoCloudClient
from pylamarzocco.clients.local import LaMarzoccoLocalClient
from pylamarzocco.devices.machine import LaMarzoccoMachine
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -23,26 +21,35 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
FIRMWARE_UPDATE_INTERVAL = 3600
STATISTICS_UPDATE_INTERVAL = 300
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=1)
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=5)
_LOGGER = logging.getLogger(__name__)
type LaMarzoccoConfigEntry = ConfigEntry[LaMarzoccoUpdateCoordinator]
@dataclass
class LaMarzoccoRuntimeData:
"""Runtime data for La Marzocco."""
config_coordinator: LaMarzoccoConfigUpdateCoordinator
firmware_coordinator: LaMarzoccoFirmwareUpdateCoordinator
statistics_coordinator: LaMarzoccoStatisticsUpdateCoordinator
type LaMarzoccoConfigEntry = ConfigEntry[LaMarzoccoRuntimeData]
class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to handle fetching data from the La Marzocco API centrally."""
"""Base class for La Marzocco coordinators."""
_default_update_interval = SCAN_INTERVAL
config_entry: LaMarzoccoConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: LaMarzoccoConfigEntry,
cloud_client: LaMarzoccoCloudClient,
local_client: LaMarzoccoLocalClient | None,
bluetooth_client: LaMarzoccoBluetoothClient | None,
device: LaMarzoccoMachine,
local_client: LaMarzoccoLocalClient | None = None,
) -> None:
"""Initialize coordinator."""
super().__init__(
@ -50,24 +57,35 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
update_interval=self._default_update_interval,
)
self.device = device
self.local_connection_configured = local_client is not None
assert self.config_entry.unique_id
self.device = LaMarzoccoMachine(
model=self.config_entry.data[CONF_MODEL],
serial_number=self.config_entry.unique_id,
name=self.config_entry.data[CONF_NAME],
cloud_client=cloud_client,
local_client=local_client,
bluetooth_client=bluetooth_client,
)
self._last_firmware_data_update: float | None = None
self._last_statistics_data_update: float | None = None
self._local_client = local_client
async def _async_update_data(self) -> None:
"""Do the data update."""
try:
await self._internal_async_update_data()
except AuthFail as ex:
_LOGGER.debug("Authentication failed", exc_info=True)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from ex
except RequestNotSuccessful as ex:
_LOGGER.debug(ex, exc_info=True)
raise UpdateFailed(
translation_domain=DOMAIN, translation_key="api_error"
) from ex
@abstractmethod
async def _internal_async_update_data(self) -> None:
"""Actual data update logic."""
class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Class to handle fetching data from the La Marzocco API centrally."""
async def _async_setup(self) -> None:
"""Set up the coordinator."""
if self._local_client is not None:
@ -96,41 +114,29 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
)
self.config_entry.async_on_unload(websocket_close)
async def _async_update_data(self) -> None:
async def _internal_async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self._async_handle_request(self.device.get_config)
if (
self._last_firmware_data_update is None
or (self._last_firmware_data_update + FIRMWARE_UPDATE_INTERVAL) < time()
):
await self._async_handle_request(self.device.get_firmware)
self._last_firmware_data_update = time()
if (
self._last_statistics_data_update is None
or (self._last_statistics_data_update + STATISTICS_UPDATE_INTERVAL) < time()
):
await self._async_handle_request(self.device.get_statistics)
self._last_statistics_data_update = time()
await self.device.get_config()
_LOGGER.debug("Current status: %s", str(self.device.config))
async def _async_handle_request[**_P](
self,
func: Callable[_P, Coroutine[None, None, None]],
*args: _P.args,
**kwargs: _P.kwargs,
) -> None:
try:
await func(*args, **kwargs)
except AuthFail as ex:
_LOGGER.debug("Authentication failed", exc_info=True)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from ex
except RequestNotSuccessful as ex:
_LOGGER.debug(ex, exc_info=True)
raise UpdateFailed(
translation_domain=DOMAIN, translation_key="api_error"
) from ex
class LaMarzoccoFirmwareUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco firmware."""
_default_update_interval = FIRMWARE_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))
class LaMarzoccoStatisticsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco statistics."""
_default_update_interval = STATISTICS_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))

View File

@ -31,7 +31,7 @@ async def async_get_config_entry_diagnostics(
entry: LaMarzoccoConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config_coordinator
device = coordinator.device
# collect all data sources
diagnostics_data = DiagnosticsData(

View File

@ -210,7 +210,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up number entities."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config_coordinator
entities: list[NumberEntity] = [
LaMarzoccoNumberEntity(coordinator, description)
for description in ENTITIES

View File

@ -107,7 +107,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up select entities."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config_coordinator
async_add_entities(
LaMarzoccoSelectEntity(coordinator, description)

View File

@ -33,24 +33,6 @@ class LaMarzoccoSensorEntityDescription(
ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
LaMarzoccoSensorEntityDescription(
key="drink_stats_coffee",
translation_key="drink_stats_coffee",
native_unit_of_measurement="drinks",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.statistics.drink_stats.get(PhysicalKey.A, 0),
available_fn=lambda device: len(device.statistics.drink_stats) > 0,
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoSensorEntityDescription(
key="drink_stats_flushing",
translation_key="drink_stats_flushing",
native_unit_of_measurement="drinks",
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,
),
LaMarzoccoSensorEntityDescription(
key="shot_timer",
translation_key="shot_timer",
@ -88,6 +70,27 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
),
)
STATISTIC_ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
LaMarzoccoSensorEntityDescription(
key="drink_stats_coffee",
translation_key="drink_stats_coffee",
native_unit_of_measurement="drinks",
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda device: device.statistics.drink_stats.get(PhysicalKey.A, 0),
available_fn=lambda device: len(device.statistics.drink_stats) > 0,
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoSensorEntityDescription(
key="drink_stats_flushing",
translation_key="drink_stats_flushing",
native_unit_of_measurement="drinks",
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,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@ -95,14 +98,23 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensor entities."""
coordinator = entry.runtime_data
config_coordinator = entry.runtime_data.config_coordinator
async_add_entities(
LaMarzoccoSensorEntity(coordinator, description)
entities = [
LaMarzoccoSensorEntity(config_coordinator, description)
for description in ENTITIES
if description.supported_fn(coordinator)
if description.supported_fn(config_coordinator)
]
statistics_coordinator = entry.runtime_data.statistics_coordinator
entities.extend(
LaMarzoccoSensorEntity(statistics_coordinator, description)
for description in STATISTIC_ENTITIES
if description.supported_fn(statistics_coordinator)
)
async_add_entities(entities)
class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity):
"""Sensor representing espresso machine temperature data."""

View File

@ -68,7 +68,7 @@ async def async_setup_entry(
) -> None:
"""Set up switch entities and services."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config_coordinator
entities: list[SwitchEntity] = []
entities.extend(

View File

@ -59,7 +59,7 @@ async def async_setup_entry(
) -> None:
"""Create update entities."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.firmware_coordinator
async_add_entities(
LaMarzoccoUpdateEntity(coordinator, description)
for description in ENTITIES

View File

@ -143,7 +143,7 @@ def mock_lamarzocco(device_fixture: MachineModel) -> Generator[MagicMock]:
with (
patch(
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine",
"homeassistant.components.lamarzocco.LaMarzoccoMachine",
autospec=True,
) as lamarzocco_mock,
):

View File

@ -174,9 +174,7 @@ async def test_bluetooth_is_set_from_discovery(
"homeassistant.components.lamarzocco.async_discovered_service_info",
return_value=[service_info],
) as discovery,
patch(
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine"
) as init_device,
patch("homeassistant.components.lamarzocco.LaMarzoccoMachine") as init_device,
):
await async_init_integration(hass, mock_config_entry)
discovery.assert_called_once()