mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00
Split coordinator in lamarzocco (#133208)
This commit is contained in:
parent
89387760d3
commit
0030a970a1
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
):
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user