core/homeassistant/components/lamarzocco/config_flow.py

389 lines
13 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 import LaMarzoccoCloudClient
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.models import Thing
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,
OptionsFlowWithReload,
)
from homeassistant.const import (
CONF_ADDRESS,
CONF_MAC,
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"
BT_MODEL_PREFIXES = ("MICRA", "MINI", "GS3")
_LOGGER = logging.getLogger(__name__)
class LmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for La Marzocco."""
VERSION = 3
_client: ClientSession
def __init__(self) -> None:
"""Initialize the config flow."""
self._config: dict[str, Any] = {}
self._things: dict[str, Thing] = {}
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._client = async_create_clientsession(self.hass)
cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
client=self._client,
)
try:
things = await cloud_client.list_things()
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:
self._things = {thing.serial_number: thing for thing in things}
if not self._things:
errors["base"] = "no_machines"
if not errors:
self._config = data
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=data
)
if self._discovered:
if self._discovered[CONF_MACHINE] not in self._things:
errors["base"] = "machine_not_found"
else:
# store discovered connection address
if CONF_MAC in self._discovered:
self._config[CONF_MAC] = self._discovered[CONF_MAC]
if CONF_ADDRESS in self._discovered:
self._config[CONF_ADDRESS] = self._discovered[CONF_ADDRESS]
return await self.async_step_machine_selection(
user_input={CONF_MACHINE: self._discovered[CONF_MACHINE]}
)
if not errors:
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._things[serial_number]
if not errors:
if self.source == SOURCE_RECONFIGURE:
for service_info in async_discovered_service_info(self.hass):
if service_info.name.startswith(BT_MODEL_PREFIXES):
self._discovered[service_info.name] = service_info.address
if self._discovered:
return await self.async_step_bluetooth_selection()
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data_updates=self._config,
)
return self.async_create_entry(
title=selected_device.name,
data={
**self._config,
CONF_TOKEN: self._things[serial_number].ble_auth_token,
},
)
machine_options = [
SelectOptionDict(
value=thing.serial_number,
label=f"{thing.name} ({thing.serial_number})",
)
for thing in self._things.values()
]
machine_selection_schema = vol.Schema(
{
vol.Required(
CONF_MACHINE, default=machine_options[0]["value"]
): SelectSelector(
SelectSelectorConfig(
options=machine_options,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
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_updates={
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_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_NAME] = discovery_info.hostname
self._discovered[CONF_MACHINE] = serial
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(OptionsFlowWithReload):
"""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,
)