Add Bluetooth support to La Marzocco integration (#108287)

* init

* init tests

* linting

* checks

* tests, linting

* pylint

* add tests

* switch tests

* add water heater tests

* change icons

* extra args cleanup

* moar tests

* services tests

* remove extra platforms

* test for unique id

* back to single instance

* add diagnostics

* remove extra platforms

* test for unique id

* back to single instance

* Add better connection management for Idasen Desk (#102135)

* Return 'None' for light attributes when off instead of removing them (#101946)

* Bump home-assistant-bluetooth to 1.10.4 (#102268)

* Bump orjson to 3.9.9 (#102267)

* Bump opower to 0.0.37 (#102265)

* Bump Python-Roborock to 0.35.0 (#102275)

* Add CodeQL CI Job (#102273)

* Remove unused dsmr sensors (#102223)

* rebase messed up conftest

* more tests for init

* add client to coveragerc

* add client to coveragerc

* next lmcloud version

* strict typing

* more typing

* allow multiple machines

* remove unneeded var

* Update homeassistant/components/lamarzocco/coordinator.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/diagnostics.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/__init__.py

Co-authored-by: Robert Resch <robert@resch.dev>

* PR suggestions

* remove base exception

* Update manifest.json

* update lmcloud

* update lmcloud

* remove ignore

* selection bugfix for machines with space in name

* bugfix temps

* add options flow

* send out full user input

* remove options flow

* split the tests to avoid timeouts

* use selectoptionsdict for selection

* removing rccoleman

* improve test coverage to 100%

* Update config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* autoselect cloud machine for discovered machine

* move default values to 3rd party lib

* bring property changes from lmcloud

* moving things to lmcloud

* move validation to method

* move more things to lmcloud

* remove unused const

* Update homeassistant/components/lamarzocco/coordinator.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/coordinator.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/__init__.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/__init__.py

Co-authored-by: Robert Resch <robert@resch.dev>

* remove callback from coordinator

* remove waterheater, add switch

* improvement to background task

* next lmcloud

* adapt to lib changes

* Update homeassistant/components/lamarzocco/strings.json

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/entity.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/entity.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/switch.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/switch.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/entity.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/strings.json

Co-authored-by: Robert Resch <robert@resch.dev>

* requested changes

* Update homeassistant/components/lamarzocco/switch.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/lamarzocco/entity.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update tests/components/lamarzocco/test_config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update tests/components/lamarzocco/test_config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* some requested changes

* changes

* requested changes

* move steam boiler to controls

* fix: remove entities from GS3MP model + tests

* remove dataclass decorator

* next lmcloud version

* improvements

* move reauth to user step

* improve config flow

* remove asserts in favor of runtimeerrors

* undo conftest comment

* make duc return none

* Update homeassistant/components/lamarzocco/switch.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/lamarzocco/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/lamarzocco/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* remove diagnostics, changes

* refine config flow

* remove runtimeerrors in favor of asserts

* move initialization of lm_client to coordinator

* remove things from lmclient

* remove lm_client

* remove lm_client

* bump lm version

* correctly set initialized for tests

* move exception handling inside init + tests

* add test for switch without bluetooth on

* bump lmcloud

* pass httpx client to LMLocalAPI

* add call function to reduce code

* switch to snapshot testing

* remove bluetooth

* bump version

* cleanup import

* remove unused const

* set correct integration_type

* correct default selection in CF

* reduce unnecessary tests by fixture change

* use other json loads helpers

* move prebrew/infusion to select entity

* bump lmcloud

* Update coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* requested feedback

* step description, bump lmcloud

* create init integration functino

* revert

* ruff

* remove leftover BT test

* make main switch main entity

* bump lmcloud

* re-add bluetooth

* improve

* bump firmware (again)

* correct test

* Update homeassistant/components/lamarzocco/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/lamarzocco/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/lamarzocco/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* separate device test

* add BT to entites

* fix import

* docstring

* minor

* fix rebase

* get device from discovered devices

* tweak

* change tests

* switch to dict

* switch to options

* fix

* fix

---------

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>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Josef Zweck 2024-03-21 13:08:11 +01:00 committed by GitHub
parent e23943debf
commit e5fa6f0176
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 633 additions and 59 deletions

View File

@ -29,6 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
await hass.config_entries.async_reload(entry.entry_id)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True

View File

@ -8,8 +8,22 @@ from lmcloud import LMCloud as LaMarzoccoClient
from lmcloud.exceptions import AuthFail, RequestNotSuccessful from lmcloud.exceptions import AuthFail, RequestNotSuccessful
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.components.bluetooth import BluetoothServiceInfo
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
SelectOptionDict, SelectOptionDict,
@ -18,7 +32,7 @@ from homeassistant.helpers.selector import (
SelectSelectorMode, SelectSelectorMode,
) )
from .const import CONF_MACHINE, DOMAIN from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,6 +46,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
self.reauth_entry: ConfigEntry | None = None self.reauth_entry: ConfigEntry | None = None
self._config: dict[str, Any] = {} self._config: dict[str, Any] = {}
self._machines: list[tuple[str, str]] = [] self._machines: list[tuple[str, str]] = []
self._discovered: dict[str, str] = {}
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -47,6 +62,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
data = { data = {
**data, **data,
**user_input, **user_input,
**self._discovered,
} }
lm = LaMarzoccoClient() lm = LaMarzoccoClient()
@ -71,6 +87,18 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
self.reauth_entry.entry_id self.reauth_entry.entry_id
) )
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
if self._discovered:
serials = [machine[0] for machine in self._machines]
if self._discovered[CONF_MACHINE] not in serials:
errors["base"] = "machine_not_found"
else:
self._config = data
return self.async_show_form(
step_id="machine_selection",
data_schema=vol.Schema(
{vol.Optional(CONF_HOST): cv.string}
),
)
if not errors: if not errors:
self._config = data self._config = data
@ -93,9 +121,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Let user select machine to connect to.""" """Let user select machine to connect to."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input: if user_input:
if not self._discovered:
serial_number = user_input[CONF_MACHINE] serial_number = user_input[CONF_MACHINE]
await self.async_set_unique_id(serial_number) await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
else:
serial_number = self._discovered[CONF_MACHINE]
# validate local connection if host is provided # validate local connection if host is provided
if user_input.get(CONF_HOST): if user_input.get(CONF_HOST):
@ -141,6 +172,30 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery over Bluetooth."""
address = discovery_info.address
name = discovery_info.name
_LOGGER.debug(
"Discovered La Marzocco machine %s through Bluetooth at address %s",
name,
address,
)
self._discovered[CONF_NAME] = name
self._discovered[CONF_MAC] = address
serial = name.split("_")[1]
self._discovered[CONF_MACHINE] = serial
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()
return await self.async_step_user()
async def async_step_reauth( async def async_step_reauth(
self, entry_data: Mapping[str, Any] self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@ -165,3 +220,36 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
) )
return await self.async_step_user(user_input) return await self.async_step_user(user_input)
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
return LmOptionsFlowHandler(config_entry)
class LmOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handles options flow for the component."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options for the custom component."""
if user_input:
return self.async_create_entry(title="", data=user_input)
options_schema = vol.Schema(
{
vol.Optional(
CONF_USE_BLUETOOTH,
default=self.options.get(CONF_USE_BLUETOOTH, True),
): cv.boolean,
}
)
return self.async_show_form(
step_id="init",
data_schema=options_schema,
)

View File

@ -5,3 +5,5 @@ from typing import Final
DOMAIN: Final = "lamarzocco" DOMAIN: Final = "lamarzocco"
CONF_MACHINE: Final = "machine" CONF_MACHINE: Final = "machine"
CONF_USE_BLUETOOTH = "use_bluetooth"

View File

@ -5,23 +5,32 @@ from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from bleak.backends.device import BLEDevice
from lmcloud import LMCloud as LaMarzoccoClient from lmcloud import LMCloud as LaMarzoccoClient
from lmcloud.const import BT_MODEL_NAMES
from lmcloud.exceptions import AuthFail, RequestNotSuccessful from lmcloud.exceptions import AuthFail, RequestNotSuccessful
from homeassistant.components.bluetooth import (
async_ble_device_from_address,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_MACHINE, DOMAIN from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NAME_PREFIXES = tuple(BT_MODEL_NAMES)
class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to handle fetching data from the La Marzocco API centrally.""" """Class to handle fetching data from the La Marzocco API centrally."""
@ -36,6 +45,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
self.local_connection_configured = ( self.local_connection_configured = (
self.config_entry.data.get(CONF_HOST) is not None self.config_entry.data.get(CONF_HOST) is not None
) )
self._use_bluetooth = False
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
@ -80,6 +90,46 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
name="lm_websocket_task", name="lm_websocket_task",
) )
# initialize Bluetooth
if self.config_entry.options.get(CONF_USE_BLUETOOTH, True):
def bluetooth_configured() -> bool:
return self.config_entry.data.get(
CONF_MAC, ""
) and self.config_entry.data.get(CONF_NAME, "")
if not bluetooth_configured():
machine = self.config_entry.data[CONF_MACHINE]
for discovery_info in async_discovered_service_info(self.hass):
if (
(name := discovery_info.name)
and name.startswith(NAME_PREFIXES)
and name.split("_")[1] == machine
):
_LOGGER.debug(
"Found Bluetooth device, configuring with Bluetooth"
)
# found a device, add MAC address to config entry
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
CONF_MAC: discovery_info.address,
CONF_NAME: discovery_info.name,
},
)
break
if bluetooth_configured():
# config entry contains BT config
_LOGGER.debug("Initializing with known Bluetooth device")
await self.lm.init_bluetooth_with_known_device(
self.config_entry.data[CONF_USERNAME],
self.config_entry.data.get(CONF_MAC, ""),
self.config_entry.data.get(CONF_NAME, ""),
)
self._use_bluetooth = True
self.lm.initialized = True self.lm.initialized = True
async def _async_handle_request( async def _async_handle_request(
@ -98,3 +148,15 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
except RequestNotSuccessful as ex: except RequestNotSuccessful as ex:
_LOGGER.debug(ex, exc_info=True) _LOGGER.debug(ex, exc_info=True)
raise UpdateFailed("Querying API failed. Error: %s" % ex) from ex raise UpdateFailed("Querying API failed. Error: %s" % ex) from ex
def async_get_ble_device(self) -> BLEDevice | None:
"""Get a Bleak Client for the machine."""
# according to HA best practices, we should not reuse the same client
# get a new BLE device from hass and init a new Bleak Client with it
if not self._use_bluetooth:
return None
return async_ble_device_from_address(
self.hass,
self.lm.lm_bluetooth.address,
)

View File

@ -1,8 +1,23 @@
{ {
"domain": "lamarzocco", "domain": "lamarzocco",
"name": "La Marzocco", "name": "La Marzocco",
"bluetooth": [
{
"local_name": "MICRA_*"
},
{
"local_name": "MINI_*"
},
{
"local_name": "GS3_*"
},
{
"local_name": "GS3AV_*"
}
],
"codeowners": ["@zweckj"], "codeowners": ["@zweckj"],
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/lamarzocco", "documentation": "https://www.home-assistant.io/integrations/lamarzocco",
"integration_type": "device", "integration_type": "device",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

@ -63,7 +63,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_step=PRECISION_TENTHS, native_step=PRECISION_TENTHS,
native_min_value=85, native_min_value=85,
native_max_value=104, native_max_value=104,
set_value_fn=lambda coordinator, temp: coordinator.lm.set_coffee_temp(temp), set_value_fn=lambda coordinator, temp: coordinator.lm.set_coffee_temp(
temp, coordinator.async_get_ble_device()
),
native_value_fn=lambda lm: lm.current_status["coffee_set_temp"], native_value_fn=lambda lm: lm.current_status["coffee_set_temp"],
), ),
LaMarzoccoNumberEntityDescription( LaMarzoccoNumberEntityDescription(
@ -74,7 +76,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
native_step=PRECISION_WHOLE, native_step=PRECISION_WHOLE,
native_min_value=126, native_min_value=126,
native_max_value=131, native_max_value=131,
set_value_fn=lambda coordinator, temp: coordinator.lm.set_steam_temp(int(temp)), set_value_fn=lambda coordinator, temp: coordinator.lm.set_steam_temp(
int(temp), coordinator.async_get_ble_device()
),
native_value_fn=lambda lm: lm.current_status["steam_set_temp"], native_value_fn=lambda lm: lm.current_status["steam_set_temp"],
supported_fn=lambda coordinator: coordinator.lm.model_name supported_fn=lambda coordinator: coordinator.lm.model_name
in ( in (

View File

@ -36,7 +36,7 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
translation_key="steam_temp_select", translation_key="steam_temp_select",
options=["1", "2", "3"], options=["1", "2", "3"],
select_option_fn=lambda coordinator, option: coordinator.lm.set_steam_level( select_option_fn=lambda coordinator, option: coordinator.lm.set_steam_level(
int(option) int(option), coordinator.async_get_ble_device()
), ),
current_option_fn=lambda lm: lm.current_status["steam_level_set"], current_option_fn=lambda lm: lm.current_status["steam_level_set"],
supported_fn=lambda coordinator: coordinator.lm.model_name supported_fn=lambda coordinator: coordinator.lm.model_name

View File

@ -42,6 +42,16 @@
} }
} }
}, },
"options": {
"step": {
"init": {
"data": {
"title": "Update Configuration",
"use_bluetooth": "Use Bluetooth"
}
}
}
},
"entity": { "entity": {
"binary_sensor": { "binary_sensor": {
"brew_active": { "brew_active": {

View File

@ -31,7 +31,9 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
key="main", key="main",
translation_key="main", translation_key="main",
name=None, name=None,
control_fn=lambda coordinator, state: coordinator.lm.set_power(state), control_fn=lambda coordinator, state: coordinator.lm.set_power(
state, coordinator.async_get_ble_device()
),
is_on_fn=lambda coordinator: coordinator.lm.current_status["power"], is_on_fn=lambda coordinator: coordinator.lm.current_status["power"],
), ),
LaMarzoccoSwitchEntityDescription( LaMarzoccoSwitchEntityDescription(
@ -47,7 +49,9 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = (
LaMarzoccoSwitchEntityDescription( LaMarzoccoSwitchEntityDescription(
key="steam_boiler_enable", key="steam_boiler_enable",
translation_key="steam_boiler", translation_key="steam_boiler",
control_fn=lambda coordinator, state: coordinator.lm.set_steam(state), control_fn=lambda coordinator, state: coordinator.lm.set_steam(
state, coordinator.async_get_ble_device()
),
is_on_fn=lambda coordinator: coordinator.lm.current_status[ is_on_fn=lambda coordinator: coordinator.lm.current_status[
"steam_boiler_enable" "steam_boiler_enable"
], ],

View File

@ -266,6 +266,22 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
"domain": "keymitt_ble", "domain": "keymitt_ble",
"local_name": "mib*", "local_name": "mib*",
}, },
{
"domain": "lamarzocco",
"local_name": "MICRA_*",
},
{
"domain": "lamarzocco",
"local_name": "MINI_*",
},
{
"domain": "lamarzocco",
"local_name": "GS3_*",
},
{
"domain": "lamarzocco",
"local_name": "GS3AV_*",
},
{ {
"domain": "ld2410_ble", "domain": "ld2410_ble",
"local_name": "HLK-LD2410B_*", "local_name": "HLK-LD2410B_*",

View File

@ -1,7 +1,10 @@
"""Mock inputs for tests.""" """Mock inputs for tests."""
from lmcloud.const import LaMarzoccoModel
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -15,6 +18,13 @@ PASSWORD_SELECTION = {
USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"} USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"}
MODEL_DICT = {
LaMarzoccoModel.GS3_AV: ("GS01234", "GS3 AV"),
LaMarzoccoModel.GS3_MP: ("GS01234", "GS3 MP"),
LaMarzoccoModel.LINEA_MICRA: ("MR01234", "Linea Micra"),
LaMarzoccoModel.LINEA_MINI: ("LM01234", "Linea Mini"),
}
async def async_init_integration( async def async_init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry hass: HomeAssistant, mock_config_entry: MockConfigEntry
@ -22,3 +32,24 @@ async def async_init_integration(
"""Set up the La Marzocco integration for testing.""" """Set up the La Marzocco integration for testing."""
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
def get_bluetooth_service_info(
model: LaMarzoccoModel, serial: str
) -> BluetoothServiceInfo:
"""Return a mocked BluetoothServiceInfo."""
if model in (LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP):
name = f"GS3_{serial}"
elif model == LaMarzoccoModel.LINEA_MINI:
name = f"MINI_{serial}"
elif model == LaMarzoccoModel.LINEA_MICRA:
name = f"MICRA_{serial}"
return BluetoothServiceInfo(
name=name,
address="aa:bb:cc:dd:ee:ff",
rssi=-63,
manufacturer_data={},
service_data={},
service_uuids=[],
source="local",
)

View File

@ -7,10 +7,10 @@ from lmcloud.const import LaMarzoccoModel
import pytest import pytest
from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import USER_INPUT, async_init_integration from . import MODEL_DICT, USER_INPUT, async_init_integration
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
@ -28,7 +28,12 @@ def mock_config_entry(
title="My LaMarzocco", title="My LaMarzocco",
domain=DOMAIN, domain=DOMAIN,
data=USER_INPUT data=USER_INPUT
| {CONF_MACHINE: mock_lamarzocco.serial_number, CONF_HOST: "host"}, | {
CONF_MACHINE: mock_lamarzocco.serial_number,
CONF_HOST: "host",
CONF_NAME: "name",
CONF_MAC: "mac",
},
unique_id=mock_lamarzocco.serial_number, unique_id=mock_lamarzocco.serial_number,
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -58,25 +63,17 @@ def mock_lamarzocco(
"""Return a mocked LM client.""" """Return a mocked LM client."""
model_name = device_fixture model_name = device_fixture
if model_name == LaMarzoccoModel.GS3_AV: (serial_number, true_model_name) = MODEL_DICT[model_name]
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( with (
patch(
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient", "homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient",
autospec=True, autospec=True,
) as lamarzocco_mock, patch( ) as lamarzocco_mock,
patch(
"homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient", "homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient",
new=lamarzocco_mock, new=lamarzocco_mock,
),
): ):
lamarzocco = lamarzocco_mock.return_value lamarzocco = lamarzocco_mock.return_value
@ -118,6 +115,9 @@ def mock_lamarzocco(
lamarzocco.lm_local_api.websocket_connect = websocket_connect_mock lamarzocco.lm_local_api.websocket_connect = websocket_connect_mock
lamarzocco.lm_bluetooth = MagicMock()
lamarzocco.lm_bluetooth.address = "AA:BB:CC:DD:EE:FF"
yield lamarzocco yield lamarzocco
@ -130,3 +130,8 @@ def remove_local_connection(
del data[CONF_HOST] del data[CONF_HOST]
hass.config_entries.async_update_entry(mock_config_entry, data=data) hass.config_entries.async_update_entry(mock_config_entry, data=data)
return mock_config_entry return mock_config_entry
@pytest.fixture(autouse=True)
def mock_bluetooth(enable_bluetooth):
"""Auto mock bluetooth."""

View File

@ -29,7 +29,7 @@
'via_device_id': None, 'via_device_id': None,
}) })
# --- # ---
# name: test_switches[-set_power] # name: test_switches[-set_power-args_on0-args_off0]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234', 'friendly_name': 'GS01234',
@ -42,7 +42,7 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_switches[-set_power].1 # name: test_switches[-set_power-args_on0-args_off0].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -75,7 +75,51 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_switches[_auto_on_off-set_auto_on_off_global] # name: test_switches[-set_power-kwargs_on0-kwargs_off0]
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-kwargs_on0-kwargs_off0].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-args_on1-args_off1]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Auto on/off', 'friendly_name': 'GS01234 Auto on/off',
@ -88,7 +132,7 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_switches[_auto_on_off-set_auto_on_off_global].1 # name: test_switches[_auto_on_off-set_auto_on_off_global-args_on1-args_off1].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -121,7 +165,51 @@
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_switches[_steam_boiler-set_steam] # name: test_switches[_auto_on_off-set_auto_on_off_global-kwargs_on1-kwargs_off1]
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-kwargs_on1-kwargs_off1].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-args_on2-args_off2]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'GS01234 Steam boiler', 'friendly_name': 'GS01234 Steam boiler',
@ -134,7 +222,53 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_switches[_steam_boiler-set_steam].1 # name: test_switches[_steam_boiler-set_steam-args_on2-args_off2].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>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'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,
})
# ---
# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2]
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-kwargs_on2-kwargs_off2].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),

View File

@ -5,13 +5,17 @@ from unittest.mock import MagicMock
from lmcloud.exceptions import AuthFail, RequestNotSuccessful from lmcloud.exceptions import AuthFail, RequestNotSuccessful
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN from homeassistant.components.lamarzocco.const import (
CONF_MACHINE,
CONF_USE_BLUETOOTH,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult, FlowResultType from homeassistant.data_entry_flow import FlowResult, FlowResultType
from . import USER_INPUT from . import USER_INPUT, async_init_integration, get_bluetooth_service_info
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -233,3 +237,131 @@ async def test_reauth_flow(
assert result2["reason"] == "reauth_successful" assert result2["reason"] == "reauth_successful"
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1
assert mock_config_entry.data[CONF_PASSWORD] == "new_password" assert mock_config_entry.data[CONF_PASSWORD] == "new_password"
async def test_bluetooth_discovery(
hass: HomeAssistant, mock_lamarzocco: MagicMock
) -> None:
"""Test bluetooth discovery."""
service_info = get_bluetooth_service_info(
mock_lamarzocco.model_name, mock_lamarzocco.serial_number
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=service_info
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result2["type"] == FlowResultType.FORM
assert result2["step_id"] == "machine_selection"
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_HOST: "192.168.1.1",
},
)
await hass.async_block_till_done()
assert result3["type"] == 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,
CONF_NAME: service_info.name,
CONF_MAC: "aa:bb:cc:dd:ee:ff",
}
assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1
async def test_bluetooth_discovery_errors(
hass: HomeAssistant, mock_lamarzocco: MagicMock
) -> None:
"""Test bluetooth discovery errors."""
service_info = get_bluetooth_service_info(
mock_lamarzocco.model_name, mock_lamarzocco.serial_number
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=service_info,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
mock_lamarzocco.get_all_machines.return_value = [("GS98765", "GS3 MP")]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "machine_not_found"}
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1
mock_lamarzocco.get_all_machines.return_value = [
(mock_lamarzocco.serial_number, mock_lamarzocco.model_name)
]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result2["type"] == FlowResultType.FORM
assert result2["step_id"] == "machine_selection"
assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_HOST: "192.168.1.1",
},
)
await hass.async_block_till_done()
assert result3["type"] == 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,
CONF_NAME: service_info.name,
CONF_MAC: "aa:bb:cc:dd:ee:ff",
}
async def test_options_flow(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test options flow."""
await async_init_integration(hass, mock_config_entry)
assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_USE_BLUETOOTH: False,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["data"] == {
CONF_USE_BLUETOOTH: False,
}

View File

@ -1,13 +1,16 @@
"""Test initialization of lamarzocco.""" """Test initialization of lamarzocco."""
from unittest.mock import MagicMock from unittest.mock import MagicMock, patch
from lmcloud.exceptions import AuthFail, RequestNotSuccessful from lmcloud.exceptions import AuthFail, RequestNotSuccessful
from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_MAC, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import async_init_integration, get_bluetooth_service_info
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -17,8 +20,7 @@ async def test_load_unload_config_entry(
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
) -> None: ) -> None:
"""Test loading and unloading the integration.""" """Test loading and unloading the integration."""
await hass.config_entries.async_setup(mock_config_entry.entry_id) await async_init_integration(hass, mock_config_entry)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_config_entry.state is ConfigEntryState.LOADED
@ -36,8 +38,7 @@ async def test_config_entry_not_ready(
"""Test the La Marzocco configuration entry not ready.""" """Test the La Marzocco configuration entry not ready."""
mock_lamarzocco.update_local_machine_status.side_effect = RequestNotSuccessful("") mock_lamarzocco.update_local_machine_status.side_effect = RequestNotSuccessful("")
await hass.config_entries.async_setup(mock_config_entry.entry_id) await async_init_integration(hass, mock_config_entry)
await hass.async_block_till_done()
assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@ -50,8 +51,7 @@ async def test_invalid_auth(
) -> None: ) -> None:
"""Test auth error during setup.""" """Test auth error during setup."""
mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("") mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("")
await hass.config_entries.async_setup(mock_config_entry.entry_id) await async_init_integration(hass, mock_config_entry)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1
@ -66,3 +66,29 @@ async def test_invalid_auth(
assert "context" in flow assert "context" in flow
assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("source") == SOURCE_REAUTH
assert flow["context"].get("entry_id") == mock_config_entry.entry_id assert flow["context"].get("entry_id") == mock_config_entry.entry_id
async def test_bluetooth_is_set_from_discovery(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
) -> None:
"""Assert we're not searching for a new BT device when we already found one previously."""
# remove the bluetooth configuration from entry
data = mock_config_entry.data.copy()
del data[CONF_NAME]
del data[CONF_MAC]
hass.config_entries.async_update_entry(mock_config_entry, data=data)
service_info = get_bluetooth_service_info(
mock_lamarzocco.model_name, mock_lamarzocco.serial_number
)
with patch(
"homeassistant.components.lamarzocco.coordinator.async_discovered_service_info",
return_value=[service_info],
):
await async_init_integration(hass, mock_config_entry)
mock_lamarzocco.init_bluetooth_with_known_device.assert_called_once()
assert mock_config_entry.data[CONF_NAME] == service_info.name
assert mock_config_entry.data[CONF_MAC] == service_info.address

View File

@ -53,7 +53,9 @@ async def test_coffee_boiler(
) )
assert len(mock_lamarzocco.set_coffee_temp.mock_calls) == 1 assert len(mock_lamarzocco.set_coffee_temp.mock_calls) == 1
mock_lamarzocco.set_coffee_temp.assert_called_once_with(temperature=95) mock_lamarzocco.set_coffee_temp.assert_called_once_with(
temperature=95, ble_device=None
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -62,7 +64,12 @@ async def test_coffee_boiler(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_name", "value", "func_name", "kwargs"), ("entity_name", "value", "func_name", "kwargs"),
[ [
("steam_target_temperature", 131, "set_steam_temp", {"temperature": 131}), (
"steam_target_temperature",
131,
"set_steam_temp",
{"temperature": 131, "ble_device": None},
),
("tea_water_duration", 15, "set_dose_hot_water", {"value": 15}), ("tea_water_duration", 15, "set_dose_hot_water", {"value": 15}),
], ],
) )

View File

@ -50,7 +50,7 @@ async def test_steam_boiler_level(
) )
assert len(mock_lamarzocco.set_steam_level.mock_calls) == 1 assert len(mock_lamarzocco.set_steam_level.mock_calls) == 1
mock_lamarzocco.set_steam_level.assert_called_once_with(level=1) mock_lamarzocco.set_steam_level.assert_called_once_with(1, None)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -5,6 +5,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.lamarzocco.const import DOMAIN
from homeassistant.components.switch import ( from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN, DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@ -14,15 +15,22 @@ from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("init_integration") pytestmark = pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_name", "method_name"), ("entity_name", "method_name", "args_on", "args_off"),
[ [
("", "set_power"), ("", "set_power", (True, None), (False, None)),
("_auto_on_off", "set_auto_on_off_global"), (
("_steam_boiler", "set_steam"), "_auto_on_off",
"set_auto_on_off_global",
(True,),
(False,),
),
("_steam_boiler", "set_steam", (True, None), (False, None)),
], ],
) )
async def test_switches( async def test_switches(
@ -32,6 +40,8 @@ async def test_switches(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_name: str, entity_name: str,
method_name: str, method_name: str,
args_on: tuple,
args_off: tuple,
) -> None: ) -> None:
"""Test the La Marzocco switches.""" """Test the La Marzocco switches."""
serial_number = mock_lamarzocco.serial_number serial_number = mock_lamarzocco.serial_number
@ -56,7 +66,7 @@ async def test_switches(
) )
assert len(control_fn.mock_calls) == 1 assert len(control_fn.mock_calls) == 1
control_fn.assert_called_once_with(False) control_fn.assert_called_once_with(*args_off)
await hass.services.async_call( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
@ -68,7 +78,7 @@ async def test_switches(
) )
assert len(control_fn.mock_calls) == 2 assert len(control_fn.mock_calls) == 2
control_fn.assert_called_with(True) control_fn.assert_called_with(*args_on)
async def test_device( async def test_device(
@ -90,3 +100,26 @@ async def test_device(
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device assert device
assert device == snapshot assert device == snapshot
async def test_call_without_bluetooth_works(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that if not using bluetooth, the switch still works."""
serial_number = mock_lamarzocco.serial_number
coordinator = hass.data[DOMAIN][mock_config_entry.entry_id]
coordinator._use_bluetooth = False
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: f"switch.{serial_number}_steam_boiler",
},
blocking=True,
)
assert len(mock_lamarzocco.set_steam.mock_calls) == 1
mock_lamarzocco.set_steam.assert_called_once_with(False, None)