Implement bridge-backed Home Assistant integration
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"""Config flow for MB-Secure."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .api import (
|
||||
BridgeAuthenticationError,
|
||||
BridgeClient,
|
||||
BridgeConnectionError,
|
||||
BridgeResponseError,
|
||||
BridgeVersionError,
|
||||
)
|
||||
from .const import (
|
||||
CONF_TOKEN,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
MIN_TOKEN_LENGTH,
|
||||
)
|
||||
from .models import BridgeInfo
|
||||
|
||||
|
||||
class MBSecureConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle MB-Secure configuration."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow state."""
|
||||
self._discovery_data: dict[str, Any] | None = None
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle manual setup."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
error, info = await self._async_validate(user_input)
|
||||
if error is None:
|
||||
assert info is not None
|
||||
if info.bridge_id is None:
|
||||
errors["base"] = "missing_bridge_id"
|
||||
return self._show_user_form(user_input, errors)
|
||||
await self.async_set_unique_id(info.bridge_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title="MB-Secure Bridge", data=user_input
|
||||
)
|
||||
else:
|
||||
errors["base"] = error
|
||||
|
||||
return self._show_user_form(user_input, errors)
|
||||
|
||||
def _show_user_form(
|
||||
self,
|
||||
user_input: dict[str, Any] | None,
|
||||
errors: dict[str, str],
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the manual configuration form."""
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_HOST,
|
||||
default=(user_input or {}).get(CONF_HOST, DEFAULT_HOST),
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_PORT,
|
||||
default=(user_input or {}).get(CONF_PORT, DEFAULT_PORT),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Required(CONF_TOKEN): vol.All(
|
||||
str, vol.Length(min=MIN_TOKEN_LENGTH)
|
||||
),
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
@override
|
||||
async def async_step_hassio(
|
||||
self, discovery_info: HassioServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle discovery from the MB-Secure Bridge app."""
|
||||
config = discovery_info.config
|
||||
if not all(key in config for key in (CONF_HOST, CONF_PORT, CONF_TOKEN)):
|
||||
return self.async_abort(reason="invalid_discovery")
|
||||
if not isinstance(config[CONF_HOST], str) or not isinstance(
|
||||
config[CONF_TOKEN], str
|
||||
):
|
||||
return self.async_abort(reason="invalid_discovery")
|
||||
if isinstance(config[CONF_PORT], bool):
|
||||
return self.async_abort(reason="invalid_discovery")
|
||||
try:
|
||||
host = config[CONF_HOST]
|
||||
port = int(config[CONF_PORT])
|
||||
token = config[CONF_TOKEN]
|
||||
except (TypeError, ValueError):
|
||||
return self.async_abort(reason="invalid_discovery")
|
||||
if not host or len(token) < MIN_TOKEN_LENGTH or not 1 <= port <= 65535:
|
||||
return self.async_abort(reason="invalid_discovery")
|
||||
self._discovery_data = {
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
CONF_TOKEN: token,
|
||||
}
|
||||
await self.async_set_unique_id(discovery_info.uuid)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host, CONF_PORT: port})
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
async def async_step_hassio_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm Supervisor discovery before creating an entry."""
|
||||
if self._discovery_data is None:
|
||||
return self.async_abort(reason="invalid_discovery")
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
error, info = await self._async_validate(self._discovery_data)
|
||||
if error is None:
|
||||
assert info is not None
|
||||
if info.bridge_id is None:
|
||||
errors["base"] = "missing_bridge_id"
|
||||
else:
|
||||
await self.async_set_unique_id(info.bridge_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self._discovery_data[CONF_HOST],
|
||||
CONF_PORT: self._discovery_data[CONF_PORT],
|
||||
}
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title="MB-Secure Bridge", data=self._discovery_data
|
||||
)
|
||||
else:
|
||||
errors["base"] = error
|
||||
return self.async_show_form(
|
||||
step_id="hassio_confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
errors=errors,
|
||||
description_placeholders={"host": self._discovery_data[CONF_HOST]},
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Start reauthentication."""
|
||||
self._discovery_data = dict(entry_data)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Replace a rejected local bridge token."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
candidate = {**reauth_entry.data, CONF_TOKEN: user_input[CONF_TOKEN]}
|
||||
error, _ = await self._async_validate(candidate)
|
||||
if error is None:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data_updates={CONF_TOKEN: user_input[CONF_TOKEN]},
|
||||
)
|
||||
errors["base"] = error
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TOKEN): vol.All(
|
||||
str, vol.Length(min=MIN_TOKEN_LENGTH)
|
||||
)
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _async_validate(
|
||||
self, data: dict[str, Any]
|
||||
) -> tuple[str | None, BridgeInfo | None]:
|
||||
try:
|
||||
client = BridgeClient(
|
||||
async_get_clientsession(self.hass),
|
||||
host=data[CONF_HOST],
|
||||
port=data[CONF_PORT],
|
||||
token=data[CONF_TOKEN],
|
||||
)
|
||||
info = await client.async_get_info()
|
||||
except BridgeAuthenticationError:
|
||||
return "invalid_auth", None
|
||||
except BridgeVersionError:
|
||||
return "unsupported_version", None
|
||||
except (BridgeConnectionError, BridgeResponseError, ValueError):
|
||||
return "cannot_connect", None
|
||||
return None, info
|
||||
Reference in New Issue
Block a user