mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
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>
This commit is contained in:
parent
a874895a81
commit
6bc36666b1
@ -248,6 +248,7 @@ homeassistant.components.knx.*
|
|||||||
homeassistant.components.kraken.*
|
homeassistant.components.kraken.*
|
||||||
homeassistant.components.lacrosse.*
|
homeassistant.components.lacrosse.*
|
||||||
homeassistant.components.lacrosse_view.*
|
homeassistant.components.lacrosse_view.*
|
||||||
|
homeassistant.components.lamarzocco.*
|
||||||
homeassistant.components.lametric.*
|
homeassistant.components.lametric.*
|
||||||
homeassistant.components.laundrify.*
|
homeassistant.components.laundrify.*
|
||||||
homeassistant.components.lawn_mower.*
|
homeassistant.components.lawn_mower.*
|
||||||
|
@ -688,6 +688,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/kulersky/ @emlove
|
/tests/components/kulersky/ @emlove
|
||||||
/homeassistant/components/lacrosse_view/ @IceBotYT
|
/homeassistant/components/lacrosse_view/ @IceBotYT
|
||||||
/tests/components/lacrosse_view/ @IceBotYT
|
/tests/components/lacrosse_view/ @IceBotYT
|
||||||
|
/homeassistant/components/lamarzocco/ @zweckj
|
||||||
|
/tests/components/lamarzocco/ @zweckj
|
||||||
/homeassistant/components/lametric/ @robbiet480 @frenck @bachya
|
/homeassistant/components/lametric/ @robbiet480 @frenck @bachya
|
||||||
/tests/components/lametric/ @robbiet480 @frenck @bachya
|
/tests/components/lametric/ @robbiet480 @frenck @bachya
|
||||||
/homeassistant/components/landisgyr_heat_meter/ @vpathuis
|
/homeassistant/components/landisgyr_heat_meter/ @vpathuis
|
||||||
|
36
homeassistant/components/lamarzocco/__init__.py
Normal file
36
homeassistant/components/lamarzocco/__init__.py
Normal file
@ -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
|
156
homeassistant/components/lamarzocco/config_flow.py
Normal file
156
homeassistant/components/lamarzocco/config_flow.py
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
7
homeassistant/components/lamarzocco/const.py
Normal file
7
homeassistant/components/lamarzocco/const.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""Constants for the La Marzocco integration."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
DOMAIN: Final = "lamarzocco"
|
||||||
|
|
||||||
|
CONF_MACHINE: Final = "machine"
|
96
homeassistant/components/lamarzocco/coordinator.py
Normal file
96
homeassistant/components/lamarzocco/coordinator.py
Normal file
@ -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
|
50
homeassistant/components/lamarzocco/entity.py
Normal file
50
homeassistant/components/lamarzocco/entity.py
Normal file
@ -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,
|
||||||
|
)
|
11
homeassistant/components/lamarzocco/manifest.json
Normal file
11
homeassistant/components/lamarzocco/manifest.json
Normal file
@ -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"]
|
||||||
|
}
|
48
homeassistant/components/lamarzocco/strings.json
Normal file
48
homeassistant/components/lamarzocco/strings.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
homeassistant/components/lamarzocco/switch.py
Normal file
92
homeassistant/components/lamarzocco/switch.py
Normal file
@ -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",
|
"kraken",
|
||||||
"kulersky",
|
"kulersky",
|
||||||
"lacrosse_view",
|
"lacrosse_view",
|
||||||
|
"lamarzocco",
|
||||||
"lametric",
|
"lametric",
|
||||||
"landisgyr_heat_meter",
|
"landisgyr_heat_meter",
|
||||||
"lastfm",
|
"lastfm",
|
||||||
|
@ -2999,6 +2999,12 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
"lamarzocco": {
|
||||||
|
"name": "La Marzocco",
|
||||||
|
"integration_type": "device",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"lametric": {
|
"lametric": {
|
||||||
"name": "LaMetric",
|
"name": "LaMetric",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
|
10
mypy.ini
10
mypy.ini
@ -2241,6 +2241,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.lametric.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -1209,6 +1209,9 @@ linear-garage-door==0.2.7
|
|||||||
# homeassistant.components.linode
|
# homeassistant.components.linode
|
||||||
linode-api==4.1.9b1
|
linode-api==4.1.9b1
|
||||||
|
|
||||||
|
# homeassistant.components.lamarzocco
|
||||||
|
lmcloud==0.4.34
|
||||||
|
|
||||||
# homeassistant.components.google_maps
|
# homeassistant.components.google_maps
|
||||||
locationsharinglib==5.0.1
|
locationsharinglib==5.0.1
|
||||||
|
|
||||||
|
@ -954,6 +954,9 @@ libsoundtouch==0.8
|
|||||||
# homeassistant.components.linear_garage_door
|
# homeassistant.components.linear_garage_door
|
||||||
linear-garage-door==0.2.7
|
linear-garage-door==0.2.7
|
||||||
|
|
||||||
|
# homeassistant.components.lamarzocco
|
||||||
|
lmcloud==0.4.34
|
||||||
|
|
||||||
# homeassistant.components.logi_circle
|
# homeassistant.components.logi_circle
|
||||||
logi-circle==0.2.3
|
logi-circle==0.2.3
|
||||||
|
|
||||||
|
25
tests/components/lamarzocco/__init__.py
Normal file
25
tests/components/lamarzocco/__init__.py
Normal file
@ -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()
|
104
tests/components/lamarzocco/conftest.py
Normal file
104
tests/components/lamarzocco/conftest.py
Normal file
@ -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
|
187
tests/components/lamarzocco/fixtures/config.json
Normal file
187
tests/components/lamarzocco/fixtures/config.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
61
tests/components/lamarzocco/fixtures/current_status.json
Normal file
61
tests/components/lamarzocco/fixtures/current_status.json
Normal file
@ -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
|
||||||
|
}
|
26
tests/components/lamarzocco/fixtures/statistics.json
Normal file
26
tests/components/lamarzocco/fixtures/statistics.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
161
tests/components/lamarzocco/snapshots/test_switch.ambr
Normal file
161
tests/components/lamarzocco/snapshots/test_switch.ambr
Normal file
@ -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,
|
||||||
|
})
|
||||||
|
# ---
|
235
tests/components/lamarzocco/test_config_flow.py
Normal file
235
tests/components/lamarzocco/test_config_flow.py
Normal file
@ -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
|
70
tests/components/lamarzocco/test_init.py
Normal file
70
tests/components/lamarzocco/test_init.py
Normal file
@ -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
|
91
tests/components/lamarzocco/test_switch.py
Normal file
91
tests/components/lamarzocco/test_switch.py
Normal file
@ -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…
x
Reference in New Issue
Block a user