Skip to content

Commit

Permalink
Split coordinator in lamarzocco (#133208)
Browse files Browse the repository at this point in the history
  • Loading branch information
zweckj authored Dec 15, 2024
1 parent 8938776 commit 0030a97
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 103 deletions.
34 changes: 26 additions & 8 deletions homeassistant/components/lamarzocco/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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
Expand All @@ -25,7 +26,13 @@
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,
Expand Down Expand Up @@ -99,18 +106,29 @@ def bluetooth_configured() -> bool:
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),
)

# 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 = coordinator.device.firmware[FirmwareType.GATEWAY].current_version
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(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
128 changes: 67 additions & 61 deletions homeassistant/components/lamarzocco/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,89 @@

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

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__(
hass,
_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
self._local_client = local_client

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,
)
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

self._last_firmware_data_update: float | None = None
self._last_statistics_data_update: float | None = None
self._local_client = local_client
@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."""
Expand Down Expand Up @@ -96,41 +114,29 @@ async def websocket_close(_: Any | None = None) -> 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))
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
56 changes: 34 additions & 22 deletions homeassistant/components/lamarzocco/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -88,21 +70,51 @@ class 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,
entry: LaMarzoccoConfigEntry,
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."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lamarzocco/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 0030a97

Please sign in to comment.