core/homeassistant/components/lamarzocco/config_flow.py

410 lines
14 KiB
Python

"""Config flow for La Marzocco integration."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from aiohttp import ClientSession
from pylamarzocco.clients.cloud import LaMarzoccoCloudClient
from pylamarzocco.clients.local import LaMarzoccoLocalClient
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.models import LaMarzoccoDeviceInfo
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfo,
async_discovered_service_info,
)
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import (
CONF_ADDRESS,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PASSWORD,
CONF_TOKEN,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import LaMarzoccoConfigEntry
CONF_MACHINE = "machine"
_LOGGER = logging.getLogger(__name__)
class LmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for La Marzocco."""
VERSION = 2
_client: ClientSession
def __init__(self) -> None:
"""Initialize the config flow."""
self._config: dict[str, Any] = {}
self._fleet: dict[str, LaMarzoccoDeviceInfo] = {}
self._discovered: dict[str, str] = {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input:
data: dict[str, Any] = {}
if self.source == SOURCE_REAUTH:
data = dict(self._get_reauth_entry().data)
data = {
**data,
**user_input,
**self._discovered,
}
self._client = async_create_clientsession(self.hass)
cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
client=self._client,
)
try:
self._fleet = await cloud_client.get_customer_fleet()
except AuthFail:
_LOGGER.debug("Server rejected login credentials")
errors["base"] = "invalid_auth"
except RequestNotSuccessful as exc:
_LOGGER.error("Error connecting to server: %s", exc)
errors["base"] = "cannot_connect"
else:
if not self._fleet:
errors["base"] = "no_machines"
if not errors:
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=data
)
if self._discovered:
if self._discovered[CONF_MACHINE] not in self._fleet:
errors["base"] = "machine_not_found"
else:
self._config = data
# if DHCP discovery was used, auto fill machine selection
if CONF_HOST in self._discovered:
return await self.async_step_machine_selection(
user_input={
CONF_HOST: self._discovered[CONF_HOST],
CONF_MACHINE: self._discovered[CONF_MACHINE],
}
)
# if Bluetooth discovery was used, only select host
return self.async_show_form(
step_id="machine_selection",
data_schema=vol.Schema(
{vol.Optional(CONF_HOST): cv.string}
),
)
if not errors:
self._config = data
return await self.async_step_machine_selection()
placeholders: dict[str, str] | None = None
if self._discovered:
self.context["title_placeholders"] = placeholders = {
CONF_NAME: self._discovered[CONF_MACHINE]
}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME): TextSelector(
TextSelectorConfig(
type=TextSelectorType.EMAIL, autocomplete="username"
)
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
}
),
errors=errors,
description_placeholders=placeholders,
)
async def async_step_machine_selection(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Let user select machine to connect to."""
errors: dict[str, str] = {}
if user_input:
if not self._discovered:
serial_number = user_input[CONF_MACHINE]
if self.source != SOURCE_RECONFIGURE:
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured()
else:
serial_number = self._discovered[CONF_MACHINE]
selected_device = self._fleet[serial_number]
# validate local connection if host is provided
if user_input.get(CONF_HOST):
if not await LaMarzoccoLocalClient.validate_connection(
client=self._client,
host=user_input[CONF_HOST],
token=selected_device.communication_key,
):
errors[CONF_HOST] = "cannot_connect"
else:
self._config[CONF_HOST] = user_input[CONF_HOST]
if not errors:
if self.source == SOURCE_RECONFIGURE:
for service_info in async_discovered_service_info(self.hass):
self._discovered[service_info.name] = service_info.address
if self._discovered:
return await self.async_step_bluetooth_selection()
return self.async_create_entry(
title=selected_device.name,
data={
**self._config,
CONF_NAME: selected_device.name,
CONF_MODEL: selected_device.model,
CONF_TOKEN: selected_device.communication_key,
},
)
machine_options = [
SelectOptionDict(
value=device.serial_number,
label=f"{device.model} ({device.serial_number})",
)
for device in self._fleet.values()
]
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_bluetooth_selection(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle Bluetooth device selection."""
if user_input is not None:
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data={
**self._config,
CONF_MAC: user_input[CONF_MAC],
},
)
bt_options = [
SelectOptionDict(
value=device_mac,
label=f"{device_name} ({device_mac})",
)
for device_name, device_mac in self._discovered.items()
]
return self.async_show_form(
step_id="bluetooth_selection",
data_schema=vol.Schema(
{
vol.Required(CONF_MAC): SelectSelector(
SelectSelectorConfig(
options=bt_options,
mode=SelectSelectorMode.DROPDOWN,
)
),
},
),
)
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_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle discovery via dhcp."""
serial = discovery_info.hostname.upper()
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured(
updates={
CONF_HOST: discovery_info.ip,
CONF_ADDRESS: discovery_info.macaddress,
}
)
self._async_abort_entries_match({CONF_ADDRESS: discovery_info.macaddress})
_LOGGER.debug(
"Discovered La Marzocco machine %s through DHCP at address %s",
discovery_info.hostname,
discovery_info.ip,
)
self._discovered[CONF_MACHINE] = serial
self._discovered[CONF_HOST] = discovery_info.ip
self._discovered[CONF_ADDRESS] = discovery_info.macaddress
return await self.async_step_user()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if not user_input:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
),
)
return await self.async_step_user(user_input)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Perform reconfiguration of the config entry."""
if not user_input:
reconfigure_entry = self._get_reconfigure_entry()
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(
CONF_USERNAME, default=reconfigure_entry.data[CONF_USERNAME]
): TextSelector(
TextSelectorConfig(
type=TextSelectorType.EMAIL, autocomplete="username"
),
),
vol.Required(
CONF_PASSWORD, default=reconfigure_entry.data[CONF_PASSWORD]
): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
),
),
}
),
)
return await self.async_step_user(user_input)
@staticmethod
@callback
def async_get_options_flow(
config_entry: LaMarzoccoConfigEntry,
) -> LmOptionsFlowHandler:
"""Create the options flow."""
return LmOptionsFlowHandler()
class LmOptionsFlowHandler(OptionsFlow):
"""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.config_entry.options.get(CONF_USE_BLUETOOTH, True),
): cv.boolean,
}
)
return self.async_show_form(
step_id="init",
data_schema=options_schema,
)