mirror of
https://github.com/home-assistant/core.git
synced 2025-04-19 14:57:52 +00:00
Merge branch 'dev' into esphome_reconfig
This commit is contained in:
commit
3ba98c64ed
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from aioesphomeapi import APIClient
|
||||
|
||||
from homeassistant.components import ffmpeg, zeroconf
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.bluetooth import async_remove_scanner
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@ -17,13 +17,10 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import async_delete_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_BLUETOOTH_MAC_ADDRESS, CONF_NOISE_PSK, DATA_FFMPEG_PROXY, DOMAIN
|
||||
from .dashboard import async_setup as async_setup_dashboard
|
||||
from . import dashboard, ffmpeg_proxy
|
||||
from .const import CONF_BLUETOOTH_MAC_ADDRESS, CONF_NOISE_PSK, DOMAIN
|
||||
from .domain_data import DomainData
|
||||
|
||||
# Import config flow so that it's added to the registry
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
from .ffmpeg_proxy import FFmpegProxyData, FFmpegProxyView
|
||||
from .manager import DEVICE_CONFLICT_ISSUE_FORMAT, ESPHomeManager, cleanup_instance
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@ -33,12 +30,8 @@ CLIENT_INFO = f"Home Assistant {ha_version}"
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the esphome component."""
|
||||
proxy_data = hass.data[DATA_FFMPEG_PROXY] = FFmpegProxyData()
|
||||
|
||||
await async_setup_dashboard(hass)
|
||||
hass.http.register_view(
|
||||
FFmpegProxyView(ffmpeg.get_ffmpeg_manager(hass), proxy_data)
|
||||
)
|
||||
ffmpeg_proxy.async_setup(hass)
|
||||
await dashboard.async_setup(hass)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -49,6 +49,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
from .manager import async_replace_device
|
||||
|
||||
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||
@ -710,7 +711,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: ESPHomeConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler()
|
||||
|
@ -25,5 +25,3 @@ PROJECT_URLS = {
|
||||
# ESPHome always uses .0 for the changelog URL
|
||||
STABLE_BLE_URL_VERSION = f"{STABLE_BLE_VERSION.major}.{STABLE_BLE_VERSION.minor}.0"
|
||||
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_URL_VERSION}.html"
|
||||
|
||||
DATA_FFMPEG_PROXY = f"{DOMAIN}.ffmpeg_proxy"
|
||||
|
@ -194,6 +194,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
device_entry: dr.DeviceEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -11,17 +11,20 @@ from typing import Final
|
||||
from aiohttp import web
|
||||
from aiohttp.abc import AbstractStreamWriter, BaseRequest
|
||||
|
||||
from homeassistant.components import ffmpeg
|
||||
from homeassistant.components.ffmpeg import FFmpegManager
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DATA_FFMPEG_PROXY
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MAX_CONVERSIONS_PER_DEVICE: Final[int] = 2
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_proxy_url(
|
||||
hass: HomeAssistant,
|
||||
device_id: str,
|
||||
@ -32,7 +35,7 @@ def async_create_proxy_url(
|
||||
width: int | None = None,
|
||||
) -> str:
|
||||
"""Create a use proxy URL that automatically converts the media."""
|
||||
data: FFmpegProxyData = hass.data[DATA_FFMPEG_PROXY]
|
||||
data = hass.data[DATA_FFMPEG_PROXY]
|
||||
return data.async_create_proxy_url(
|
||||
device_id, media_url, media_format, rate, channels, width
|
||||
)
|
||||
@ -313,3 +316,16 @@ class FFmpegProxyView(HomeAssistantView):
|
||||
assert writer is not None
|
||||
await resp.transcode(request, writer)
|
||||
return resp
|
||||
|
||||
|
||||
DATA_FFMPEG_PROXY: HassKey[FFmpegProxyData] = HassKey(f"{DOMAIN}.ffmpeg_proxy")
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the ffmpeg proxy."""
|
||||
proxy_data = FFmpegProxyData()
|
||||
hass.data[DATA_FFMPEG_PROXY] = proxy_data
|
||||
hass.http.register_view(
|
||||
FFmpegProxyView(ffmpeg.get_ffmpeg_manager(hass), proxy_data)
|
||||
)
|
||||
|
20
homeassistant/components/esphome/icons.json
Normal file
20
homeassistant/components/esphome/icons.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"assist_in_progress": {
|
||||
"default": "mdi:timer-sand"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"pipeline": {
|
||||
"default": "mdi:filter-outline"
|
||||
},
|
||||
"vad_sensitivity": {
|
||||
"default": "mdi:volume-high"
|
||||
},
|
||||
"wake_word": {
|
||||
"default": "mdi:microphone"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -148,10 +148,6 @@ class EsphomeMediaPlayer(
|
||||
announcement: bool,
|
||||
) -> str | None:
|
||||
"""Get URL for ffmpeg proxy."""
|
||||
if self.device_entry is None:
|
||||
# Device id is required
|
||||
return None
|
||||
|
||||
# Choose the first default or announcement supported format
|
||||
format_to_use: MediaPlayerSupportedFormat | None = None
|
||||
for supported_format in supported_formats:
|
||||
|
@ -20,13 +20,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from .entity import EsphomeEntity, platform_async_setup_entry
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
from .enum_mapper import EsphomeEnumMapper
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@ -34,7 +34,7 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ESPHomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up esphome sensors based on a config entry."""
|
||||
|
@ -18,7 +18,6 @@ from homeassistant.components.update import (
|
||||
UpdateEntity,
|
||||
UpdateEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@ -36,7 +35,7 @@ from .entity import (
|
||||
esphome_state_property,
|
||||
platform_async_setup_entry,
|
||||
)
|
||||
from .entry_data import RuntimeEntryData
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@ -47,7 +46,7 @@ NO_FEATURES = UpdateEntityFeature(0)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ESPHomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up ESPHome update based on a config entry."""
|
||||
|
@ -252,9 +252,7 @@ class HomeConnectCoordinator(
|
||||
appliance_data = await self._get_appliance_data(
|
||||
appliance_info, self.data.get(appliance_info.ha_id)
|
||||
)
|
||||
if event_message_ha_id in self.data:
|
||||
self.data[event_message_ha_id].update(appliance_data)
|
||||
else:
|
||||
if event_message_ha_id not in self.data:
|
||||
self.data[event_message_ha_id] = appliance_data
|
||||
for listener, context in self._special_listeners.values():
|
||||
if (
|
||||
|
@ -17,7 +17,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
AVAILABLE_MAPS_ENUM,
|
||||
BEAN_AMOUNT_OPTIONS,
|
||||
BEAN_CONTAINER_OPTIONS,
|
||||
@ -313,7 +312,7 @@ def _get_entities_for_appliance(
|
||||
HomeConnectProgramSelectEntity(entry.runtime_data, appliance, desc)
|
||||
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
|
||||
]
|
||||
if appliance.info.type in APPLIANCES_WITH_PROGRAMS
|
||||
if appliance.programs
|
||||
else []
|
||||
),
|
||||
*[
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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}"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -251,7 +251,7 @@ DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.UPDATE,
|
||||
entity_description=UpdateEntityDescription(
|
||||
key="MatterUpdate", device_class=UpdateDeviceClass.FIRMWARE, name=None
|
||||
key="MatterUpdate", device_class=UpdateDeviceClass.FIRMWARE
|
||||
),
|
||||
entity_class=MatterUpdate,
|
||||
required_attributes=(
|
||||
|
80
homeassistant/components/miele/diagnostics.py
Normal file
80
homeassistant/components/miele/diagnostics.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Diagnostics support for Miele."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import MieleConfigEntry
|
||||
|
||||
TO_REDACT = {"access_token", "refresh_token", "fabNumber"}
|
||||
|
||||
|
||||
def hash_identifier(key: str) -> str:
|
||||
"""Hash the identifier string."""
|
||||
return f"**REDACTED_{hashlib.sha256(key.encode()).hexdigest()[:16]}"
|
||||
|
||||
|
||||
def redact_identifiers(in_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Redact identifiers from the data."""
|
||||
for key in in_data:
|
||||
in_data[hash_identifier(key)] = in_data.pop(key)
|
||||
return in_data
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: MieleConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
miele_data = {
|
||||
"devices": redact_identifiers(
|
||||
{
|
||||
device_id: device_data.raw
|
||||
for device_id, device_data in config_entry.runtime_data.data.devices.items()
|
||||
}
|
||||
),
|
||||
"actions": redact_identifiers(
|
||||
{
|
||||
device_id: action_data.raw
|
||||
for device_id, action_data in config_entry.runtime_data.data.actions.items()
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
|
||||
"miele_data": async_redact_data(miele_data, TO_REDACT),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, config_entry: MieleConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
info = {
|
||||
"manufacturer": device.manufacturer,
|
||||
"model": device.model,
|
||||
}
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
device_id = cast(str, device.serial_number)
|
||||
miele_data = {
|
||||
"devices": {
|
||||
hash_identifier(device_id): coordinator.data.devices[device_id].raw
|
||||
},
|
||||
"actions": {
|
||||
hash_identifier(device_id): coordinator.data.actions[device_id].raw
|
||||
},
|
||||
"programs": "Not implemented",
|
||||
}
|
||||
return {
|
||||
"info": async_redact_data(info, TO_REDACT),
|
||||
"data": async_redact_data(config_entry.data, TO_REDACT),
|
||||
"miele_data": async_redact_data(miele_data, TO_REDACT),
|
||||
}
|
@ -104,7 +104,7 @@ class LazyState(State):
|
||||
return self._last_updated_ts
|
||||
|
||||
@cached_property
|
||||
def last_changed_timestamp(self) -> float: # type: ignore[override]
|
||||
def last_changed_timestamp(self) -> float:
|
||||
"""Last changed timestamp."""
|
||||
ts = self._last_changed_ts or self._last_updated_ts
|
||||
if TYPE_CHECKING:
|
||||
@ -112,7 +112,7 @@ class LazyState(State):
|
||||
return ts
|
||||
|
||||
@cached_property
|
||||
def last_reported_timestamp(self) -> float: # type: ignore[override]
|
||||
def last_reported_timestamp(self) -> float:
|
||||
"""Last reported timestamp."""
|
||||
ts = self._last_reported_ts or self._last_updated_ts
|
||||
if TYPE_CHECKING:
|
||||
|
@ -22,34 +22,34 @@ from homeassistant.helpers.network import is_internal_request
|
||||
from .const import UNPLAYABLE_TYPES
|
||||
|
||||
LIBRARY = [
|
||||
"Favorites",
|
||||
"Artists",
|
||||
"Albums",
|
||||
"Tracks",
|
||||
"Playlists",
|
||||
"Genres",
|
||||
"New Music",
|
||||
"Album Artists",
|
||||
"Apps",
|
||||
"Radios",
|
||||
"favorites",
|
||||
"artists",
|
||||
"albums",
|
||||
"tracks",
|
||||
"playlists",
|
||||
"genres",
|
||||
"new music",
|
||||
"album artists",
|
||||
"apps",
|
||||
"radios",
|
||||
]
|
||||
|
||||
MEDIA_TYPE_TO_SQUEEZEBOX: dict[str | MediaType, str] = {
|
||||
"Favorites": "favorites",
|
||||
"Artists": "artists",
|
||||
"Albums": "albums",
|
||||
"Tracks": "titles",
|
||||
"Playlists": "playlists",
|
||||
"Genres": "genres",
|
||||
"New Music": "new music",
|
||||
"Album Artists": "album artists",
|
||||
"favorites": "favorites",
|
||||
"artists": "artists",
|
||||
"albums": "albums",
|
||||
"tracks": "titles",
|
||||
"playlists": "playlists",
|
||||
"genres": "genres",
|
||||
"new music": "new music",
|
||||
"album artists": "album artists",
|
||||
MediaType.ALBUM: "album",
|
||||
MediaType.ARTIST: "artist",
|
||||
MediaType.TRACK: "title",
|
||||
MediaType.PLAYLIST: "playlist",
|
||||
MediaType.GENRE: "genre",
|
||||
"Apps": "apps",
|
||||
"Radios": "radios",
|
||||
MediaType.APPS: "apps",
|
||||
"radios": "radios",
|
||||
}
|
||||
|
||||
SQUEEZEBOX_ID_BY_TYPE: dict[str | MediaType, str] = {
|
||||
@ -58,22 +58,20 @@ SQUEEZEBOX_ID_BY_TYPE: dict[str | MediaType, str] = {
|
||||
MediaType.TRACK: "track_id",
|
||||
MediaType.PLAYLIST: "playlist_id",
|
||||
MediaType.GENRE: "genre_id",
|
||||
"Favorites": "item_id",
|
||||
"favorites": "item_id",
|
||||
MediaType.APPS: "item_id",
|
||||
}
|
||||
|
||||
CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | str]] = {
|
||||
"Favorites": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
|
||||
"Apps": {"item": MediaClass.DIRECTORY, "children": MediaClass.APP},
|
||||
"Radios": {"item": MediaClass.DIRECTORY, "children": MediaClass.APP},
|
||||
"App": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
|
||||
"Artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
|
||||
"Albums": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
|
||||
"Tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
|
||||
"Playlists": {"item": MediaClass.DIRECTORY, "children": MediaClass.PLAYLIST},
|
||||
"Genres": {"item": MediaClass.DIRECTORY, "children": MediaClass.GENRE},
|
||||
"New Music": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
|
||||
"Album Artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
|
||||
"favorites": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
|
||||
"radios": {"item": MediaClass.DIRECTORY, "children": MediaClass.APP},
|
||||
"artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
|
||||
"albums": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
|
||||
"tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
|
||||
"playlists": {"item": MediaClass.DIRECTORY, "children": MediaClass.PLAYLIST},
|
||||
"genres": {"item": MediaClass.DIRECTORY, "children": MediaClass.GENRE},
|
||||
"new music": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
|
||||
"album artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
|
||||
MediaType.ALBUM: {"item": MediaClass.ALBUM, "children": MediaClass.TRACK},
|
||||
MediaType.ARTIST: {"item": MediaClass.ARTIST, "children": MediaClass.ALBUM},
|
||||
MediaType.TRACK: {"item": MediaClass.TRACK, "children": ""},
|
||||
@ -91,17 +89,15 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[
|
||||
MediaType.PLAYLIST: MediaType.PLAYLIST,
|
||||
MediaType.ARTIST: MediaType.ALBUM,
|
||||
MediaType.GENRE: MediaType.ARTIST,
|
||||
"Artists": MediaType.ARTIST,
|
||||
"Albums": MediaType.ALBUM,
|
||||
"Tracks": MediaType.TRACK,
|
||||
"Playlists": MediaType.PLAYLIST,
|
||||
"Genres": MediaType.GENRE,
|
||||
"Favorites": None, # can only be determined after inspecting the item
|
||||
"Apps": MediaClass.APP,
|
||||
"Radios": MediaClass.APP,
|
||||
"App": None, # can only be determined after inspecting the item
|
||||
"New Music": MediaType.ALBUM,
|
||||
"Album Artists": MediaType.ARTIST,
|
||||
"artists": MediaType.ARTIST,
|
||||
"albums": MediaType.ALBUM,
|
||||
"tracks": MediaType.TRACK,
|
||||
"playlists": MediaType.PLAYLIST,
|
||||
"genres": MediaType.GENRE,
|
||||
"favorites": None, # can only be determined after inspecting the item
|
||||
"radios": MediaClass.APP,
|
||||
"new music": MediaType.ALBUM,
|
||||
"album artists": MediaType.ARTIST,
|
||||
MediaType.APPS: MediaType.APP,
|
||||
MediaType.APP: MediaType.TRACK,
|
||||
}
|
||||
@ -173,7 +169,7 @@ def _build_response_known_app(
|
||||
|
||||
|
||||
def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia:
|
||||
"""Build item for Favorites."""
|
||||
"""Build item for favorites."""
|
||||
if "album_id" in item:
|
||||
return BrowseMedia(
|
||||
media_content_id=str(item["album_id"]),
|
||||
@ -183,21 +179,21 @@ def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia:
|
||||
can_expand=True,
|
||||
can_play=True,
|
||||
)
|
||||
if item["hasitems"] and not item["isaudio"]:
|
||||
if item.get("hasitems") and not item.get("isaudio"):
|
||||
return BrowseMedia(
|
||||
media_content_id=item["id"],
|
||||
title=item["title"],
|
||||
media_content_type="Favorites",
|
||||
media_class=CONTENT_TYPE_MEDIA_CLASS["Favorites"]["item"],
|
||||
media_content_type="favorites",
|
||||
media_class=CONTENT_TYPE_MEDIA_CLASS["favorites"]["item"],
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
)
|
||||
return BrowseMedia(
|
||||
media_content_id=item["id"],
|
||||
title=item["title"],
|
||||
media_content_type="Favorites",
|
||||
media_content_type="favorites",
|
||||
media_class=CONTENT_TYPE_MEDIA_CLASS[MediaType.TRACK]["item"],
|
||||
can_expand=item["hasitems"],
|
||||
can_expand=bool(item.get("hasitems")),
|
||||
can_play=bool(item["isaudio"] and item.get("url")),
|
||||
)
|
||||
|
||||
@ -220,7 +216,7 @@ def _get_item_thumbnail(
|
||||
item_type, item["id"], artwork_track_id
|
||||
)
|
||||
|
||||
elif search_type in ["Apps", "Radios"]:
|
||||
elif search_type in ["apps", "radios"]:
|
||||
item_thumbnail = player.generate_image_url(item["icon"])
|
||||
if item_thumbnail is None:
|
||||
item_thumbnail = item.get("image_url") # will not be proxied by HA
|
||||
@ -265,10 +261,10 @@ async def build_item_response(
|
||||
for item in result["items"]:
|
||||
# Force the item id to a string in case it's numeric from some lms
|
||||
item["id"] = str(item.get("id", ""))
|
||||
if search_type == "Favorites":
|
||||
if search_type == "favorites":
|
||||
child_media = _build_response_favorites(item)
|
||||
|
||||
elif search_type in ["Apps", "Radios"]:
|
||||
elif search_type in ["apps", "radios"]:
|
||||
# item["cmd"] contains the name of the command to use with the cli for the app
|
||||
# add the command to the dictionaries
|
||||
if item["title"] == "Search" or item.get("type") in UNPLAYABLE_TYPES:
|
||||
@ -364,11 +360,11 @@ async def library_payload(
|
||||
assert media_class["children"] is not None
|
||||
library_info["children"].append(
|
||||
BrowseMedia(
|
||||
title=item,
|
||||
title=item.title(),
|
||||
media_class=media_class["children"],
|
||||
media_content_id=item,
|
||||
media_content_type=item,
|
||||
can_play=item not in ["Favorites", "Apps", "Radios"],
|
||||
can_play=item not in ["favorites", "apps", "radios"],
|
||||
can_expand=True,
|
||||
)
|
||||
)
|
||||
|
@ -446,6 +446,9 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
"""Send the play_media command to the media player."""
|
||||
index = None
|
||||
|
||||
if media_type:
|
||||
media_type = media_type.lower()
|
||||
|
||||
enqueue: MediaPlayerEnqueue | None = kwargs.get(ATTR_MEDIA_ENQUEUE)
|
||||
|
||||
if enqueue == MediaPlayerEnqueue.ADD:
|
||||
@ -617,6 +620,9 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
media_content_id,
|
||||
)
|
||||
|
||||
if media_content_type:
|
||||
media_content_type = media_content_type.lower()
|
||||
|
||||
if media_content_type in [None, "library"]:
|
||||
return await library_payload(self.hass, self._player, self._browse_data)
|
||||
|
||||
|
92
homeassistant/components/whirlpool/quality_scale.yaml
Normal file
92
homeassistant/components/whirlpool/quality_scale.yaml
Normal file
@ -0,0 +1,92 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: todo
|
||||
comment: |
|
||||
When fetch_appliances fails, ConfigEntryNotReady should be raised.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: todo
|
||||
comment: |
|
||||
- The calls to the api can be changed to return bool, and services can then raise HomeAssistantError
|
||||
- Current services raise ValueError and should raise ServiceValidationError instead.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration has no configuration parameters
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
- Test helper init_integration() does not set a unique_id
|
||||
- Merge test_setup_http_exception and test_setup_auth_account_locked
|
||||
- The climate platform is at 94%
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration is a cloud service and thus does not support discovery.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration is a cloud service and thus does not support discovery.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: The "unknown" state should not be part of the enum for the dispense level sensor.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: todo
|
||||
comment: |
|
||||
Time remaining sensor still has hardcoded icon.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No known use cases for repair issues or flows, yet
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
@ -363,11 +363,17 @@ class DriverEvents:
|
||||
self.dev_reg.async_get_device(identifiers={get_device_id(driver, node)})
|
||||
for node in controller.nodes.values()
|
||||
]
|
||||
provisioned_devices = [
|
||||
self.dev_reg.async_get(entry.additional_properties["device_id"])
|
||||
for entry in await controller.async_get_provisioning_entries()
|
||||
if entry.additional_properties
|
||||
and "device_id" in entry.additional_properties
|
||||
]
|
||||
|
||||
# Devices that are in the device registry that are not known by the controller
|
||||
# can be removed
|
||||
for device in stored_devices:
|
||||
if device not in known_devices:
|
||||
if device not in known_devices and device not in provisioned_devices:
|
||||
self.dev_reg.async_remove_device(device.id)
|
||||
|
||||
# run discovery on controller node
|
||||
@ -448,6 +454,8 @@ class ControllerEvents:
|
||||
)
|
||||
)
|
||||
|
||||
await self.async_check_preprovisioned_device(node)
|
||||
|
||||
if node.is_controller_node:
|
||||
# Create a controller status sensor for each device
|
||||
async_dispatcher_send(
|
||||
@ -497,7 +505,7 @@ class ControllerEvents:
|
||||
|
||||
# we do submit the node to device registry so user has
|
||||
# some visual feedback that something is (in the process of) being added
|
||||
self.register_node_in_dev_reg(node)
|
||||
await self.async_register_node_in_dev_reg(node)
|
||||
|
||||
@callback
|
||||
def async_on_node_removed(self, event: dict) -> None:
|
||||
@ -574,18 +582,52 @@ class ControllerEvents:
|
||||
f"{DOMAIN}.identify_controller.{dev_id[1]}",
|
||||
)
|
||||
|
||||
@callback
|
||||
def register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry:
|
||||
async def async_check_preprovisioned_device(self, node: ZwaveNode) -> None:
|
||||
"""Check if the node was preprovisioned and update the device registry."""
|
||||
provisioning_entry = (
|
||||
await self.driver_events.driver.controller.async_get_provisioning_entry(
|
||||
node.node_id
|
||||
)
|
||||
)
|
||||
if (
|
||||
provisioning_entry
|
||||
and provisioning_entry.additional_properties
|
||||
and "device_id" in provisioning_entry.additional_properties
|
||||
):
|
||||
preprovisioned_device = self.dev_reg.async_get(
|
||||
provisioning_entry.additional_properties["device_id"]
|
||||
)
|
||||
|
||||
if preprovisioned_device:
|
||||
dsk = provisioning_entry.dsk
|
||||
dsk_identifier = (DOMAIN, f"provision_{dsk}")
|
||||
|
||||
# If the pre-provisioned device has the DSK identifier, remove it
|
||||
if dsk_identifier in preprovisioned_device.identifiers:
|
||||
driver = self.driver_events.driver
|
||||
device_id = get_device_id(driver, node)
|
||||
device_id_ext = get_device_id_ext(driver, node)
|
||||
new_identifiers = preprovisioned_device.identifiers.copy()
|
||||
new_identifiers.remove(dsk_identifier)
|
||||
new_identifiers.add(device_id)
|
||||
if device_id_ext:
|
||||
new_identifiers.add(device_id_ext)
|
||||
self.dev_reg.async_update_device(
|
||||
preprovisioned_device.id,
|
||||
new_identifiers=new_identifiers,
|
||||
)
|
||||
|
||||
async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry:
|
||||
"""Register node in dev reg."""
|
||||
driver = self.driver_events.driver
|
||||
device_id = get_device_id(driver, node)
|
||||
device_id_ext = get_device_id_ext(driver, node)
|
||||
node_id_device = self.dev_reg.async_get_device(identifiers={device_id})
|
||||
via_device_id = None
|
||||
via_identifier = None
|
||||
controller = driver.controller
|
||||
# Get the controller node device ID if this node is not the controller
|
||||
if controller.own_node and controller.own_node != node:
|
||||
via_device_id = get_device_id(driver, controller.own_node)
|
||||
via_identifier = get_device_id(driver, controller.own_node)
|
||||
|
||||
if device_id_ext:
|
||||
# If there is a device with this node ID but with a different hardware
|
||||
@ -632,7 +674,7 @@ class ControllerEvents:
|
||||
model=node.device_config.label,
|
||||
manufacturer=node.device_config.manufacturer,
|
||||
suggested_area=node.location if node.location else UNDEFINED,
|
||||
via_device=via_device_id,
|
||||
via_device=via_identifier,
|
||||
)
|
||||
|
||||
async_dispatcher_send(self.hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device)
|
||||
@ -666,7 +708,7 @@ class NodeEvents:
|
||||
"""Handle node ready event."""
|
||||
LOGGER.debug("Processing node %s", node)
|
||||
# register (or update) node in device registry
|
||||
device = self.controller_events.register_node_in_dev_reg(node)
|
||||
device = await self.controller_events.async_register_node_in_dev_reg(node)
|
||||
|
||||
# Remove any old value ids if this is a reinterview.
|
||||
self.controller_events.discovered_value_ids.pop(device.id, None)
|
||||
|
@ -91,6 +91,7 @@ from .const import (
|
||||
from .helpers import (
|
||||
async_enable_statistics,
|
||||
async_get_node_from_device_id,
|
||||
async_get_provisioning_entry_from_device_id,
|
||||
get_device_id,
|
||||
)
|
||||
|
||||
@ -171,6 +172,10 @@ ADDITIONAL_PROPERTIES = "additional_properties"
|
||||
STATUS = "status"
|
||||
REQUESTED_SECURITY_CLASSES = "requestedSecurityClasses"
|
||||
|
||||
PROTOCOL = "protocol"
|
||||
DEVICE_NAME = "device_name"
|
||||
AREA_ID = "area_id"
|
||||
|
||||
FEATURE = "feature"
|
||||
STRATEGY = "strategy"
|
||||
|
||||
@ -398,6 +403,7 @@ def async_register_api(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_s2_inclusion)
|
||||
websocket_api.async_register_command(hass, websocket_grant_security_classes)
|
||||
websocket_api.async_register_command(hass, websocket_validate_dsk_and_enter_pin)
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_new_devices)
|
||||
websocket_api.async_register_command(hass, websocket_provision_smart_start_node)
|
||||
websocket_api.async_register_command(hass, websocket_unprovision_smart_start_node)
|
||||
websocket_api.async_register_command(hass, websocket_get_provisioning_entries)
|
||||
@ -631,14 +637,38 @@ async def websocket_node_metadata(
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_get_node
|
||||
async def websocket_node_alerts(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
node: Node,
|
||||
) -> None:
|
||||
"""Get the alerts for a Z-Wave JS node."""
|
||||
try:
|
||||
node = async_get_node_from_device_id(hass, msg[DEVICE_ID])
|
||||
except ValueError as err:
|
||||
if "can't be found" in err.args[0]:
|
||||
provisioning_entry = await async_get_provisioning_entry_from_device_id(
|
||||
hass, msg[DEVICE_ID]
|
||||
)
|
||||
if provisioning_entry:
|
||||
connection.send_result(
|
||||
msg[ID],
|
||||
{
|
||||
"comments": [
|
||||
{
|
||||
"level": "info",
|
||||
"text": "This device has been provisioned but is not yet included in the "
|
||||
"network.",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
else:
|
||||
connection.send_error(msg[ID], ERR_NOT_FOUND, str(err))
|
||||
else:
|
||||
connection.send_error(msg[ID], ERR_NOT_LOADED, str(err))
|
||||
return
|
||||
|
||||
connection.send_result(
|
||||
msg[ID],
|
||||
{
|
||||
@ -971,12 +1001,58 @@ async def websocket_validate_dsk_and_enter_pin(
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/subscribe_new_devices",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_subscribe_new_devices(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Subscribe to new devices."""
|
||||
|
||||
@callback
|
||||
def async_cleanup() -> None:
|
||||
for unsub in unsubs:
|
||||
unsub()
|
||||
|
||||
@callback
|
||||
def device_registered(device: dr.DeviceEntry) -> None:
|
||||
device_details = {
|
||||
"name": device.name,
|
||||
"id": device.id,
|
||||
"manufacturer": device.manufacturer,
|
||||
"model": device.model,
|
||||
}
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg[ID], {"event": "device registered", "device": device_details}
|
||||
)
|
||||
)
|
||||
|
||||
connection.subscriptions[msg["id"]] = async_cleanup
|
||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||
async_dispatcher_connect(
|
||||
hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered
|
||||
),
|
||||
]
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/provision_smart_start_node",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Required(QR_PROVISIONING_INFORMATION): QR_PROVISIONING_INFORMATION_SCHEMA,
|
||||
vol.Optional(PROTOCOL): vol.Coerce(Protocols),
|
||||
vol.Optional(DEVICE_NAME): str,
|
||||
vol.Optional(AREA_ID): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@ -991,18 +1067,68 @@ async def websocket_provision_smart_start_node(
|
||||
driver: Driver,
|
||||
) -> None:
|
||||
"""Pre-provision a smart start node."""
|
||||
qr_info = msg[QR_PROVISIONING_INFORMATION]
|
||||
|
||||
provisioning_info = msg[QR_PROVISIONING_INFORMATION]
|
||||
|
||||
if provisioning_info.version == QRCodeVersion.S2:
|
||||
if qr_info.version == QRCodeVersion.S2:
|
||||
connection.send_error(
|
||||
msg[ID],
|
||||
ERR_INVALID_FORMAT,
|
||||
"QR code version S2 is not supported for this command",
|
||||
)
|
||||
return
|
||||
|
||||
provisioning_info = ProvisioningEntry(
|
||||
dsk=qr_info.dsk,
|
||||
security_classes=qr_info.security_classes,
|
||||
requested_security_classes=qr_info.requested_security_classes,
|
||||
protocol=msg.get(PROTOCOL),
|
||||
additional_properties=qr_info.additional_properties,
|
||||
)
|
||||
|
||||
device = None
|
||||
# Create an empty device if device_name is provided
|
||||
if device_name := msg.get(DEVICE_NAME):
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
# Create a unique device identifier using the DSK
|
||||
device_identifier = (DOMAIN, f"provision_{qr_info.dsk}")
|
||||
|
||||
manufacturer = None
|
||||
model = None
|
||||
|
||||
device_info = await driver.config_manager.lookup_device(
|
||||
qr_info.manufacturer_id,
|
||||
qr_info.product_type,
|
||||
qr_info.product_id,
|
||||
)
|
||||
if device_info:
|
||||
manufacturer = device_info.manufacturer
|
||||
model = device_info.label
|
||||
|
||||
# Create an empty device
|
||||
device = dev_reg.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={device_identifier},
|
||||
name=device_name,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
via_device=get_device_id(driver, driver.controller.own_node)
|
||||
if driver.controller.own_node
|
||||
else None,
|
||||
)
|
||||
dev_reg.async_update_device(
|
||||
device.id, area_id=msg.get(AREA_ID), name_by_user=device_name
|
||||
)
|
||||
|
||||
if provisioning_info.additional_properties is None:
|
||||
provisioning_info.additional_properties = {}
|
||||
provisioning_info.additional_properties["device_id"] = device.id
|
||||
|
||||
await driver.controller.async_provision_smart_start_node(provisioning_info)
|
||||
connection.send_result(msg[ID])
|
||||
if device:
|
||||
connection.send_result(msg[ID], device.id)
|
||||
else:
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@ -1036,7 +1162,24 @@ async def websocket_unprovision_smart_start_node(
|
||||
)
|
||||
return
|
||||
dsk_or_node_id = msg.get(DSK) or msg[NODE_ID]
|
||||
provisioning_entry = await driver.controller.async_get_provisioning_entry(
|
||||
dsk_or_node_id
|
||||
)
|
||||
if (
|
||||
provisioning_entry
|
||||
and provisioning_entry.additional_properties
|
||||
and "device_id" in provisioning_entry.additional_properties
|
||||
):
|
||||
device_identifier = (DOMAIN, f"provision_{provisioning_entry.dsk}")
|
||||
device_id = provisioning_entry.additional_properties["device_id"]
|
||||
dev_reg = dr.async_get(hass)
|
||||
device = dev_reg.async_get(device_id)
|
||||
if device and device.identifiers == {device_identifier}:
|
||||
# Only remove the device if nothing else has claimed it
|
||||
dev_reg.async_remove_device(device_id)
|
||||
|
||||
await driver.controller.async_unprovision_smart_start_node(dsk_or_node_id)
|
||||
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
|
@ -15,7 +15,7 @@ from zwave_js_server.const import (
|
||||
ConfigurationValueType,
|
||||
LogLevel,
|
||||
)
|
||||
from zwave_js_server.model.controller import Controller
|
||||
from zwave_js_server.model.controller import Controller, ProvisioningEntry
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.log_config import LogConfig
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
@ -233,7 +233,7 @@ def get_home_and_node_id_from_device_entry(
|
||||
),
|
||||
None,
|
||||
)
|
||||
if device_id is None:
|
||||
if device_id is None or device_id.startswith("provision_"):
|
||||
return None
|
||||
id_ = device_id.split("-")
|
||||
return (id_[0], int(id_[1]))
|
||||
@ -264,12 +264,12 @@ def async_get_node_from_device_id(
|
||||
),
|
||||
None,
|
||||
)
|
||||
if entry and entry.state != ConfigEntryState.LOADED:
|
||||
raise ValueError(f"Device {device_id} config entry is not loaded")
|
||||
if entry is None:
|
||||
raise ValueError(
|
||||
f"Device {device_id} is not from an existing zwave_js config entry"
|
||||
)
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
raise ValueError(f"Device {device_id} config entry is not loaded")
|
||||
|
||||
client: ZwaveClient = entry.runtime_data[DATA_CLIENT]
|
||||
driver = client.driver
|
||||
@ -289,6 +289,53 @@ def async_get_node_from_device_id(
|
||||
return driver.controller.nodes[node_id]
|
||||
|
||||
|
||||
async def async_get_provisioning_entry_from_device_id(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> ProvisioningEntry | None:
|
||||
"""Get provisioning entry from a device ID.
|
||||
|
||||
Raises ValueError if device is invalid
|
||||
"""
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
if not (device_entry := dev_reg.async_get(device_id)):
|
||||
raise ValueError(f"Device ID {device_id} is not valid")
|
||||
|
||||
# Use device config entry ID's to validate that this is a valid zwave_js device
|
||||
# and to get the client
|
||||
config_entry_ids = device_entry.config_entries
|
||||
entry = next(
|
||||
(
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.entry_id in config_entry_ids
|
||||
),
|
||||
None,
|
||||
)
|
||||
if entry is None:
|
||||
raise ValueError(
|
||||
f"Device {device_id} is not from an existing zwave_js config entry"
|
||||
)
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
raise ValueError(f"Device {device_id} config entry is not loaded")
|
||||
|
||||
client: ZwaveClient = entry.runtime_data[DATA_CLIENT]
|
||||
driver = client.driver
|
||||
|
||||
if driver is None:
|
||||
raise ValueError("Driver is not ready.")
|
||||
|
||||
provisioning_entries = await driver.controller.async_get_provisioning_entries()
|
||||
for provisioning_entry in provisioning_entries:
|
||||
if (
|
||||
provisioning_entry.additional_properties
|
||||
and provisioning_entry.additional_properties.get("device_id") == device_id
|
||||
):
|
||||
return provisioning_entry
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_node_from_entity_id(
|
||||
hass: HomeAssistant,
|
||||
|
@ -3329,7 +3329,7 @@
|
||||
"name": "La Marzocco",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"lametric": {
|
||||
"name": "LaMetric",
|
||||
|
@ -1072,7 +1072,7 @@ class TemplateStateBase(State):
|
||||
raise KeyError
|
||||
|
||||
@under_cached_property
|
||||
def entity_id(self) -> str: # type: ignore[override]
|
||||
def entity_id(self) -> str:
|
||||
"""Wrap State.entity_id.
|
||||
|
||||
Intentionally does not collect state
|
||||
@ -1128,7 +1128,7 @@ class TemplateStateBase(State):
|
||||
return self._state.object_id
|
||||
|
||||
@property
|
||||
def name(self) -> str: # type: ignore[override]
|
||||
def name(self) -> str:
|
||||
"""Wrap State.name."""
|
||||
self._collect_state()
|
||||
return self._state.name
|
||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -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
|
||||
|
@ -10,9 +10,10 @@
|
||||
astroid==3.3.9
|
||||
coverage==7.6.12
|
||||
freezegun==1.5.1
|
||||
go2rtc-client==0.1.2
|
||||
license-expression==30.4.1
|
||||
mock-open==1.4.0
|
||||
mypy-dev==1.16.0a7
|
||||
mypy-dev==1.16.0a8
|
||||
pre-commit==4.0.0
|
||||
pydantic==2.11.3
|
||||
pylint==3.3.6
|
||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -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
|
||||
|
@ -1100,7 +1100,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"weatherkit",
|
||||
"webmin",
|
||||
"wemo",
|
||||
"whirlpool",
|
||||
"whois",
|
||||
"wiffi",
|
||||
"wilight",
|
||||
|
@ -181,5 +181,29 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Hood": {
|
||||
"data": {
|
||||
"programs": [
|
||||
{
|
||||
"key": "Cooking.Common.Program.Hood.Automatic",
|
||||
"constraints": {
|
||||
"execution": "selectandstart"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Cooking.Common.Program.Hood.Venting",
|
||||
"constraints": {
|
||||
"execution": "selectandstart"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Cooking.Common.Program.Hood.DelayedShutOff",
|
||||
"constraints": {
|
||||
"execution": "selectandstart"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,9 @@
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000004',
|
||||
'name': 'Hood',
|
||||
'programs': list([
|
||||
'Cooking.Common.Program.Hood.Automatic',
|
||||
'Cooking.Common.Program.Hood.Venting',
|
||||
'Cooking.Common.Program.Hood.DelayedShutOff',
|
||||
]),
|
||||
'settings': dict({
|
||||
'BSH.Common.Setting.AmbientLightBrightness': 70,
|
||||
|
@ -10,6 +10,7 @@ from aiohomeconnect.model import (
|
||||
EventMessage,
|
||||
EventType,
|
||||
HomeAppliance,
|
||||
StatusKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectApiError
|
||||
import pytest
|
||||
@ -105,9 +106,19 @@ async def test_paired_depaired_devices_flow(
|
||||
assert entity_registry.async_get(entity_entry.entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"Washer",
|
||||
(StatusKey.BSH_COMMON_REMOTE_CONTROL_ACTIVE,),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -138,7 +149,17 @@ async def test_connected_devices(
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{EventKey.BSH_COMMON_APPLIANCE_CONNECTED}",
|
||||
)
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -151,10 +172,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert len(new_entity_entries) > len(entity_entries)
|
||||
for key in (*keys_to_check, EventKey.BSH_COMMON_APPLIANCE_CONNECTED):
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
|
@ -99,9 +99,19 @@ async def test_paired_depaired_devices_flow(
|
||||
assert entity_registry.async_get(entity_entry.entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"Washer",
|
||||
(CommandKey.BSH_COMMON_PAUSE_PROGRAM,),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -116,7 +126,7 @@ async def test_connected_devices(
|
||||
not be obtained while disconnected and once connected, the entities are added.
|
||||
"""
|
||||
get_available_commands_original_mock = client.get_available_commands
|
||||
get_available_programs_mock = client.get_available_programs
|
||||
get_all_programs_mock = client.get_all_programs
|
||||
|
||||
async def get_available_commands_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
@ -125,28 +135,36 @@ async def test_connected_devices(
|
||||
)
|
||||
return await get_available_commands_original_mock.side_effect(ha_id)
|
||||
|
||||
async def get_available_programs_side_effect(ha_id: str):
|
||||
async def get_all_programs_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
raise HomeConnectApiError(
|
||||
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
|
||||
)
|
||||
return await get_available_programs_mock.side_effect(ha_id)
|
||||
return await get_all_programs_mock.side_effect(ha_id)
|
||||
|
||||
client.get_available_commands = AsyncMock(
|
||||
side_effect=get_available_commands_side_effect
|
||||
)
|
||||
client.get_available_programs = AsyncMock(
|
||||
side_effect=get_available_programs_side_effect
|
||||
)
|
||||
client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
client.get_available_commands = get_available_commands_original_mock
|
||||
client.get_available_programs = get_available_programs_mock
|
||||
client.get_all_programs = get_all_programs_mock
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-StopProgram",
|
||||
)
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -159,10 +177,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert len(new_entity_entries) > len(entity_entries)
|
||||
for key in (*keys_to_check, "StopProgram"):
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
|
@ -119,9 +119,19 @@ async def test_paired_depaired_devices_flow(
|
||||
assert entity_registry.async_get(entity_entry.entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Hood"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"Hood",
|
||||
(SettingKey.COOKING_COMMON_LIGHTING,),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -136,7 +146,6 @@ async def test_connected_devices(
|
||||
not be obtained while disconnected and once connected, the entities are added.
|
||||
"""
|
||||
get_settings_original_mock = client.get_settings
|
||||
get_available_programs_mock = client.get_available_programs
|
||||
|
||||
async def get_settings_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
@ -145,26 +154,20 @@ async def test_connected_devices(
|
||||
)
|
||||
return await get_settings_original_mock.side_effect(ha_id)
|
||||
|
||||
async def get_available_programs_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
raise HomeConnectApiError(
|
||||
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
|
||||
)
|
||||
return await get_available_programs_mock.side_effect(ha_id)
|
||||
|
||||
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
|
||||
client.get_available_programs = AsyncMock(
|
||||
side_effect=get_available_programs_side_effect
|
||||
)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
client.get_settings = get_settings_original_mock
|
||||
client.get_available_programs = get_available_programs_mock
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.LIGHT,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -177,10 +180,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert len(new_entity_entries) > len(entity_entries)
|
||||
for key in keys_to_check:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.LIGHT,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Hood"], indirect=True)
|
||||
|
@ -135,9 +135,21 @@ async def test_paired_depaired_devices_flow(
|
||||
assert entity_registry.async_get(entity_entry.entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"FridgeFreezer",
|
||||
(
|
||||
SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
|
||||
),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -168,7 +180,12 @@ async def test_connected_devices(
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.NUMBER,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -181,10 +198,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert len(new_entity_entries) > len(entity_entries)
|
||||
for key in keys_to_check:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.NUMBER,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True)
|
||||
|
@ -20,6 +20,7 @@ from aiohomeconnect.model import (
|
||||
)
|
||||
from aiohomeconnect.model.error import (
|
||||
ActiveProgramNotSetError,
|
||||
HomeConnectApiError,
|
||||
HomeConnectError,
|
||||
SelectedProgramNotSetError,
|
||||
TooManyRequestsError,
|
||||
@ -138,9 +139,23 @@ async def test_paired_depaired_devices_flow(
|
||||
assert entity_registry.async_get(entity_entry.entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"Hood",
|
||||
(
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
SettingKey.COOKING_HOOD_COLOR_TEMPERATURE,
|
||||
),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -154,13 +169,39 @@ async def test_connected_devices(
|
||||
Specifically those devices whose settings, status, etc. could
|
||||
not be obtained while disconnected and once connected, the entities are added.
|
||||
"""
|
||||
get_settings_original_mock = client.get_settings
|
||||
get_all_programs_mock = client.get_all_programs
|
||||
|
||||
async def get_settings_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
raise HomeConnectApiError(
|
||||
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
|
||||
)
|
||||
return await get_settings_original_mock.side_effect(ha_id)
|
||||
|
||||
async def get_all_programs_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
raise HomeConnectApiError(
|
||||
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
|
||||
)
|
||||
return await get_all_programs_mock.side_effect(ha_id)
|
||||
|
||||
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
|
||||
client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
client.get_settings = get_settings_original_mock
|
||||
client.get_all_programs = get_all_programs_mock
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.SELECT,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -173,10 +214,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert entity_entries
|
||||
for key in keys_to_check:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.SELECT,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
|
@ -178,9 +178,19 @@ async def test_paired_depaired_devices_flow(
|
||||
assert hass.states.is_state("sensor.washer_poor_i_dos_1_fill_level", "present")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"Washer",
|
||||
(StatusKey.BSH_COMMON_OPERATION_STATE,),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -211,7 +221,12 @@ async def test_connected_devices(
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -224,10 +239,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert len(new_entity_entries) > len(entity_entries)
|
||||
for key in keys_to_check:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True)
|
||||
|
@ -147,9 +147,23 @@ async def test_paired_depaired_devices_flow(
|
||||
assert entity_registry.async_get(entity_entry.entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"Washer",
|
||||
(
|
||||
SettingKey.BSH_COMMON_POWER_STATE,
|
||||
SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
"Program Cotton",
|
||||
),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -164,7 +178,7 @@ async def test_connected_devices(
|
||||
not be obtained while disconnected and once connected, the entities are added.
|
||||
"""
|
||||
get_settings_original_mock = client.get_settings
|
||||
get_available_programs_mock = client.get_available_programs
|
||||
get_all_programs_mock = client.get_all_programs
|
||||
|
||||
async def get_settings_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
@ -173,26 +187,29 @@ async def test_connected_devices(
|
||||
)
|
||||
return await get_settings_original_mock.side_effect(ha_id)
|
||||
|
||||
async def get_available_programs_side_effect(ha_id: str):
|
||||
async def get_all_programs_side_effect(ha_id: str):
|
||||
if ha_id == appliance.ha_id:
|
||||
raise HomeConnectApiError(
|
||||
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
|
||||
)
|
||||
return await get_available_programs_mock.side_effect(ha_id)
|
||||
return await get_all_programs_mock.side_effect(ha_id)
|
||||
|
||||
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
|
||||
client.get_available_programs = AsyncMock(
|
||||
side_effect=get_available_programs_side_effect
|
||||
)
|
||||
client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
client.get_settings = get_settings_original_mock
|
||||
client.get_available_programs = get_available_programs_mock
|
||||
client.get_all_programs = get_all_programs_mock
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -205,10 +222,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert len(new_entity_entries) > len(entity_entries)
|
||||
for key in keys_to_check:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
|
@ -113,9 +113,19 @@ async def test_paired_depaired_devices_flow(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("appliance", "keys_to_check"),
|
||||
[
|
||||
(
|
||||
"Oven",
|
||||
(SettingKey.BSH_COMMON_ALARM_CLOCK,),
|
||||
)
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_connected_devices(
|
||||
appliance: HomeAppliance,
|
||||
keys_to_check: tuple,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
@ -146,7 +156,12 @@ async def test_connected_devices(
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
for key in keys_to_check:
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.TIME,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
@ -159,10 +174,12 @@ async def test_connected_devices(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)})
|
||||
assert device
|
||||
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
|
||||
assert len(new_entity_entries) > len(entity_entries)
|
||||
for key in keys_to_check:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
Platform.TIME,
|
||||
DOMAIN,
|
||||
f"{appliance.ha_id}-{key}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
|
@ -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,
|
||||
|
@ -1,37 +1,25 @@
|
||||
"""Lamarzocco session fixtures."""
|
||||
|
||||
from collections.abc import Generator
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import 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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -42,33 +30,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 +51,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 +70,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)
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
377
tests/components/lamarzocco/fixtures/config_gs3.json
Normal file
377
tests/components/lamarzocco/fixtures/config_gs3.json
Normal 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": []
|
||||
}
|
237
tests/components/lamarzocco/fixtures/config_micra.json
Normal file
237
tests/components/lamarzocco/fixtures/config_micra.json
Normal 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": []
|
||||
}
|
@ -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": []
|
||||
}
|
||||
|
61
tests/components/lamarzocco/fixtures/schedule.json
Normal file
61
tests/components/lamarzocco/fixtures/schedule.json
Normal 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
|
||||
}
|
50
tests/components/lamarzocco/fixtures/settings.json
Normal file
50
tests/components/lamarzocco/fixtures/settings.json
Normal 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": "What’s 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": "What’s 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
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
16
tests/components/lamarzocco/fixtures/thing.json
Normal file
16
tests/components/lamarzocco/fixtures/thing.json
Normal 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
|
||||
}
|
@ -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,
|
||||
})
|
||||
# ---
|
||||
|
@ -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': '''
|
||||
What’s 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': '''
|
||||
What’s 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': '''
|
||||
What’s 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': '''
|
||||
What’s 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',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
@ -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
@ -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({
|
||||
}),
|
||||
|
@ -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',
|
||||
})
|
||||
# ---
|
@ -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',
|
||||
})
|
||||
# ---
|
||||
|
@ -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
|
||||
@ -21,7 +18,6 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat
|
||||
|
||||
async def test_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
@ -35,26 +31,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 +63,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 +71,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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
@ -34,9 +27,18 @@ from . import USER_INPUT, async_init_integration, get_bluetooth_service_info
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
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,40 +52,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()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{CONF_MACHINE: "GS012345"},
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert result3["title"] == "GS3"
|
||||
assert result3["data"] == {
|
||||
assert result["title"] == "GS012345"
|
||||
assert result["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 result["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."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -93,14 +83,12 @@ async def test_form(
|
||||
assert result["errors"] == {}
|
||||
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)
|
||||
result = await __do_successful_user_step(hass, result, mock_cloud_client)
|
||||
await __do_sucessful_machine_selection_step(hass, result)
|
||||
|
||||
|
||||
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."""
|
||||
@ -112,139 +100,89 @@ async def test_form_abort_already_configured(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = 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"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "machine_selection"
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_MACHINE: mock_device_info.serial_number,
|
||||
CONF_MACHINE: "GS012345",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.ABORT
|
||||
assert result3["reason"] == "already_configured"
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["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}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1
|
||||
|
||||
# test recovery from failure
|
||||
mock_cloud_client.get_customer_fleet.side_effect = None
|
||||
result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
|
||||
await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
|
||||
|
||||
|
||||
async def test_form_invalid_host(
|
||||
hass: HomeAssistant,
|
||||
mock_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"] == {}
|
||||
|
||||
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
|
||||
assert result["errors"] == {"base": error}
|
||||
assert len(mock_cloud_client.list_things.mock_calls) == 1
|
||||
|
||||
# test recovery from failure
|
||||
await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
|
||||
mock_cloud_client.list_things.side_effect = None
|
||||
result = await __do_successful_user_step(hass, result, mock_cloud_client)
|
||||
await __do_sucessful_machine_selection_step(hass, result)
|
||||
|
||||
|
||||
async def test_form_cannot_connect(
|
||||
async def test_form_no_machines(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_client: MagicMock,
|
||||
mock_device_info: LaMarzoccoDeviceInfo,
|
||||
mock_setup_entry: Generator[AsyncMock],
|
||||
) -> None:
|
||||
"""Test cannot connect error."""
|
||||
"""Test we don't have any devices."""
|
||||
|
||||
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}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
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 result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_machines"}
|
||||
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
|
||||
}
|
||||
result2 = await __do_successful_user_step(hass, result, mock_cloud_client)
|
||||
await __do_sucessful_machine_selection_step(hass, result2, mock_device_info)
|
||||
mock_cloud_client.list_things.return_value = original_return
|
||||
|
||||
result = await __do_successful_user_step(hass, result, mock_cloud_client)
|
||||
await __do_sucessful_machine_selection_step(hass, result)
|
||||
|
||||
|
||||
async def test_reauth_flow(
|
||||
@ -261,15 +199,15 @@ async def test_reauth_flow(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "new_password"},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result["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 result["reason"] == "reauth_successful"
|
||||
assert len(mock_cloud_client.list_things.mock_calls) == 1
|
||||
assert mock_config_entry.data[CONF_PASSWORD] == "new_password"
|
||||
|
||||
|
||||
@ -277,8 +215,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."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
@ -288,40 +224,33 @@ async def test_reconfigure_flow(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
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
|
||||
)
|
||||
result = await __do_successful_user_step(hass, result, mock_cloud_client)
|
||||
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],
|
||||
),
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_MACHINE: mock_device_info.serial_number,
|
||||
CONF_MACHINE: "GS012345",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["step_id"] == "bluetooth_selection"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "bluetooth_selection"
|
||||
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_MAC: service_info.address},
|
||||
)
|
||||
|
||||
assert result4["type"] is FlowResultType.ABORT
|
||||
assert result4["reason"] == "reconfigure_successful"
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
assert mock_config_entry.title == "My LaMarzocco"
|
||||
assert mock_config_entry.data == {
|
||||
@ -334,12 +263,13 @@ async def test_bluetooth_discovery(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_cloud_client: MagicMock,
|
||||
mock_setup_entry: Generator[AsyncMock],
|
||||
) -> 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
|
||||
)
|
||||
@ -347,52 +277,30 @@ async def test_bluetooth_discovery(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = 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 result["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 result["title"] == "GS012345"
|
||||
assert result["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",
|
||||
}
|
||||
|
||||
|
||||
async def test_bluetooth_discovery_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_cloud_client: MagicMock,
|
||||
mock_setup_entry: Generator[AsyncMock],
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test bluetooth discovery."""
|
||||
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 +313,10 @@ 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,62 +327,36 @@ 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", ""}
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
original_return = deepcopy(mock_cloud_client.list_things.return_value)
|
||||
mock_cloud_client.list_things.return_value[0].serial_number = "GS98765"
|
||||
|
||||
result = 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 result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "machine_not_found"}
|
||||
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
|
||||
}
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
mock_cloud_client.list_things.return_value = original_return
|
||||
result = 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 result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert result3["title"] == "GS3"
|
||||
assert result3["data"] == {
|
||||
assert result["title"] == "GS012345"
|
||||
assert result["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,30 +373,20 @@ 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,
|
||||
}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
**USER_INPUT,
|
||||
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||
CONF_TOKEN: None,
|
||||
}
|
||||
|
||||
|
||||
async def test_dhcp_discovery_abort_on_hostname_changed(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_cloud_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test dhcp discovery aborts when hostname was changed manually."""
|
||||
@ -537,11 +407,9 @@ async def test_dhcp_discovery_abort_on_hostname_changed(
|
||||
async def test_dhcp_already_configured_and_update(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_cloud_client: MagicMock,
|
||||
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,18 +425,13 @@ 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"
|
||||
|
||||
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_setup_entry: Generator[AsyncMock],
|
||||
) -> None:
|
||||
"""Test options flow."""
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
@ -579,7 +442,7 @@ async def test_options_flow(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_USE_BLUETOOTH: False,
|
||||
@ -587,7 +450,7 @@ async def test_options_flow(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["data"] == {
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_USE_BLUETOOTH: False,
|
||||
}
|
||||
|
@ -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,13 +29,12 @@ 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(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lamarzocco: MagicMock,
|
||||
) -> None:
|
||||
"""Test loading and unloading the integration."""
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
@ -54,25 +53,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 +108,52 @@ 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_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 +165,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 +196,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 +213,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 +230,37 @@ 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 +269,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 +308,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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
@ -24,7 +25,6 @@ from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
async def test_switches(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
@ -47,7 +47,7 @@ async def test_switches(
|
||||
(
|
||||
"_smart_standby_enabled",
|
||||
"set_smart_standby",
|
||||
{"mode": "LastBrewing", "minutes": 10},
|
||||
{"mode": SmartStandByType.POWER_ON, "minutes": 10},
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -124,12 +124,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 +143,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 +186,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,
|
||||
|
@ -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
|
||||
@ -20,7 +19,6 @@ from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
async def test_update(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
@ -31,19 +29,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 +44,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,
|
||||
)
|
||||
|
@ -86,7 +86,7 @@ async def test_update_entity(
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test update entity exists and update check got made."""
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
@ -101,7 +101,7 @@ async def test_update_check_service(
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test check device update through service call."""
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "v1.0"
|
||||
@ -124,14 +124,14 @@ async def test_update_check_service(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light",
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light_firmware",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.check_node_update.call_count == 2
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("latest_version") == "v2.0"
|
||||
@ -150,7 +150,7 @@ async def test_update_install(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test device update with Matter attribute changes influence progress."""
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "v1.0"
|
||||
@ -173,7 +173,7 @@ async def test_update_install(
|
||||
|
||||
assert matter_client.check_node_update.call_count == 2
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("latest_version") == "v2.0"
|
||||
@ -186,7 +186,7 @@ async def test_update_install(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light",
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light_firmware",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
@ -199,7 +199,7 @@ async def test_update_install(
|
||||
)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes["in_progress"] is True
|
||||
@ -213,7 +213,7 @@ async def test_update_install(
|
||||
)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes["in_progress"] is True
|
||||
@ -239,7 +239,7 @@ async def test_update_install(
|
||||
)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "v2.0"
|
||||
|
||||
@ -254,7 +254,7 @@ async def test_update_install_failure(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test update entity service call errors."""
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "v1.0"
|
||||
@ -277,7 +277,7 @@ async def test_update_install_failure(
|
||||
|
||||
assert matter_client.check_node_update.call_count == 2
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("latest_version") == "v2.0"
|
||||
@ -293,7 +293,7 @@ async def test_update_install_failure(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light",
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light_firmware",
|
||||
ATTR_VERSION: "v3.0",
|
||||
},
|
||||
blocking=True,
|
||||
@ -306,7 +306,7 @@ async def test_update_install_failure(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light",
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light_firmware",
|
||||
ATTR_VERSION: "v3.0",
|
||||
},
|
||||
blocking=True,
|
||||
@ -323,7 +323,7 @@ async def test_update_state_save_and_restore(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test latest update information is retained across reload/restart."""
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "v1.0"
|
||||
@ -336,7 +336,7 @@ async def test_update_state_save_and_restore(
|
||||
|
||||
assert matter_client.check_node_update.call_count == 2
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("latest_version") == "v2.0"
|
||||
@ -345,7 +345,7 @@ async def test_update_state_save_and_restore(
|
||||
|
||||
assert len(hass_storage[RESTORE_STATE_KEY]["data"]) == 1
|
||||
state = hass_storage[RESTORE_STATE_KEY]["data"][0]["state"]
|
||||
assert state["entity_id"] == "update.mock_dimmable_light"
|
||||
assert state["entity_id"] == "update.mock_dimmable_light_firmware"
|
||||
extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"]
|
||||
|
||||
# Check that the extra data has the format we expect.
|
||||
@ -376,7 +376,7 @@ async def test_update_state_restore(
|
||||
(
|
||||
(
|
||||
State(
|
||||
"update.mock_dimmable_light",
|
||||
"update.mock_dimmable_light_firmware",
|
||||
STATE_ON,
|
||||
{
|
||||
"auto_update": False,
|
||||
@ -393,7 +393,7 @@ async def test_update_state_restore(
|
||||
|
||||
assert check_node_update.call_count == 0
|
||||
|
||||
state = hass.states.get("update.mock_dimmable_light")
|
||||
state = hass.states.get("update.mock_dimmable_light_firmware")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("latest_version") == "v2.0"
|
||||
@ -402,7 +402,7 @@ async def test_update_state_restore(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light",
|
||||
ATTR_ENTITY_ID: "update.mock_dimmable_light_firmware",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import CLIENT_ID, CLIENT_SECRET
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture
|
||||
|
||||
|
||||
@pytest.fixture(name="expires_at")
|
||||
@ -91,10 +91,23 @@ def action_fixture(load_action_file: str) -> MieleAction:
|
||||
return load_json_object_fixture(load_action_file, DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def load_programs_file() -> str:
|
||||
"""Fixture for loading programs file."""
|
||||
return "programs_washing_machine.json"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def programs_fixture(load_programs_file: str) -> list[dict]:
|
||||
"""Fixture for available programs."""
|
||||
return load_fixture(load_programs_file, DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_miele_client(
|
||||
device_fixture,
|
||||
action_fixture,
|
||||
programs_fixture,
|
||||
) -> Generator[MagicMock]:
|
||||
"""Mock a Miele client."""
|
||||
|
||||
@ -106,6 +119,7 @@ def mock_miele_client(
|
||||
|
||||
client.get_devices.return_value = device_fixture
|
||||
client.get_actions.return_value = action_fixture
|
||||
client.get_programs.return_value = programs_fixture
|
||||
|
||||
yield client
|
||||
|
||||
|
@ -352,7 +352,18 @@
|
||||
"key_localized": "Fan level"
|
||||
},
|
||||
"plateStep": [],
|
||||
"ecoFeedback": null,
|
||||
"ecoFeedback": {
|
||||
"currentWaterConsumption": {
|
||||
"unit": "l",
|
||||
"value": 0.0
|
||||
},
|
||||
"currentEnergyConsumption": {
|
||||
"unit": "kWh",
|
||||
"value": 0.0
|
||||
},
|
||||
"waterForecast": 0.0,
|
||||
"energyForecast": 0.1
|
||||
},
|
||||
"batteryLevel": null
|
||||
}
|
||||
}
|
||||
|
117
tests/components/miele/fixtures/programs_washing_machine.json
Normal file
117
tests/components/miele/fixtures/programs_washing_machine.json
Normal file
@ -0,0 +1,117 @@
|
||||
[
|
||||
{
|
||||
"programId": 146,
|
||||
"program": "QuickPowerWash",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 123,
|
||||
"program": "Dark garments / Denim",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 190,
|
||||
"program": "ECO 40-60 ",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 27,
|
||||
"program": "Proofing",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 23,
|
||||
"program": "Shirts",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 9,
|
||||
"program": "Silks ",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 8,
|
||||
"program": "Woollens ",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 4,
|
||||
"program": "Delicates",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 3,
|
||||
"program": "Minimum iron",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 1,
|
||||
"program": "Cottons",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 69,
|
||||
"program": "Cottons hygiene",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 37,
|
||||
"program": "Outerwear",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 122,
|
||||
"program": "Express 20",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 29,
|
||||
"program": "Sportswear",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 31,
|
||||
"program": "Automatic plus",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 39,
|
||||
"program": "Pillows",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 22,
|
||||
"program": "Curtains",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 129,
|
||||
"program": "Down filled items",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 53,
|
||||
"program": "First wash",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 95,
|
||||
"program": "Down duvets",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 52,
|
||||
"program": "Separate rinse / Starch",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 21,
|
||||
"program": "Drain / Spin",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 91,
|
||||
"program": "Clean machine",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
670
tests/components/miele/snapshots/test_diagnostics.ambr
Normal file
670
tests/components/miele/snapshots/test_diagnostics.ambr
Normal file
@ -0,0 +1,670 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics_config_entry
|
||||
dict({
|
||||
'config_entry_data': dict({
|
||||
'auth_implementation': 'miele',
|
||||
'token': dict({
|
||||
'access_token': '**REDACTED**',
|
||||
'expires_in': 86399,
|
||||
'refresh_token': '**REDACTED**',
|
||||
'token_type': 'Bearer',
|
||||
}),
|
||||
}),
|
||||
'miele_data': dict({
|
||||
'actions': dict({
|
||||
'**REDACTED_019aa577ad1c330d': dict({
|
||||
'ambientLight': list([
|
||||
]),
|
||||
'colors': list([
|
||||
]),
|
||||
'deviceName': True,
|
||||
'light': list([
|
||||
]),
|
||||
'modes': list([
|
||||
]),
|
||||
'powerOff': False,
|
||||
'powerOn': True,
|
||||
'processAction': list([
|
||||
]),
|
||||
'programId': list([
|
||||
]),
|
||||
'runOnTime': list([
|
||||
]),
|
||||
'startTime': list([
|
||||
]),
|
||||
'targetTemperature': list([
|
||||
]),
|
||||
'ventilationStep': list([
|
||||
]),
|
||||
}),
|
||||
'**REDACTED_57d53e72806e88b4': dict({
|
||||
'ambientLight': list([
|
||||
]),
|
||||
'colors': list([
|
||||
]),
|
||||
'deviceName': True,
|
||||
'light': list([
|
||||
]),
|
||||
'modes': list([
|
||||
]),
|
||||
'powerOff': False,
|
||||
'powerOn': True,
|
||||
'processAction': list([
|
||||
]),
|
||||
'programId': list([
|
||||
]),
|
||||
'runOnTime': list([
|
||||
]),
|
||||
'startTime': list([
|
||||
]),
|
||||
'targetTemperature': list([
|
||||
]),
|
||||
'ventilationStep': list([
|
||||
]),
|
||||
}),
|
||||
'**REDACTED_c9fe55cdf70786ca': dict({
|
||||
'ambientLight': list([
|
||||
]),
|
||||
'colors': list([
|
||||
]),
|
||||
'deviceName': True,
|
||||
'light': list([
|
||||
]),
|
||||
'modes': list([
|
||||
]),
|
||||
'powerOff': False,
|
||||
'powerOn': True,
|
||||
'processAction': list([
|
||||
]),
|
||||
'programId': list([
|
||||
]),
|
||||
'runOnTime': list([
|
||||
]),
|
||||
'startTime': list([
|
||||
]),
|
||||
'targetTemperature': list([
|
||||
]),
|
||||
'ventilationStep': list([
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
'devices': dict({
|
||||
'**REDACTED_019aa577ad1c330d': dict({
|
||||
'ident': dict({
|
||||
'deviceIdentLabel': dict({
|
||||
'fabIndex': '17',
|
||||
'fabNumber': '**REDACTED**',
|
||||
'matNumber': '10804770',
|
||||
'swids': list([
|
||||
'4497',
|
||||
]),
|
||||
'techType': 'KS 28423 D ed/c',
|
||||
}),
|
||||
'deviceName': '',
|
||||
'protocolVersion': 201,
|
||||
'type': dict({
|
||||
'key_localized': 'Device type',
|
||||
'value_localized': 'Refrigerator',
|
||||
'value_raw': 19,
|
||||
}),
|
||||
'xkmIdentLabel': dict({
|
||||
'releaseVersion': '31.17',
|
||||
'techType': 'EK042',
|
||||
}),
|
||||
}),
|
||||
'state': dict({
|
||||
'ProgramID': dict({
|
||||
'key_localized': 'Program name',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'ambientLight': None,
|
||||
'batteryLevel': None,
|
||||
'coreTargetTemperature': list([
|
||||
]),
|
||||
'coreTemperature': list([
|
||||
]),
|
||||
'dryingStep': dict({
|
||||
'key_localized': 'Drying level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
'ecoFeedback': None,
|
||||
'elapsedTime': list([
|
||||
]),
|
||||
'light': None,
|
||||
'plateStep': list([
|
||||
]),
|
||||
'programPhase': dict({
|
||||
'key_localized': 'Program phase',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'programType': dict({
|
||||
'key_localized': 'Program type',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'remainingTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'remoteEnable': dict({
|
||||
'fullRemoteControl': True,
|
||||
'mobileStart': False,
|
||||
'smartGrid': False,
|
||||
}),
|
||||
'signalDoor': False,
|
||||
'signalFailure': False,
|
||||
'signalInfo': False,
|
||||
'spinningSpeed': dict({
|
||||
'key_localized': 'Spin speed',
|
||||
'unit': 'rpm',
|
||||
'value_localized': None,
|
||||
'value_raw': None,
|
||||
}),
|
||||
'startTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'status': dict({
|
||||
'key_localized': 'status',
|
||||
'value_localized': 'In use',
|
||||
'value_raw': 5,
|
||||
}),
|
||||
'targetTemperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': 4,
|
||||
'value_raw': 400,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'temperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': 4,
|
||||
'value_raw': 400,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'ventilationStep': dict({
|
||||
'key_localized': 'Fan level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'**REDACTED_57d53e72806e88b4': dict({
|
||||
'ident': dict({
|
||||
'deviceIdentLabel': dict({
|
||||
'fabIndex': '21',
|
||||
'fabNumber': '**REDACTED**',
|
||||
'matNumber': '10805070',
|
||||
'swids': list([
|
||||
'4497',
|
||||
]),
|
||||
'techType': 'FNS 28463 E ed/',
|
||||
}),
|
||||
'deviceName': '',
|
||||
'protocolVersion': 201,
|
||||
'type': dict({
|
||||
'key_localized': 'Device type',
|
||||
'value_localized': 'Freezer',
|
||||
'value_raw': 20,
|
||||
}),
|
||||
'xkmIdentLabel': dict({
|
||||
'releaseVersion': '31.17',
|
||||
'techType': 'EK042',
|
||||
}),
|
||||
}),
|
||||
'state': dict({
|
||||
'ProgramID': dict({
|
||||
'key_localized': 'Program name',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'ambientLight': None,
|
||||
'batteryLevel': None,
|
||||
'coreTargetTemperature': list([
|
||||
]),
|
||||
'coreTemperature': list([
|
||||
]),
|
||||
'dryingStep': dict({
|
||||
'key_localized': 'Drying level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
'ecoFeedback': None,
|
||||
'elapsedTime': list([
|
||||
]),
|
||||
'light': None,
|
||||
'plateStep': list([
|
||||
]),
|
||||
'programPhase': dict({
|
||||
'key_localized': 'Program phase',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'programType': dict({
|
||||
'key_localized': 'Program type',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'remainingTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'remoteEnable': dict({
|
||||
'fullRemoteControl': True,
|
||||
'mobileStart': False,
|
||||
'smartGrid': False,
|
||||
}),
|
||||
'signalDoor': False,
|
||||
'signalFailure': False,
|
||||
'signalInfo': False,
|
||||
'spinningSpeed': dict({
|
||||
'key_localized': 'Spin speed',
|
||||
'unit': 'rpm',
|
||||
'value_localized': None,
|
||||
'value_raw': None,
|
||||
}),
|
||||
'startTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'status': dict({
|
||||
'key_localized': 'status',
|
||||
'value_localized': 'In use',
|
||||
'value_raw': 5,
|
||||
}),
|
||||
'targetTemperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': -18,
|
||||
'value_raw': -1800,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'temperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': -18,
|
||||
'value_raw': -1800,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'ventilationStep': dict({
|
||||
'key_localized': 'Fan level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'**REDACTED_c9fe55cdf70786ca': dict({
|
||||
'ident': dict({
|
||||
'deviceIdentLabel': dict({
|
||||
'fabIndex': '44',
|
||||
'fabNumber': '**REDACTED**',
|
||||
'matNumber': '11387290',
|
||||
'swids': list([
|
||||
'5975',
|
||||
'20456',
|
||||
'25213',
|
||||
'25191',
|
||||
'25446',
|
||||
'25205',
|
||||
'25447',
|
||||
'25319',
|
||||
]),
|
||||
'techType': 'WCI870',
|
||||
}),
|
||||
'deviceName': '',
|
||||
'protocolVersion': 4,
|
||||
'type': dict({
|
||||
'key_localized': 'Device type',
|
||||
'value_localized': 'Washing machine',
|
||||
'value_raw': 1,
|
||||
}),
|
||||
'xkmIdentLabel': dict({
|
||||
'releaseVersion': '08.32',
|
||||
'techType': 'EK057',
|
||||
}),
|
||||
}),
|
||||
'state': dict({
|
||||
'ProgramID': dict({
|
||||
'key_localized': 'Program name',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'ambientLight': None,
|
||||
'batteryLevel': None,
|
||||
'coreTargetTemperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'coreTemperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'dryingStep': dict({
|
||||
'key_localized': 'Drying level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
'ecoFeedback': dict({
|
||||
'currentEnergyConsumption': dict({
|
||||
'unit': 'kWh',
|
||||
'value': 0.0,
|
||||
}),
|
||||
'currentWaterConsumption': dict({
|
||||
'unit': 'l',
|
||||
'value': 0.0,
|
||||
}),
|
||||
'energyForecast': 0.1,
|
||||
'waterForecast': 0.0,
|
||||
}),
|
||||
'elapsedTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'light': None,
|
||||
'plateStep': list([
|
||||
]),
|
||||
'programPhase': dict({
|
||||
'key_localized': 'Program phase',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'programType': dict({
|
||||
'key_localized': 'Program type',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'remainingTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'remoteEnable': dict({
|
||||
'fullRemoteControl': True,
|
||||
'mobileStart': False,
|
||||
'smartGrid': False,
|
||||
}),
|
||||
'signalDoor': True,
|
||||
'signalFailure': False,
|
||||
'signalInfo': False,
|
||||
'spinningSpeed': dict({
|
||||
'key_localized': 'Spin speed',
|
||||
'unit': 'rpm',
|
||||
'value_localized': None,
|
||||
'value_raw': None,
|
||||
}),
|
||||
'startTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'status': dict({
|
||||
'key_localized': 'status',
|
||||
'value_localized': 'Off',
|
||||
'value_raw': 1,
|
||||
}),
|
||||
'targetTemperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'temperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'ventilationStep': dict({
|
||||
'key_localized': 'Fan level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_diagnostics_device
|
||||
dict({
|
||||
'data': dict({
|
||||
'auth_implementation': 'miele',
|
||||
'token': dict({
|
||||
'access_token': '**REDACTED**',
|
||||
'expires_in': 86399,
|
||||
'refresh_token': '**REDACTED**',
|
||||
'token_type': 'Bearer',
|
||||
}),
|
||||
}),
|
||||
'info': dict({
|
||||
'manufacturer': 'Miele',
|
||||
'model': 'FNS 28463 E ed/',
|
||||
}),
|
||||
'miele_data': dict({
|
||||
'actions': dict({
|
||||
'**REDACTED_57d53e72806e88b4': dict({
|
||||
'ambientLight': list([
|
||||
]),
|
||||
'colors': list([
|
||||
]),
|
||||
'deviceName': True,
|
||||
'light': list([
|
||||
]),
|
||||
'modes': list([
|
||||
]),
|
||||
'powerOff': False,
|
||||
'powerOn': True,
|
||||
'processAction': list([
|
||||
]),
|
||||
'programId': list([
|
||||
]),
|
||||
'runOnTime': list([
|
||||
]),
|
||||
'startTime': list([
|
||||
]),
|
||||
'targetTemperature': list([
|
||||
]),
|
||||
'ventilationStep': list([
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
'devices': dict({
|
||||
'**REDACTED_57d53e72806e88b4': dict({
|
||||
'ident': dict({
|
||||
'deviceIdentLabel': dict({
|
||||
'fabIndex': '21',
|
||||
'fabNumber': '**REDACTED**',
|
||||
'matNumber': '10805070',
|
||||
'swids': list([
|
||||
'4497',
|
||||
]),
|
||||
'techType': 'FNS 28463 E ed/',
|
||||
}),
|
||||
'deviceName': '',
|
||||
'protocolVersion': 201,
|
||||
'type': dict({
|
||||
'key_localized': 'Device type',
|
||||
'value_localized': 'Freezer',
|
||||
'value_raw': 20,
|
||||
}),
|
||||
'xkmIdentLabel': dict({
|
||||
'releaseVersion': '31.17',
|
||||
'techType': 'EK042',
|
||||
}),
|
||||
}),
|
||||
'state': dict({
|
||||
'ProgramID': dict({
|
||||
'key_localized': 'Program name',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'ambientLight': None,
|
||||
'batteryLevel': None,
|
||||
'coreTargetTemperature': list([
|
||||
]),
|
||||
'coreTemperature': list([
|
||||
]),
|
||||
'dryingStep': dict({
|
||||
'key_localized': 'Drying level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
'ecoFeedback': None,
|
||||
'elapsedTime': list([
|
||||
]),
|
||||
'light': None,
|
||||
'plateStep': list([
|
||||
]),
|
||||
'programPhase': dict({
|
||||
'key_localized': 'Program phase',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'programType': dict({
|
||||
'key_localized': 'Program type',
|
||||
'value_localized': '',
|
||||
'value_raw': 0,
|
||||
}),
|
||||
'remainingTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'remoteEnable': dict({
|
||||
'fullRemoteControl': True,
|
||||
'mobileStart': False,
|
||||
'smartGrid': False,
|
||||
}),
|
||||
'signalDoor': False,
|
||||
'signalFailure': False,
|
||||
'signalInfo': False,
|
||||
'spinningSpeed': dict({
|
||||
'key_localized': 'Spin speed',
|
||||
'unit': 'rpm',
|
||||
'value_localized': None,
|
||||
'value_raw': None,
|
||||
}),
|
||||
'startTime': list([
|
||||
0,
|
||||
0,
|
||||
]),
|
||||
'status': dict({
|
||||
'key_localized': 'status',
|
||||
'value_localized': 'In use',
|
||||
'value_raw': 5,
|
||||
}),
|
||||
'targetTemperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': -18,
|
||||
'value_raw': -1800,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'temperature': list([
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': -18,
|
||||
'value_raw': -1800,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
dict({
|
||||
'unit': 'Celsius',
|
||||
'value_localized': None,
|
||||
'value_raw': -32768,
|
||||
}),
|
||||
]),
|
||||
'ventilationStep': dict({
|
||||
'key_localized': 'Fan level',
|
||||
'value_localized': '',
|
||||
'value_raw': None,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'programs': 'Not implemented',
|
||||
}),
|
||||
})
|
||||
# ---
|
69
tests/components/miele/test_diagnostics.py
Normal file
69
tests/components/miele/test_diagnostics.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""Tests for the diagnostics data provided by the miele integration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from syrupy import SnapshotAssertion
|
||||
from syrupy.filters import paths
|
||||
|
||||
from homeassistant.components.miele.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import (
|
||||
get_diagnostics_for_config_entry,
|
||||
get_diagnostics_for_device,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics_config_entry(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_miele_client: Generator[MagicMock],
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics for config entry."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
result = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
assert result == snapshot(
|
||||
exclude=paths(
|
||||
"config_entry_data.token.expires_at",
|
||||
"miele_test.entry_id",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def test_diagnostics_device(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_miele_client: Generator[MagicMock],
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics for device."""
|
||||
|
||||
TEST_DEVICE = "Dummy_Appliance_1"
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, TEST_DEVICE)})
|
||||
assert device_entry is not None
|
||||
|
||||
result = await get_diagnostics_for_device(
|
||||
hass, hass_client, mock_config_entry, device_entry
|
||||
)
|
||||
assert result == snapshot(
|
||||
exclude=paths(
|
||||
"data.token.expires_at",
|
||||
"miele_test.entry_id",
|
||||
)
|
||||
)
|
@ -65,21 +65,21 @@ async def test_async_browse_media_root(
|
||||
assert response["success"]
|
||||
result = response["result"]
|
||||
for idx, item in enumerate(result["children"]):
|
||||
assert item["title"] == LIBRARY[idx]
|
||||
assert item["title"].lower() == LIBRARY[idx]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("category", "child_count"),
|
||||
[
|
||||
("Favorites", 4),
|
||||
("Artists", 4),
|
||||
("Albums", 4),
|
||||
("Playlists", 4),
|
||||
("Genres", 4),
|
||||
("New Music", 4),
|
||||
("Album Artists", 4),
|
||||
("Apps", 3),
|
||||
("Radios", 3),
|
||||
("favorites", 4),
|
||||
("artists", 4),
|
||||
("albums", 4),
|
||||
("playlists", 4),
|
||||
("genres", 4),
|
||||
("new music", 4),
|
||||
("album artists", 4),
|
||||
("apps", 3),
|
||||
("radios", 3),
|
||||
],
|
||||
)
|
||||
async def test_async_browse_media_with_subitems(
|
||||
|
@ -5,10 +5,12 @@ from unittest.mock import MagicMock, patch
|
||||
import aiohttp
|
||||
import pytest
|
||||
from whirlpool.auth import AccountLockedError
|
||||
from whirlpool.backendselector import Brand, Region
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@ -20,6 +22,42 @@ CONFIG_INPUT = {
|
||||
}
|
||||
|
||||
|
||||
def assert_successful_user_flow(
|
||||
mock_whirlpool_setup_entry: MagicMock,
|
||||
result: ConfigFlowResult,
|
||||
region: str,
|
||||
brand: str,
|
||||
) -> None:
|
||||
"""Assert that the flow was successful."""
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-username"
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: CONFIG_INPUT[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONFIG_INPUT[CONF_PASSWORD],
|
||||
CONF_REGION: region,
|
||||
CONF_BRAND: brand,
|
||||
}
|
||||
assert result["result"].unique_id == CONFIG_INPUT[CONF_USERNAME]
|
||||
assert len(mock_whirlpool_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
def assert_successful_reauth_flow(
|
||||
mock_entry: MockConfigEntry,
|
||||
result: ConfigFlowResult,
|
||||
region: str,
|
||||
brand: str,
|
||||
) -> None:
|
||||
"""Assert that the reauth flow was successful."""
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_entry.data == {
|
||||
CONF_USERNAME: CONFIG_INPUT[CONF_USERNAME],
|
||||
CONF_PASSWORD: "new-password",
|
||||
CONF_REGION: region[0],
|
||||
CONF_BRAND: brand[0],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_whirlpool_setup_entry")
|
||||
def fixture_mock_whirlpool_setup_entry():
|
||||
"""Set up async_setup_entry fixture."""
|
||||
@ -30,14 +68,14 @@ def fixture_mock_whirlpool_setup_entry():
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_auth_api", "mock_appliances_manager_api")
|
||||
async def test_form(
|
||||
async def test_user_flow(
|
||||
hass: HomeAssistant,
|
||||
region,
|
||||
brand,
|
||||
region: tuple[str, Region],
|
||||
brand: tuple[str, Brand],
|
||||
mock_backend_selector_api: MagicMock,
|
||||
mock_whirlpool_setup_entry: MagicMock,
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
"""Test successful flow initialized by the user."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@ -45,38 +83,39 @@ async def test_form(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == config_entries.SOURCE_USER
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]}
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "test-username"
|
||||
assert result2["data"] == {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"region": region[0],
|
||||
"brand": brand[0],
|
||||
}
|
||||
assert len(mock_whirlpool_setup_entry.mock_calls) == 1
|
||||
assert_successful_user_flow(mock_whirlpool_setup_entry, result, region[0], brand[0])
|
||||
mock_backend_selector_api.assert_called_once_with(brand[1], region[1])
|
||||
|
||||
|
||||
async def test_form_invalid_auth(
|
||||
hass: HomeAssistant, region, brand, mock_auth_api: MagicMock
|
||||
async def test_user_flow_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
region: tuple[str, Region],
|
||||
brand: tuple[str, Brand],
|
||||
mock_auth_api: MagicMock,
|
||||
mock_whirlpool_setup_entry: MagicMock,
|
||||
) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
"""Test invalid authentication in the flow initialized by the user."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_auth_api.return_value.is_access_token_valid.return_value = False
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]}
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
# Test that it succeeds if the authentication is valid
|
||||
mock_auth_api.return_value.is_access_token_valid.return_value = True
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]}
|
||||
)
|
||||
assert_successful_user_flow(mock_whirlpool_setup_entry, result, region[0], brand[0])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_appliances_manager_api")
|
||||
@ -89,16 +128,16 @@ async def test_form_invalid_auth(
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_auth_error(
|
||||
async def test_user_flow_auth_error(
|
||||
hass: HomeAssistant,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
region,
|
||||
brand,
|
||||
region: tuple[str, Region],
|
||||
brand: tuple[str, Brand],
|
||||
mock_auth_api: MagicMock,
|
||||
mock_whirlpool_setup_entry: MagicMock,
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
"""Test authentication exceptions in the flow initialized by the user."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@ -108,8 +147,8 @@ async def test_form_auth_error(
|
||||
result["flow_id"],
|
||||
CONFIG_INPUT
|
||||
| {
|
||||
"region": region[0],
|
||||
"brand": brand[0],
|
||||
CONF_REGION: region[0],
|
||||
CONF_BRAND: brand[0],
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@ -118,27 +157,20 @@ async def test_form_auth_error(
|
||||
# Test that it succeeds after the error is cleared
|
||||
mock_auth_api.return_value.do_auth.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
result["flow_id"], CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-username"
|
||||
assert result["data"] == {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"region": region[0],
|
||||
"brand": brand[0],
|
||||
}
|
||||
assert len(mock_whirlpool_setup_entry.mock_calls) == 1
|
||||
assert_successful_user_flow(mock_whirlpool_setup_entry, result, region[0], brand[0])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_auth_api", "mock_appliances_manager_api")
|
||||
async def test_form_already_configured(hass: HomeAssistant, region, brand) -> None:
|
||||
async def test_already_configured(
|
||||
hass: HomeAssistant, region: tuple[str, Region], brand: tuple[str, Brand]
|
||||
) -> None:
|
||||
"""Test that configuring the integration twice with the same data fails."""
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
data=CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]},
|
||||
unique_id="test-username",
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
@ -150,22 +182,20 @@ async def test_form_already_configured(hass: HomeAssistant, region, brand) -> No
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == config_entries.SOURCE_USER
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG_INPUT
|
||||
| {
|
||||
"region": region[0],
|
||||
"brand": brand[0],
|
||||
},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]}
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "already_configured"
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_auth_api")
|
||||
async def test_no_appliances_flow(
|
||||
hass: HomeAssistant, region, brand, mock_appliances_manager_api: MagicMock
|
||||
hass: HomeAssistant,
|
||||
region: tuple[str, Region],
|
||||
brand: tuple[str, Brand],
|
||||
mock_appliances_manager_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test we get an error with no appliances."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -177,23 +207,24 @@ async def test_no_appliances_flow(
|
||||
|
||||
mock_appliances_manager_api.return_value.aircons = []
|
||||
mock_appliances_manager_api.return_value.washer_dryers = []
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]}
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "no_appliances"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_appliances"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_auth_api", "mock_appliances_manager_api", "mock_whirlpool_setup_entry"
|
||||
)
|
||||
async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None:
|
||||
async def test_reauth_flow(
|
||||
hass: HomeAssistant, region: tuple[str, Region], brand: tuple[str, Brand]
|
||||
) -> None:
|
||||
"""Test a successful reauth flow."""
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
data=CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]},
|
||||
unique_id="test-username",
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
@ -204,30 +235,25 @@ async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
assert mock_entry.data == {
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "new-password",
|
||||
"region": region[0],
|
||||
"brand": brand[0],
|
||||
}
|
||||
assert_successful_reauth_flow(mock_entry, result, region, brand)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_appliances_manager_api", "mock_whirlpool_setup_entry")
|
||||
async def test_reauth_flow_invalid_auth(
|
||||
hass: HomeAssistant, region, brand, mock_auth_api: MagicMock
|
||||
hass: HomeAssistant,
|
||||
region: tuple[str, Region],
|
||||
brand: tuple[str, Brand],
|
||||
mock_auth_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test an authorization error reauth flow."""
|
||||
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
data=CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]},
|
||||
unique_id="test-username",
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
@ -238,13 +264,21 @@ async def test_reauth_flow_invalid_auth(
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_auth_api.return_value.is_access_token_valid.return_value = False
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
# Test that it succeeds if the credentials are valid
|
||||
mock_auth_api.return_value.is_access_token_valid.return_value = True
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}
|
||||
)
|
||||
|
||||
assert_successful_reauth_flow(mock_entry, result, region, brand)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_appliances_manager_api", "mock_whirlpool_setup_entry")
|
||||
@ -261,15 +295,15 @@ async def test_reauth_flow_auth_error(
|
||||
hass: HomeAssistant,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
region,
|
||||
brand,
|
||||
region: tuple[str, Region],
|
||||
brand: tuple[str, Brand],
|
||||
mock_auth_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test a connection error reauth flow."""
|
||||
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]},
|
||||
data=CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]},
|
||||
unique_id="test-username",
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
@ -281,9 +315,16 @@ async def test_reauth_flow_auth_error(
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_auth_api.return_value.do_auth.side_effect = exception
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": expected_error}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
|
||||
# Test that it succeeds if the exception is cleared
|
||||
mock_auth_api.return_value.do_auth.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}
|
||||
)
|
||||
|
||||
assert_successful_reauth_flow(mock_entry, result, region, brand)
|
||||
|
@ -3,6 +3,7 @@
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
from whirlpool.washerdryer import MachineState
|
||||
@ -58,6 +59,7 @@ async def test_washer_dryer_time_sensor(
|
||||
entity_id: str,
|
||||
mock_fixture: str,
|
||||
request: pytest.FixtureRequest,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test Washer/Dryer end time sensors."""
|
||||
now = utcnow()
|
||||
@ -113,7 +115,8 @@ async def test_washer_dryer_time_sensor(
|
||||
|
||||
# Test that periodic updates call the API to fetch data
|
||||
mock_instance.fetch_data.reset_mock()
|
||||
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
mock_instance.fetch_data.assert_called_once()
|
||||
|
||||
|
@ -39,10 +39,12 @@ from zwave_js_server.model.value import ConfigurationValue, get_value_id_str
|
||||
from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND
|
||||
from homeassistant.components.zwave_js.api import (
|
||||
APPLICATION_VERSION,
|
||||
AREA_ID,
|
||||
CLIENT_SIDE_AUTH,
|
||||
COMMAND_CLASS_ID,
|
||||
CONFIG,
|
||||
DEVICE_ID,
|
||||
DEVICE_NAME,
|
||||
DSK,
|
||||
ENABLED,
|
||||
ENDPOINT,
|
||||
@ -67,6 +69,7 @@ from homeassistant.components.zwave_js.api import (
|
||||
PRODUCT_TYPE,
|
||||
PROPERTY,
|
||||
PROPERTY_KEY,
|
||||
PROTOCOL,
|
||||
QR_CODE_STRING,
|
||||
QR_PROVISIONING_INFORMATION,
|
||||
REQUESTED_SECURITY_CLASSES,
|
||||
@ -485,14 +488,14 @@ async def test_node_alerts(
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the node comments websocket command."""
|
||||
entry = integration
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, "3245146787-35")})
|
||||
assert device
|
||||
|
||||
await ws_client.send_json(
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
ID: 3,
|
||||
TYPE: "zwave_js/node_alerts",
|
||||
DEVICE_ID: device.id,
|
||||
}
|
||||
@ -502,6 +505,83 @@ async def test_node_alerts(
|
||||
assert result["comments"] == [{"level": "info", "text": "test"}]
|
||||
assert result["is_embedded"]
|
||||
|
||||
# Test with provisioned device
|
||||
valid_qr_info = {
|
||||
VERSION: 1,
|
||||
SECURITY_CLASSES: [0],
|
||||
DSK: "test",
|
||||
GENERIC_DEVICE_CLASS: 1,
|
||||
SPECIFIC_DEVICE_CLASS: 1,
|
||||
INSTALLER_ICON_TYPE: 1,
|
||||
MANUFACTURER_ID: 1,
|
||||
PRODUCT_TYPE: 1,
|
||||
PRODUCT_ID: 1,
|
||||
APPLICATION_VERSION: "test",
|
||||
}
|
||||
|
||||
# Test QR provisioning information
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/provision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
QR_PROVISIONING_INFORMATION: valid_qr_info,
|
||||
DEVICE_NAME: "test",
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
with patch(
|
||||
f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entries",
|
||||
return_value=[
|
||||
ProvisioningEntry.from_dict({**valid_qr_info, "device_id": msg["result"]})
|
||||
],
|
||||
):
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/node_alerts",
|
||||
DEVICE_ID: msg["result"],
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"]["comments"] == [
|
||||
{
|
||||
"level": "info",
|
||||
"text": "This device has been provisioned but is not yet included in the network.",
|
||||
}
|
||||
]
|
||||
|
||||
# Test missing node with no provisioning entry
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, "3245146787-12")},
|
||||
)
|
||||
assert device
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/node_alerts",
|
||||
DEVICE_ID: device.id,
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == ERR_NOT_FOUND
|
||||
|
||||
# Test integration not loaded error - need to unload the integration
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/node_alerts",
|
||||
DEVICE_ID: device.id,
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == ERR_NOT_LOADED
|
||||
|
||||
|
||||
async def test_add_node(
|
||||
hass: HomeAssistant,
|
||||
@ -1093,7 +1173,11 @@ async def test_validate_dsk_and_enter_pin(
|
||||
|
||||
|
||||
async def test_provision_smart_start_node(
|
||||
hass: HomeAssistant, integration, client, hass_ws_client: WebSocketGenerator
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
integration,
|
||||
client,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test provision_smart_start_node websocket command."""
|
||||
entry = integration
|
||||
@ -1131,20 +1215,9 @@ async def test_provision_smart_start_node(
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
"command": "controller.provision_smart_start_node",
|
||||
"entry": QRProvisioningInformation(
|
||||
version=QRCodeVersion.SMART_START,
|
||||
security_classes=[SecurityClass.S2_UNAUTHENTICATED],
|
||||
"entry": ProvisioningEntry(
|
||||
dsk="test",
|
||||
generic_device_class=1,
|
||||
specific_device_class=1,
|
||||
installer_icon_type=1,
|
||||
manufacturer_id=1,
|
||||
product_type=1,
|
||||
product_id=1,
|
||||
application_version="test",
|
||||
max_inclusion_request_interval=None,
|
||||
uuid=None,
|
||||
supported_protocols=None,
|
||||
security_classes=[SecurityClass.S2_UNAUTHENTICATED],
|
||||
additional_properties={"name": "test"},
|
||||
).to_dict(),
|
||||
}
|
||||
@ -1152,6 +1225,51 @@ async def test_provision_smart_start_node(
|
||||
client.async_send_command.reset_mock()
|
||||
client.async_send_command.return_value = {"success": True}
|
||||
|
||||
# Test QR provisioning information with device name and area
|
||||
await ws_client.send_json(
|
||||
{
|
||||
ID: 4,
|
||||
TYPE: "zwave_js/provision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
QR_PROVISIONING_INFORMATION: {
|
||||
**valid_qr_info,
|
||||
},
|
||||
PROTOCOL: Protocols.ZWAVE_LONG_RANGE,
|
||||
DEVICE_NAME: "test_name",
|
||||
AREA_ID: "test_area",
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
# verify a device was created
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "provision_test")},
|
||||
)
|
||||
assert device is not None
|
||||
assert device.name == "test_name"
|
||||
assert device.area_id == "test_area"
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert client.async_send_command.call_args_list[0][0][0] == {
|
||||
"command": "config_manager.lookup_device",
|
||||
"manufacturerId": 1,
|
||||
"productType": 1,
|
||||
"productId": 1,
|
||||
}
|
||||
assert client.async_send_command.call_args_list[1][0][0] == {
|
||||
"command": "controller.provision_smart_start_node",
|
||||
"entry": ProvisioningEntry(
|
||||
dsk="test",
|
||||
security_classes=[SecurityClass.S2_UNAUTHENTICATED],
|
||||
protocol=Protocols.ZWAVE_LONG_RANGE,
|
||||
additional_properties={
|
||||
"name": "test",
|
||||
"device_id": device.id,
|
||||
},
|
||||
).to_dict(),
|
||||
}
|
||||
|
||||
# Test QR provisioning information with S2 version throws error
|
||||
await ws_client.send_json(
|
||||
{
|
||||
@ -1230,7 +1348,11 @@ async def test_provision_smart_start_node(
|
||||
|
||||
|
||||
async def test_unprovision_smart_start_node(
|
||||
hass: HomeAssistant, integration, client, hass_ws_client: WebSocketGenerator
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
integration,
|
||||
client,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test unprovision_smart_start_node websocket command."""
|
||||
entry = integration
|
||||
@ -1239,9 +1361,8 @@ async def test_unprovision_smart_start_node(
|
||||
client.async_send_command.return_value = {}
|
||||
|
||||
# Test node ID as input
|
||||
await ws_client.send_json(
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
ID: 1,
|
||||
TYPE: "zwave_js/unprovision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
NODE_ID: 1,
|
||||
@ -1251,8 +1372,12 @@ async def test_unprovision_smart_start_node(
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert client.async_send_command.call_args_list[0][0][0] == {
|
||||
"command": "controller.get_provisioning_entry",
|
||||
"dskOrNodeId": 1,
|
||||
}
|
||||
assert client.async_send_command.call_args_list[1][0][0] == {
|
||||
"command": "controller.unprovision_smart_start_node",
|
||||
"dskOrNodeId": 1,
|
||||
}
|
||||
@ -1261,9 +1386,8 @@ async def test_unprovision_smart_start_node(
|
||||
client.async_send_command.return_value = {}
|
||||
|
||||
# Test DSK as input
|
||||
await ws_client.send_json(
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
ID: 2,
|
||||
TYPE: "zwave_js/unprovision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
DSK: "test",
|
||||
@ -1273,8 +1397,12 @@ async def test_unprovision_smart_start_node(
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert client.async_send_command.call_args_list[0][0][0] == {
|
||||
"command": "controller.get_provisioning_entry",
|
||||
"dskOrNodeId": "test",
|
||||
}
|
||||
assert client.async_send_command.call_args_list[1][0][0] == {
|
||||
"command": "controller.unprovision_smart_start_node",
|
||||
"dskOrNodeId": "test",
|
||||
}
|
||||
@ -1283,9 +1411,8 @@ async def test_unprovision_smart_start_node(
|
||||
client.async_send_command.return_value = {}
|
||||
|
||||
# Test not including DSK or node ID as input fails
|
||||
await ws_client.send_json(
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
ID: 3,
|
||||
TYPE: "zwave_js/unprovision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
}
|
||||
@ -1296,14 +1423,78 @@ async def test_unprovision_smart_start_node(
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 0
|
||||
|
||||
# Test with pre provisioned device
|
||||
# Create device registry entry for mock node
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, "provision_test"), ("other_domain", "test")},
|
||||
name="Node 67",
|
||||
)
|
||||
provisioning_entry = ProvisioningEntry.from_dict(
|
||||
{
|
||||
"dsk": "test",
|
||||
"securityClasses": [SecurityClass.S2_UNAUTHENTICATED],
|
||||
"device_id": device.id,
|
||||
}
|
||||
)
|
||||
with patch.object(
|
||||
client.driver.controller,
|
||||
"async_get_provisioning_entry",
|
||||
return_value=provisioning_entry,
|
||||
):
|
||||
# Don't remove the device if it has additional identifiers
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/unprovision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
DSK: "test",
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
"command": "controller.unprovision_smart_start_node",
|
||||
"dskOrNodeId": "test",
|
||||
}
|
||||
|
||||
device = device_registry.async_get(device.id)
|
||||
assert device is not None
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Remove the device if it doesn't have additional identifiers
|
||||
device_registry.async_update_device(
|
||||
device.id, new_identifiers={(DOMAIN, "provision_test")}
|
||||
)
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/unprovision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
DSK: "test",
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert client.async_send_command.call_args[0][0] == {
|
||||
"command": "controller.unprovision_smart_start_node",
|
||||
"dskOrNodeId": "test",
|
||||
}
|
||||
|
||||
# Verify device was removed from device registry
|
||||
device = device_registry.async_get(device.id)
|
||||
assert device is None
|
||||
|
||||
# Test FailedZWaveCommand is caught
|
||||
with patch(
|
||||
f"{CONTROLLER_PATCH_PREFIX}.async_unprovision_smart_start_node",
|
||||
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
|
||||
):
|
||||
await ws_client.send_json(
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
ID: 6,
|
||||
TYPE: "zwave_js/unprovision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
DSK: "test",
|
||||
@ -1319,9 +1510,8 @@ async def test_unprovision_smart_start_node(
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await ws_client.send_json(
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
ID: 7,
|
||||
TYPE: "zwave_js/unprovision_smart_start_node",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
DSK: "test",
|
||||
@ -5658,3 +5848,39 @@ async def test_lookup_device(
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == error_message
|
||||
assert msg["error"]["message"] == f"Command failed: {error_message}"
|
||||
|
||||
|
||||
async def test_subscribe_new_devices(
|
||||
hass: HomeAssistant,
|
||||
integration,
|
||||
client,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
multisensor_6_state,
|
||||
) -> None:
|
||||
"""Test the subscribe_new_devices websocket command."""
|
||||
entry = integration
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/subscribe_new_devices",
|
||||
ENTRY_ID: entry.entry_id,
|
||||
}
|
||||
)
|
||||
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] is None
|
||||
|
||||
# Simulate a device being registered
|
||||
node = Node(client, deepcopy(multisensor_6_state))
|
||||
client.driver.controller.emit("node added", {"node": node})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify we receive the expected message
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["type"] == "event"
|
||||
assert msg["event"]["event"] == "device registered"
|
||||
assert msg["event"]["device"]["name"] == node.device_config.description
|
||||
assert msg["event"]["device"]["manufacturer"] == node.device_config.manufacturer
|
||||
assert msg["event"]["device"]["model"] == node.device_config.label
|
||||
|
@ -1,17 +1,27 @@
|
||||
"""Test the Z-Wave JS helpers module."""
|
||||
|
||||
import voluptuous as vol
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.const import SecurityClass
|
||||
from zwave_js_server.model.controller import ProvisioningEntry
|
||||
|
||||
from homeassistant.components.zwave_js.const import DOMAIN
|
||||
from homeassistant.components.zwave_js.helpers import (
|
||||
async_get_node_status_sensor_entity_id,
|
||||
async_get_nodes_from_area_id,
|
||||
async_get_provisioning_entry_from_device_id,
|
||||
get_value_state_schema,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import area_registry as ar, device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONTROLLER_PATCH_PREFIX = "zwave_js_server.model.controller.Controller"
|
||||
|
||||
|
||||
async def test_async_get_node_status_sensor_entity_id(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
@ -43,3 +53,82 @@ async def test_get_value_state_schema_boolean_config_value(
|
||||
)
|
||||
assert isinstance(schema_validator, vol.Coerce)
|
||||
assert schema_validator.type is bool
|
||||
|
||||
|
||||
async def test_async_get_provisioning_entry_from_device_id(
|
||||
hass: HomeAssistant, client, device_registry: dr.DeviceRegistry, integration
|
||||
) -> None:
|
||||
"""Test async_get_provisioning_entry_from_device_id function."""
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=integration.entry_id,
|
||||
identifiers={(DOMAIN, "test-device")},
|
||||
)
|
||||
|
||||
provisioning_entry = ProvisioningEntry.from_dict(
|
||||
{
|
||||
"dsk": "test",
|
||||
"securityClasses": [SecurityClass.S2_UNAUTHENTICATED],
|
||||
"device_id": device.id,
|
||||
}
|
||||
)
|
||||
|
||||
with patch(
|
||||
f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entries",
|
||||
return_value=[provisioning_entry],
|
||||
):
|
||||
result = await async_get_provisioning_entry_from_device_id(hass, device.id)
|
||||
assert result == provisioning_entry
|
||||
|
||||
# Test invalid device
|
||||
with pytest.raises(ValueError, match="Device ID not-a-real-device is not valid"):
|
||||
await async_get_provisioning_entry_from_device_id(hass, "not-a-real-device")
|
||||
|
||||
# Test device exists but is not from a zwave_js config entry
|
||||
non_zwave_config_entry = MockConfigEntry(domain="not_zwave_js")
|
||||
non_zwave_config_entry.add_to_hass(hass)
|
||||
non_zwave_device = device_registry.async_get_or_create(
|
||||
config_entry_id=non_zwave_config_entry.entry_id,
|
||||
identifiers={("not_zwave_js", "test-device")},
|
||||
)
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=f"Device {non_zwave_device.id} is not from an existing zwave_js config entry",
|
||||
):
|
||||
await async_get_provisioning_entry_from_device_id(hass, non_zwave_device.id)
|
||||
|
||||
# Test device exists but config entry is not loaded
|
||||
not_loaded_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, state=ConfigEntryState.NOT_LOADED
|
||||
)
|
||||
not_loaded_config_entry.add_to_hass(hass)
|
||||
not_loaded_device = device_registry.async_get_or_create(
|
||||
config_entry_id=not_loaded_config_entry.entry_id,
|
||||
identifiers={(DOMAIN, "not-loaded-device")},
|
||||
)
|
||||
with pytest.raises(
|
||||
ValueError, match=f"Device {not_loaded_device.id} config entry is not loaded"
|
||||
):
|
||||
await async_get_provisioning_entry_from_device_id(hass, not_loaded_device.id)
|
||||
|
||||
# Test no matching provisioning entry
|
||||
with patch(
|
||||
f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entries",
|
||||
return_value=[],
|
||||
):
|
||||
result = await async_get_provisioning_entry_from_device_id(hass, device.id)
|
||||
assert result is None
|
||||
|
||||
# Test multiple provisioning entries but only one matches
|
||||
other_provisioning_entry = ProvisioningEntry.from_dict(
|
||||
{
|
||||
"dsk": "other",
|
||||
"securityClasses": [SecurityClass.S2_UNAUTHENTICATED],
|
||||
"device_id": "other-id",
|
||||
}
|
||||
)
|
||||
with patch(
|
||||
f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entries",
|
||||
return_value=[other_provisioning_entry, provisioning_entry],
|
||||
):
|
||||
result = await async_get_provisioning_entry_from_device_id(hass, device.id)
|
||||
assert result == provisioning_entry
|
||||
|
@ -11,12 +11,14 @@ from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import AddonsOptions
|
||||
import pytest
|
||||
from zwave_js_server.client import Client
|
||||
from zwave_js_server.const import SecurityClass
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.exceptions import (
|
||||
BaseZwaveJSServerError,
|
||||
InvalidServerVersion,
|
||||
NotConnected,
|
||||
)
|
||||
from zwave_js_server.model.controller import ProvisioningEntry
|
||||
from zwave_js_server.model.node import Node, NodeDataType
|
||||
from zwave_js_server.model.version import VersionInfo
|
||||
|
||||
@ -24,7 +26,7 @@ from homeassistant.components.hassio import HassioAPIError
|
||||
from homeassistant.components.logger import DOMAIN as LOGGER_DOMAIN, SERVICE_SET_LEVEL
|
||||
from homeassistant.components.persistent_notification import async_dismiss
|
||||
from homeassistant.components.zwave_js import DOMAIN
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id, get_device_id_ext
|
||||
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import CoreState, HomeAssistant
|
||||
@ -45,6 +47,8 @@ from tests.common import (
|
||||
)
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
CONTROLLER_PATCH_PREFIX = "zwave_js_server.model.controller.Controller"
|
||||
|
||||
|
||||
@pytest.fixture(name="connect_timeout")
|
||||
def connect_timeout_fixture() -> Generator[int]:
|
||||
@ -277,10 +281,13 @@ async def test_listen_done_during_setup_after_forward_entry(
|
||||
"""Test listen task finishing during setup after forward entry."""
|
||||
assert hass.state is CoreState.running
|
||||
|
||||
original_send_command_side_effect = client.async_send_command.side_effect
|
||||
|
||||
async def send_command_side_effect(*args: Any, **kwargs: Any) -> None:
|
||||
"""Mock send command."""
|
||||
listen_block.set()
|
||||
getattr(listen_result, listen_future_result_method)(listen_future_result)
|
||||
client.async_send_command.side_effect = original_send_command_side_effect
|
||||
# Yield to allow the listen task to run
|
||||
await asyncio.sleep(0)
|
||||
|
||||
@ -427,6 +434,46 @@ async def test_on_node_added_ready(
|
||||
)
|
||||
|
||||
|
||||
async def test_on_node_added_preprovisioned(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
multisensor_6_state,
|
||||
client,
|
||||
integration,
|
||||
) -> None:
|
||||
"""Test node added event with a preprovisioned device."""
|
||||
dsk = "test"
|
||||
node = Node(client, deepcopy(multisensor_6_state))
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=integration.entry_id,
|
||||
identifiers={(DOMAIN, f"provision_{dsk}")},
|
||||
)
|
||||
provisioning_entry = ProvisioningEntry.from_dict(
|
||||
{
|
||||
"dsk": dsk,
|
||||
"securityClasses": [SecurityClass.S2_UNAUTHENTICATED],
|
||||
"device_id": device.id,
|
||||
}
|
||||
)
|
||||
with patch(
|
||||
f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entry",
|
||||
side_effect=lambda id: provisioning_entry if id == node.node_id else None,
|
||||
):
|
||||
event = {"node": node}
|
||||
client.driver.controller.emit("node added", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get(device.id)
|
||||
assert device
|
||||
assert device.identifiers == {
|
||||
get_device_id(client.driver, node),
|
||||
get_device_id_ext(client.driver, node),
|
||||
}
|
||||
assert device.sw_version == node.firmware_version
|
||||
# There should only be the controller and the preprovisioned device
|
||||
assert len(device_registry.devices) == 2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("integration")
|
||||
async def test_on_node_added_not_ready(
|
||||
hass: HomeAssistant,
|
||||
@ -2045,7 +2092,14 @@ async def test_server_logging(hass: HomeAssistant, client: MagicMock) -> None:
|
||||
# is enabled
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(client.async_send_command.call_args_list) == 0
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert client.async_send_command.call_args_list[0][0][0] == {
|
||||
"command": "controller.get_provisioning_entries",
|
||||
}
|
||||
assert client.async_send_command.call_args_list[1][0][0] == {
|
||||
"command": "controller.get_provisioning_entry",
|
||||
"dskOrNodeId": 1,
|
||||
}
|
||||
assert not client.enable_server_logging.called
|
||||
assert not client.disable_server_logging.called
|
||||
|
||||
|
@ -123,7 +123,7 @@ async def test_number_writeable(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert len(client.async_send_command.call_args_list) == 5
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == 4
|
||||
|
@ -324,12 +324,12 @@ async def test_update_entity_ha_not_running(
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert len(client.async_send_command.call_args_list) == 4
|
||||
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert len(client.async_send_command.call_args_list) == 4
|
||||
|
||||
# Update should be delayed by a day because HA is not running
|
||||
hass.set_state(CoreState.starting)
|
||||
@ -337,15 +337,15 @@ async def test_update_entity_ha_not_running(
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
assert len(client.async_send_command.call_args_list) == 4
|
||||
|
||||
hass.set_state(CoreState.running)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
args = client.async_send_command.call_args_list[1][0][0]
|
||||
assert len(client.async_send_command.call_args_list) == 5
|
||||
args = client.async_send_command.call_args_list[4][0][0]
|
||||
assert args["command"] == "controller.get_available_firmware_updates"
|
||||
assert args["nodeId"] == zen_31.node_id
|
||||
|
||||
@ -651,12 +651,12 @@ async def test_update_entity_delay(
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert len(client.async_send_command.call_args_list) == 6
|
||||
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert len(client.async_send_command.call_args_list) == 6
|
||||
|
||||
update_interval = timedelta(minutes=5)
|
||||
freezer.tick(update_interval)
|
||||
@ -665,8 +665,8 @@ async def test_update_entity_delay(
|
||||
|
||||
nodes: set[int] = set()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 3
|
||||
args = client.async_send_command.call_args_list[2][0][0]
|
||||
assert len(client.async_send_command.call_args_list) == 7
|
||||
args = client.async_send_command.call_args_list[6][0][0]
|
||||
assert args["command"] == "controller.get_available_firmware_updates"
|
||||
nodes.add(args["nodeId"])
|
||||
|
||||
@ -674,8 +674,8 @@ async def test_update_entity_delay(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 4
|
||||
args = client.async_send_command.call_args_list[3][0][0]
|
||||
assert len(client.async_send_command.call_args_list) == 8
|
||||
args = client.async_send_command.call_args_list[7][0][0]
|
||||
assert args["command"] == "controller.get_available_firmware_updates"
|
||||
nodes.add(args["nodeId"])
|
||||
|
||||
@ -846,8 +846,8 @@ async def test_update_entity_full_restore_data_update_available(
|
||||
assert attrs[ATTR_IN_PROGRESS] is True
|
||||
assert attrs[ATTR_UPDATE_PERCENTAGE] is None
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 2
|
||||
assert client.async_send_command.call_args_list[1][0][0] == {
|
||||
assert len(client.async_send_command.call_args_list) == 5
|
||||
assert client.async_send_command.call_args_list[4][0][0] == {
|
||||
"command": "controller.firmware_update_ota",
|
||||
"nodeId": climate_radio_thermostat_ct100_plus_different_endpoints.node_id,
|
||||
"updateInfo": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user