Add integration lamarzocco (#102291)
Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com> Co-authored-by: Paul Bottein <paul.bottein@gmail.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: tronikos <tronikos@users.noreply.github.com> Co-authored-by: Luke Lashley <conway220@gmail.com> Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: dupondje <jean-louis@dupond.be>pull/108159/head
parent
a874895a81
commit
6bc36666b1
|
@ -248,6 +248,7 @@ homeassistant.components.knx.*
|
|||
homeassistant.components.kraken.*
|
||||
homeassistant.components.lacrosse.*
|
||||
homeassistant.components.lacrosse_view.*
|
||||
homeassistant.components.lamarzocco.*
|
||||
homeassistant.components.lametric.*
|
||||
homeassistant.components.laundrify.*
|
||||
homeassistant.components.lawn_mower.*
|
||||
|
|
|
@ -688,6 +688,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/kulersky/ @emlove
|
||||
/homeassistant/components/lacrosse_view/ @IceBotYT
|
||||
/tests/components/lacrosse_view/ @IceBotYT
|
||||
/homeassistant/components/lamarzocco/ @zweckj
|
||||
/tests/components/lamarzocco/ @zweckj
|
||||
/homeassistant/components/lametric/ @robbiet480 @frenck @bachya
|
||||
/tests/components/lametric/ @robbiet480 @frenck @bachya
|
||||
/homeassistant/components/landisgyr_heat_meter/ @vpathuis
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
"""The La Marzocco integration."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LaMarzoccoUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up La Marzocco as config entry."""
|
||||
|
||||
coordinator = LaMarzoccoUpdateCoordinator(hass)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
|
@ -0,0 +1,156 @@
|
|||
"""Config flow for La Marzocco integration."""
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from lmcloud import LMCloud as LaMarzoccoClient
|
||||
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_MACHINE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for La Marzocco."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
|
||||
self.reauth_entry: ConfigEntry | None = None
|
||||
self._config: dict[str, Any] = {}
|
||||
self._machines: list[tuple[str, str]] = []
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors = {}
|
||||
|
||||
if user_input:
|
||||
data: dict[str, Any] = {}
|
||||
if self.reauth_entry:
|
||||
data = dict(self.reauth_entry.data)
|
||||
data = {
|
||||
**data,
|
||||
**user_input,
|
||||
}
|
||||
|
||||
lm = LaMarzoccoClient()
|
||||
try:
|
||||
self._machines = await lm.get_all_machines(data)
|
||||
except AuthFail:
|
||||
_LOGGER.debug("Server rejected login credentials")
|
||||
errors["base"] = "invalid_auth"
|
||||
except RequestNotSuccessful as exc:
|
||||
_LOGGER.exception("Error connecting to server: %s", str(exc))
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
if not self._machines:
|
||||
errors["base"] = "no_machines"
|
||||
|
||||
if not errors:
|
||||
if self.reauth_entry:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.reauth_entry, data=user_input
|
||||
)
|
||||
await self.hass.config_entries.async_reload(
|
||||
self.reauth_entry.entry_id
|
||||
)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
if not errors:
|
||||
self._config = data
|
||||
return await self.async_step_machine_selection()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_machine_selection(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Let user select machine to connect to."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
serial_number = user_input[CONF_MACHINE]
|
||||
await self.async_set_unique_id(serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# validate local connection if host is provided
|
||||
if user_input.get(CONF_HOST):
|
||||
lm = LaMarzoccoClient()
|
||||
if not await lm.check_local_connection(
|
||||
credentials=self._config,
|
||||
host=user_input[CONF_HOST],
|
||||
serial=serial_number,
|
||||
):
|
||||
errors[CONF_HOST] = "cannot_connect"
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=serial_number,
|
||||
data=self._config | user_input,
|
||||
)
|
||||
|
||||
machine_options = [
|
||||
SelectOptionDict(
|
||||
value=serial_number,
|
||||
label=f"{model_name} ({serial_number})",
|
||||
)
|
||||
for serial_number, model_name in self._machines
|
||||
]
|
||||
|
||||
machine_selection_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_MACHINE, default=machine_options[0]["value"]
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=machine_options,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="machine_selection",
|
||||
data_schema=machine_selection_schema,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
"""Constants for the La Marzocco integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "lamarzocco"
|
||||
|
||||
CONF_MACHINE: Final = "machine"
|
|
@ -0,0 +1,96 @@
|
|||
"""Coordinator for La Marzocco API."""
|
||||
from collections.abc import Callable, Coroutine
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from lmcloud import LMCloud as LaMarzoccoClient
|
||||
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_MACHINE, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Class to handle fetching data from the La Marzocco API centrally."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
|
||||
self.lm = LaMarzoccoClient(
|
||||
callback_websocket_notify=self.async_update_listeners,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
|
||||
if not self.lm.initialized:
|
||||
await self._async_init_client()
|
||||
|
||||
await self._async_handle_request(
|
||||
self.lm.update_local_machine_status, force_update=True
|
||||
)
|
||||
|
||||
_LOGGER.debug("Current status: %s", str(self.lm.current_status))
|
||||
|
||||
async def _async_init_client(self) -> None:
|
||||
"""Initialize the La Marzocco Client."""
|
||||
|
||||
# Initialize cloud API
|
||||
_LOGGER.debug("Initializing Cloud API")
|
||||
await self._async_handle_request(
|
||||
self.lm.init_cloud_api,
|
||||
credentials=self.config_entry.data,
|
||||
machine_serial=self.config_entry.data[CONF_MACHINE],
|
||||
)
|
||||
_LOGGER.debug("Model name: %s", self.lm.model_name)
|
||||
|
||||
# initialize local API
|
||||
if (host := self.config_entry.data.get(CONF_HOST)) is not None:
|
||||
_LOGGER.debug("Initializing local API")
|
||||
await self.lm.init_local_api(
|
||||
host=host,
|
||||
client=get_async_client(self.hass),
|
||||
)
|
||||
|
||||
_LOGGER.debug("Init WebSocket in Background Task")
|
||||
|
||||
self.config_entry.async_create_background_task(
|
||||
hass=self.hass,
|
||||
target=self.lm.lm_local_api.websocket_connect(
|
||||
callback=self.lm.on_websocket_message_received,
|
||||
use_sigterm_handler=False,
|
||||
),
|
||||
name="lm_websocket_task",
|
||||
)
|
||||
|
||||
self.lm.initialized = True
|
||||
|
||||
async def _async_handle_request(
|
||||
self,
|
||||
func: Callable[..., Coroutine[None, None, None]],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Handle a request to the API."""
|
||||
try:
|
||||
await func(*args, **kwargs)
|
||||
except AuthFail as ex:
|
||||
msg = "Authentication failed."
|
||||
_LOGGER.debug(msg, exc_info=True)
|
||||
raise ConfigEntryAuthFailed(msg) from ex
|
||||
except RequestNotSuccessful as ex:
|
||||
_LOGGER.debug(ex, exc_info=True)
|
||||
raise UpdateFailed("Querying API failed. Error: %s" % ex) from ex
|
|
@ -0,0 +1,50 @@
|
|||
"""Base class for the La Marzocco entities."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lmcloud.const import LaMarzoccoModel
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LaMarzoccoUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LaMarzoccoEntityDescription(EntityDescription):
|
||||
"""Description for all LM entities."""
|
||||
|
||||
supported_models: tuple[LaMarzoccoModel, ...] = (
|
||||
LaMarzoccoModel.GS3_AV,
|
||||
LaMarzoccoModel.GS3_MP,
|
||||
LaMarzoccoModel.LINEA_MICRA,
|
||||
LaMarzoccoModel.LINEA_MINI,
|
||||
)
|
||||
|
||||
|
||||
class LaMarzoccoEntity(CoordinatorEntity[LaMarzoccoUpdateCoordinator]):
|
||||
"""Common elements for all entities."""
|
||||
|
||||
entity_description: LaMarzoccoEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LaMarzoccoUpdateCoordinator,
|
||||
entity_description: LaMarzoccoEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
lm = coordinator.lm
|
||||
self._attr_unique_id = f"{lm.serial_number}_{entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, lm.serial_number)},
|
||||
name=lm.machine_name,
|
||||
manufacturer="La Marzocco",
|
||||
model=lm.true_model_name,
|
||||
serial_number=lm.serial_number,
|
||||
sw_version=lm.firmware_version,
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"domain": "lamarzocco",
|
||||
"name": "La Marzocco",
|
||||
"codeowners": ["@zweckj"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lamarzocco",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["lmcloud"],
|
||||
"requirements": ["lmcloud==0.4.34"]
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "La Marzocco Espresso {host}",
|
||||
"abort": {
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_machines": "No machines found in account",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"machine_not_found": "The configured machine was not found in your account. Did you login to the correct account?",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "Your username from the La Marzocco app",
|
||||
"password": "Your password from the La Marzocco app"
|
||||
}
|
||||
},
|
||||
"machine_selection": {
|
||||
"description": "Select the machine you want to integrate. Set the \"IP\" to get access to shot time related sensors.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::ip%]",
|
||||
"machine": "Machine"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Local IP address of the machine"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"auto_on_off": {
|
||||
"name": "Auto on/off"
|
||||
},
|
||||
"steam_boiler": {
|
||||
"name": "Steam boiler"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
"""Switch platform for La Marzocco espresso machines."""
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LaMarzoccoUpdateCoordinator
|
||||
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LaMarzoccoSwitchEntityDescription(
|
||||
LaMarzoccoEntityDescription,
|
||||
SwitchEntityDescription,
|
||||
):
|
||||
"""Description of a La Marzocco Switch."""
|
||||
|
||||
control_fn: Callable[[LaMarzoccoUpdateCoordinator, bool], Coroutine[Any, Any, bool]]
|
||||
is_on_fn: Callable[[LaMarzoccoUpdateCoordinator], bool]
|
||||
|
||||
|
||||
ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
|
||||
LaMarzoccoSwitchEntityDescription(
|
||||
key="main",
|
||||
name=None,
|
||||
icon="mdi:power",
|
||||
control_fn=lambda coordinator, state: coordinator.lm.set_power(state),
|
||||
is_on_fn=lambda coordinator: coordinator.lm.current_status["power"],
|
||||
),
|
||||
LaMarzoccoSwitchEntityDescription(
|
||||
key="auto_on_off",
|
||||
translation_key="auto_on_off",
|
||||
icon="mdi:alarm",
|
||||
control_fn=lambda coordinator, state: coordinator.lm.set_auto_on_off_global(
|
||||
state
|
||||
),
|
||||
is_on_fn=lambda coordinator: coordinator.lm.current_status["global_auto"]
|
||||
== "Enabled",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
LaMarzoccoSwitchEntityDescription(
|
||||
key="steam_boiler_enable",
|
||||
translation_key="steam_boiler",
|
||||
icon="mdi:water-boiler",
|
||||
control_fn=lambda coordinator, state: coordinator.lm.set_steam(state),
|
||||
is_on_fn=lambda coordinator: coordinator.lm.current_status[
|
||||
"steam_boiler_enable"
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switch entities and services."""
|
||||
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
LaMarzoccoSwitchEntity(coordinator, description)
|
||||
for description in ENTITIES
|
||||
if coordinator.lm.model_name in description.supported_models
|
||||
)
|
||||
|
||||
|
||||
class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity):
|
||||
"""Switches representing espresso machine power, prebrew, and auto on/off."""
|
||||
|
||||
entity_description: LaMarzoccoSwitchEntityDescription
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn device on."""
|
||||
await self.entity_description.control_fn(self.coordinator, True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn device off."""
|
||||
await self.entity_description.control_fn(self.coordinator, False)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
return self.entity_description.is_on_fn(self.coordinator)
|
|
@ -261,6 +261,7 @@ FLOWS = {
|
|||
"kraken",
|
||||
"kulersky",
|
||||
"lacrosse_view",
|
||||
"lamarzocco",
|
||||
"lametric",
|
||||
"landisgyr_heat_meter",
|
||||
"lastfm",
|
||||
|
|
|
@ -2999,6 +2999,12 @@
|
|||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"lamarzocco": {
|
||||
"name": "La Marzocco",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"lametric": {
|
||||
"name": "LaMetric",
|
||||
"integration_type": "device",
|
||||
|
|
10
mypy.ini
10
mypy.ini
|
@ -2241,6 +2241,16 @@ disallow_untyped_defs = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.lamarzocco.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.lametric.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -1209,6 +1209,9 @@ linear-garage-door==0.2.7
|
|||
# homeassistant.components.linode
|
||||
linode-api==4.1.9b1
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
lmcloud==0.4.34
|
||||
|
||||
# homeassistant.components.google_maps
|
||||
locationsharinglib==5.0.1
|
||||
|
||||
|
|
|
@ -954,6 +954,9 @@ libsoundtouch==0.8
|
|||
# homeassistant.components.linear_garage_door
|
||||
linear-garage-door==0.2.7
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
lmcloud==0.4.34
|
||||
|
||||
# homeassistant.components.logi_circle
|
||||
logi-circle==0.2.3
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
"""Mock inputs for tests."""
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
HOST_SELECTION = {
|
||||
CONF_HOST: "192.168.1.1",
|
||||
}
|
||||
|
||||
PASSWORD_SELECTION = {
|
||||
CONF_PASSWORD: "password",
|
||||
}
|
||||
|
||||
USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"}
|
||||
|
||||
|
||||
async def async_init_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Set up the La Marzocco integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
|
@ -0,0 +1,104 @@
|
|||
"""Lamarzocco session fixtures."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from lmcloud.const import LaMarzoccoModel
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import USER_INPUT, async_init_integration
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
load_json_array_fixture,
|
||||
load_json_object_fixture,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(mock_lamarzocco: MagicMock) -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title="My LaMarzocco",
|
||||
domain=DOMAIN,
|
||||
data=USER_INPUT | {CONF_MACHINE: mock_lamarzocco.serial_number},
|
||||
unique_id=mock_lamarzocco.serial_number,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the LaMetric integration for testing."""
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_fixture() -> LaMarzoccoModel:
|
||||
"""Return the device fixture for a specific device."""
|
||||
return LaMarzoccoModel.GS3_AV
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_lamarzocco(
|
||||
request: pytest.FixtureRequest, device_fixture: LaMarzoccoModel
|
||||
) -> Generator[MagicMock, None, None]:
|
||||
"""Return a mocked LM client."""
|
||||
model_name = device_fixture
|
||||
|
||||
if model_name == LaMarzoccoModel.GS3_AV:
|
||||
serial_number = "GS01234"
|
||||
true_model_name = "GS3 AV"
|
||||
elif model_name == LaMarzoccoModel.GS3_MP:
|
||||
serial_number = "GS01234"
|
||||
true_model_name = "GS3 MP"
|
||||
elif model_name == LaMarzoccoModel.LINEA_MICRA:
|
||||
serial_number = "MR01234"
|
||||
true_model_name = "Linea Micra"
|
||||
elif model_name == LaMarzoccoModel.LINEA_MINI:
|
||||
serial_number = "LM01234"
|
||||
true_model_name = "Linea Mini"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient",
|
||||
autospec=True,
|
||||
) as lamarzocco_mock, patch(
|
||||
"homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient",
|
||||
new=lamarzocco_mock,
|
||||
):
|
||||
lamarzocco = lamarzocco_mock.return_value
|
||||
|
||||
lamarzocco.machine_info = {
|
||||
"machine_name": serial_number,
|
||||
"serial_number": serial_number,
|
||||
}
|
||||
|
||||
lamarzocco.model_name = model_name
|
||||
lamarzocco.true_model_name = true_model_name
|
||||
lamarzocco.machine_name = serial_number
|
||||
lamarzocco.serial_number = serial_number
|
||||
|
||||
lamarzocco.firmware_version = "1.1"
|
||||
lamarzocco.latest_firmware_version = "1.1"
|
||||
lamarzocco.gateway_version = "v2.2-rc0"
|
||||
lamarzocco.latest_gateway_version = "v3.1-rc4"
|
||||
|
||||
lamarzocco.current_status = load_json_object_fixture(
|
||||
"current_status.json", DOMAIN
|
||||
)
|
||||
lamarzocco.config = load_json_object_fixture("config.json", DOMAIN)
|
||||
lamarzocco.statistics = load_json_array_fixture("statistics.json", DOMAIN)
|
||||
|
||||
lamarzocco.get_all_machines.return_value = [
|
||||
(serial_number, model_name),
|
||||
]
|
||||
lamarzocco.check_local_connection.return_value = True
|
||||
lamarzocco.initialized = False
|
||||
|
||||
yield lamarzocco
|
|
@ -0,0 +1,187 @@
|
|||
{
|
||||
"version": "v1",
|
||||
"preinfusionModesAvailable": ["ByDoseType"],
|
||||
"machineCapabilities": [
|
||||
{
|
||||
"family": "GS3AV",
|
||||
"groupsNumber": 1,
|
||||
"coffeeBoilersNumber": 1,
|
||||
"hasCupWarmer": false,
|
||||
"steamBoilersNumber": 1,
|
||||
"teaDosesNumber": 1,
|
||||
"machineModes": ["BrewingMode", "StandBy"],
|
||||
"schedulingType": "weeklyScheduling"
|
||||
}
|
||||
],
|
||||
"machine_sn": "GS01234",
|
||||
"machine_hw": "2",
|
||||
"isPlumbedIn": true,
|
||||
"isBackFlushEnabled": false,
|
||||
"standByTime": 0,
|
||||
"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": [
|
||||
{
|
||||
"groupNumber": "Group1",
|
||||
"doseType": "DoseA",
|
||||
"preWetTime": 0.5,
|
||||
"preWetHoldTime": 1
|
||||
},
|
||||
{
|
||||
"groupNumber": "Group1",
|
||||
"doseType": "DoseB",
|
||||
"preWetTime": 0.5,
|
||||
"preWetHoldTime": 1
|
||||
},
|
||||
{
|
||||
"groupNumber": "Group1",
|
||||
"doseType": "DoseC",
|
||||
"preWetTime": 3.2999999523162842,
|
||||
"preWetHoldTime": 3.2999999523162842
|
||||
},
|
||||
{
|
||||
"groupNumber": "Group1",
|
||||
"doseType": "DoseD",
|
||||
"preWetTime": 2,
|
||||
"preWetHoldTime": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"weeklySchedulingConfig": {
|
||||
"enabled": true,
|
||||
"monday": {
|
||||
"enabled": true,
|
||||
"h_on": 6,
|
||||
"h_off": 16,
|
||||
"m_on": 0,
|
||||
"m_off": 0
|
||||
},
|
||||
"tuesday": {
|
||||
"enabled": true,
|
||||
"h_on": 6,
|
||||
"h_off": 16,
|
||||
"m_on": 0,
|
||||
"m_off": 0
|
||||
},
|
||||
"wednesday": {
|
||||
"enabled": true,
|
||||
"h_on": 6,
|
||||
"h_off": 16,
|
||||
"m_on": 0,
|
||||
"m_off": 0
|
||||
},
|
||||
"thursday": {
|
||||
"enabled": true,
|
||||
"h_on": 6,
|
||||
"h_off": 16,
|
||||
"m_on": 0,
|
||||
"m_off": 0
|
||||
},
|
||||
"friday": {
|
||||
"enabled": true,
|
||||
"h_on": 6,
|
||||
"h_off": 16,
|
||||
"m_on": 0,
|
||||
"m_off": 0
|
||||
},
|
||||
"saturday": {
|
||||
"enabled": true,
|
||||
"h_on": 6,
|
||||
"h_off": 16,
|
||||
"m_on": 0,
|
||||
"m_off": 0
|
||||
},
|
||||
"sunday": {
|
||||
"enabled": true,
|
||||
"h_on": 6,
|
||||
"h_off": 16,
|
||||
"m_on": 0,
|
||||
"m_off": 0
|
||||
}
|
||||
},
|
||||
"clock": "1901-07-08T10:29:00",
|
||||
"firmwareVersions": [
|
||||
{
|
||||
"name": "machine_firmware",
|
||||
"fw_version": "1.40"
|
||||
},
|
||||
{
|
||||
"name": "gateway_firmware",
|
||||
"fw_version": "v3.1-rc4"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"power": true,
|
||||
"global_auto": "Enabled",
|
||||
"enable_prebrewing": true,
|
||||
"coffee_boiler_on": true,
|
||||
"steam_boiler_on": true,
|
||||
"enable_preinfusion": false,
|
||||
"steam_boiler_enable": true,
|
||||
"steam_temp": 113,
|
||||
"steam_set_temp": 128,
|
||||
"coffee_temp": 93,
|
||||
"coffee_set_temp": 95,
|
||||
"water_reservoir_contact": true,
|
||||
"brew_active": false,
|
||||
"drinks_k1": 13,
|
||||
"drinks_k2": 2,
|
||||
"drinks_k3": 42,
|
||||
"drinks_k4": 34,
|
||||
"total_flushing": 69,
|
||||
"mon_auto": "Disabled",
|
||||
"mon_on_time": "00:00",
|
||||
"mon_off_time": "00:00",
|
||||
"tue_auto": "Disabled",
|
||||
"tue_on_time": "00:00",
|
||||
"tue_off_time": "00:00",
|
||||
"wed_auto": "Disabled",
|
||||
"wed_on_time": "00:00",
|
||||
"wed_off_time": "00:00",
|
||||
"thu_auto": "Disabled",
|
||||
"thu_on_time": "00:00",
|
||||
"thu_off_time": "00:00",
|
||||
"fri_auto": "Disabled",
|
||||
"fri_on_time": "00:00",
|
||||
"fri_off_time": "00:00",
|
||||
"sat_auto": "Disabled",
|
||||
"sat_on_time": "00:00",
|
||||
"sat_off_time": "00:00",
|
||||
"sun_auto": "Disabled",
|
||||
"sun_on_time": "00:00",
|
||||
"sun_off_time": "00:00",
|
||||
"dose_k1": 1023,
|
||||
"dose_k2": 1023,
|
||||
"dose_k3": 1023,
|
||||
"dose_k4": 1023,
|
||||
"dose_k5": 1023,
|
||||
"prebrewing_ton_k1": 3,
|
||||
"prebrewing_toff_k1": 5,
|
||||
"prebrewing_ton_k2": 3,
|
||||
"prebrewing_toff_k2": 5,
|
||||
"prebrewing_ton_k3": 3,
|
||||
"prebrewing_toff_k3": 5,
|
||||
"prebrewing_ton_k4": 3,
|
||||
"prebrewing_toff_k4": 5,
|
||||
"prebrewing_ton_k5": 3,
|
||||
"prebrewing_toff_k5": 5,
|
||||
"preinfusion_k1": 4,
|
||||
"preinfusion_k2": 4,
|
||||
"preinfusion_k3": 4,
|
||||
"preinfusion_k4": 4,
|
||||
"preinfusion_k5": 4
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"count": 1047,
|
||||
"coffeeType": 0
|
||||
},
|
||||
{
|
||||
"count": 560,
|
||||
"coffeeType": 1
|
||||
},
|
||||
{
|
||||
"count": 468,
|
||||
"coffeeType": 2
|
||||
},
|
||||
{
|
||||
"count": 312,
|
||||
"coffeeType": 3
|
||||
},
|
||||
{
|
||||
"count": 2252,
|
||||
"coffeeType": 4
|
||||
},
|
||||
{
|
||||
"coffeeType": -1,
|
||||
"count": 1740
|
||||
}
|
||||
]
|
|
@ -0,0 +1,161 @@
|
|||
# serializer version: 1
|
||||
# name: test_device
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'lamarzocco',
|
||||
'GS01234',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'manufacturer': 'La Marzocco',
|
||||
'model': 'GS3 AV',
|
||||
'name': 'GS01234',
|
||||
'name_by_user': None,
|
||||
'serial_number': 'GS01234',
|
||||
'suggested_area': None,
|
||||
'sw_version': '1.1',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[-set_power]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'GS01234',
|
||||
'icon': 'mdi:power',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.gs01234',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[-set_power].1
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.gs01234',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:power',
|
||||
'original_name': None,
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'GS01234_main',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[_auto_on_off-set_auto_on_off_global]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'GS01234 Auto on/off',
|
||||
'icon': 'mdi:alarm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.gs01234_auto_on_off',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[_auto_on_off-set_auto_on_off_global].1
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'switch.gs01234_auto_on_off',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:alarm',
|
||||
'original_name': 'Auto on/off',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'auto_on_off',
|
||||
'unique_id': 'GS01234_auto_on_off',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[_steam_boiler-set_steam]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'GS01234 Steam boiler',
|
||||
'icon': 'mdi:water-boiler',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.gs01234_steam_boiler',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switches[_steam_boiler-set_steam].1
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.gs01234_steam_boiler',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:water-boiler',
|
||||
'original_name': 'Steam boiler',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'steam_boiler',
|
||||
'unique_id': 'GS01234_steam_boiler_enable',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
|
@ -0,0 +1,235 @@
|
|||
"""Test the La Marzocco config flow."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult, FlowResultType
|
||||
|
||||
from . import PASSWORD_SELECTION, USER_INPUT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def __do_successful_user_step(
|
||||
hass: HomeAssistant, result: FlowResult
|
||||
) -> FlowResult:
|
||||
"""Successfully configure the user step."""
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "machine_selection"
|
||||
return result2
|
||||
|
||||
|
||||
async def __do_sucessful_machine_selection_step(
|
||||
hass: HomeAssistant, result2: FlowResult, mock_lamarzocco: MagicMock
|
||||
) -> None:
|
||||
"""Successfully configure the machine selection step."""
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_MACHINE: mock_lamarzocco.serial_number,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert result3["title"] == mock_lamarzocco.serial_number
|
||||
assert result3["data"] == {
|
||||
**USER_INPUT,
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_MACHINE: mock_lamarzocco.serial_number,
|
||||
}
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant, mock_lamarzocco: MagicMock) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result2 = await __do_successful_user_step(hass, result)
|
||||
await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco)
|
||||
|
||||
assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_abort_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we abort if already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == 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"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "machine_selection"
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_MACHINE: mock_lamarzocco.serial_number,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.ABORT
|
||||
assert result3["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_form_invalid_auth(
|
||||
hass: HomeAssistant, mock_lamarzocco: MagicMock
|
||||
) -> None:
|
||||
"""Test invalid auth error."""
|
||||
|
||||
mock_lamarzocco.get_all_machines.side_effect = AuthFail("")
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1
|
||||
|
||||
# test recovery from failure
|
||||
mock_lamarzocco.get_all_machines.side_effect = None
|
||||
result2 = await __do_successful_user_step(hass, result)
|
||||
await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco)
|
||||
|
||||
|
||||
async def test_form_invalid_host(
|
||||
hass: HomeAssistant, mock_lamarzocco: MagicMock
|
||||
) -> None:
|
||||
"""Test invalid auth error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_lamarzocco.check_local_connection.return_value = False
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "machine_selection"
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_MACHINE: mock_lamarzocco.serial_number,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.FORM
|
||||
assert result3["errors"] == {"host": "cannot_connect"}
|
||||
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1
|
||||
|
||||
# test recovery from failure
|
||||
mock_lamarzocco.check_local_connection.return_value = True
|
||||
await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco)
|
||||
|
||||
|
||||
async def test_form_cannot_connect(
|
||||
hass: HomeAssistant, mock_lamarzocco: MagicMock
|
||||
) -> None:
|
||||
"""Test cannot connect error."""
|
||||
|
||||
mock_lamarzocco.get_all_machines.return_value = []
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "no_machines"}
|
||||
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1
|
||||
|
||||
mock_lamarzocco.get_all_machines.side_effect = RequestNotSuccessful("")
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2
|
||||
|
||||
# test recovery from failure
|
||||
mock_lamarzocco.get_all_machines.side_effect = None
|
||||
mock_lamarzocco.get_all_machines.return_value = [
|
||||
(mock_lamarzocco.serial_number, mock_lamarzocco.model_name)
|
||||
]
|
||||
result2 = await __do_successful_user_step(hass, result)
|
||||
await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco)
|
||||
|
||||
|
||||
async def test_reauth_flow(
|
||||
hass: HomeAssistant, mock_lamarzocco: MagicMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that the reauth flow."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"unique_id": mock_config_entry.unique_id,
|
||||
"entry_id": mock_config_entry.entry_id,
|
||||
},
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
PASSWORD_SELECTION,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.ABORT
|
||||
await hass.async_block_till_done()
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1
|
|
@ -0,0 +1,70 @@
|
|||
"""Test initialization of lamarzocco."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from lmcloud.exceptions import AuthFail, RequestNotSuccessful
|
||||
|
||||
from homeassistant.components.lamarzocco.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
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."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lamarzocco: MagicMock,
|
||||
) -> None:
|
||||
"""Test the La Marzocco configuration entry not ready."""
|
||||
mock_lamarzocco.update_local_machine_status.side_effect = RequestNotSuccessful("")
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_lamarzocco: MagicMock,
|
||||
) -> None:
|
||||
"""Test auth error during setup."""
|
||||
mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("")
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow.get("step_id") == "user"
|
||||
assert flow.get("handler") == DOMAIN
|
||||
|
||||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == mock_config_entry.entry_id
|
|
@ -0,0 +1,91 @@
|
|||
"""Tests for La Marzocco switches."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_name", "method_name"),
|
||||
[
|
||||
("", "set_power"),
|
||||
("_auto_on_off", "set_auto_on_off_global"),
|
||||
("_steam_boiler", "set_steam"),
|
||||
],
|
||||
)
|
||||
async def test_switches(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_name: str,
|
||||
method_name: str,
|
||||
) -> None:
|
||||
"""Test the La Marzocco switches."""
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
|
||||
control_fn = getattr(mock_lamarzocco, method_name)
|
||||
|
||||
state = hass.states.get(f"switch.{serial_number}{entity_name}")
|
||||
assert state
|
||||
assert state == snapshot
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
||||
assert entry == snapshot
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"switch.{serial_number}{entity_name}",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(control_fn.mock_calls) == 1
|
||||
control_fn.assert_called_once_with(False)
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"switch.{serial_number}{entity_name}",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(control_fn.mock_calls) == 2
|
||||
control_fn.assert_called_with(True)
|
||||
|
||||
|
||||
async def test_device(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the device for one switch."""
|
||||
|
||||
state = hass.states.get(f"switch.{mock_lamarzocco.serial_number}")
|
||||
assert state
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
||||
assert entry.device_id
|
||||
|
||||
device = device_registry.async_get(entry.device_id)
|
||||
assert device
|
||||
assert device == snapshot
|
Loading…
Reference in New Issue