hass-core/homeassistant/components/system_bridge/config_flow.py
Aidan Timson 4c67670566
Update System Bridge to support version 4.x.x and above (#107957)
* Update System Bridge to support version 4.x.x and above

Update systembridgeconnector to version 4.0.0.dev4

Update system_bridgeconnector version to 4.0.0.dev6

Refactor WebSocket client handling in config_flow.py

Update strings

Update data handling

Add default field values to SystemBridgeCoordinatorData

Add version check and issue creation for unsupported System Bridge versions

Update coordinator.py to set disks and memory to None

Update system bridge coordinator to use token instead of API key

Update systembridgeconnector version to 4.0.0.dev7

Update systembridgeconnector version to 4.0.0.dev8

Update systembridgeconnector version to 4.0.0.dev9

Changes

Update units

Fix GPU memory calculation in sensor.py

Update GPU memory unit of measurement

Add translation keys for binary sensor names

Cleanup

Add async_migrate_entry function for entry migration

Update systembridgeconnector version to 4.0.0.dev10

Update systembridgeconnector version to 4.0.0.dev11

Add version check and authentication handling

Update token description in strings.json

Fix skipping partitions without data in system_bridge sensor

Update systembridgeconnector version to 4.0.0.dev12

Update systembridgeconnector version to 4.0.0

Add check for unsupported version of System Bridge

Update systembridgeconnector version to 4.0.1

Update debug log message in async_setup_entry function

Remove debug log statement

Fixes

Update key to token

Update tests

Update tests

Remove unused import in test_config_flow.py

Remove added missing translations for another PR

Refactor CPU power per CPU calculation

Make one liner into lambda

Refactors

Fix exception type in async_setup_entry function

Move checks to class and set minor version

Remove unnecessary comment in gpu_memory_free function

Remove translation_key for memory_used_percentage sensor

Reverse string change

Update token placeholder in strings.json

Remove suggested_display_precision from sensor descriptions

Remove suggested_display_precision from GPU sensor setup

Refactor sensor code

* Update migrate entry

* Refactor GPU-related functions to use a decorator

* Move per cpu functions to use decorator

* Refactor functions to use decorators for data availability

* Remove CONF_API_KEY from config entry data

* Add test for migration

* Refactor import statement in test_config_flow.py
2024-03-04 11:14:46 +01:00

306 lines
10 KiB
Python

"""Config flow for System Bridge integration."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
from systembridgeconnector.exceptions import (
AuthenticationException,
ConnectionClosedException,
ConnectionErrorException,
)
from systembridgeconnector.websocket_client import WebSocketClient
from systembridgemodels.modules import GetData, System
import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_AUTHENTICATE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string})
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=9170): cv.string,
vol.Required(CONF_TOKEN): cv.string,
}
)
async def _validate_input(
hass: HomeAssistant,
data: dict[str, Any],
) -> dict[str, str]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
host = data[CONF_HOST]
websocket_client = WebSocketClient(
host,
data[CONF_PORT],
data[CONF_API_KEY],
)
try:
async with asyncio.timeout(15):
await websocket_client.connect(session=async_get_clientsession(hass))
hass.async_create_task(websocket_client.listen())
response = await websocket_client.get_data(GetData(modules=["system"]))
_LOGGER.debug("Got response: %s", response.json())
if response.data is None or not isinstance(response.data, System):
raise CannotConnect("No data received")
system: System = response.data
except AuthenticationException as exception:
_LOGGER.warning(
"Authentication error when connecting to %s: %s", data[CONF_HOST], exception
)
raise InvalidAuth from exception
except (
ConnectionClosedException,
ConnectionErrorException,
) as exception:
_LOGGER.warning(
"Connection error when connecting to %s: %s", data[CONF_HOST], exception
)
raise CannotConnect from exception
except TimeoutError as exception:
_LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception)
raise CannotConnect from exception
except ValueError as exception:
raise CannotConnect from exception
_LOGGER.debug("Got System data: %s", system.json())
return {"hostname": host, "uuid": system.uuid}
async def _async_get_info(
hass: HomeAssistant,
user_input: dict[str, Any],
) -> tuple[dict[str, str], dict[str, str] | None]:
errors = {}
try:
info = await _validate_input(hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return errors, info
return errors, None
class SystemBridgeConfigFlow(
ConfigFlow,
domain=DOMAIN,
):
"""Handle a config flow for System Bridge."""
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize flow."""
self._name: str | None = None
self._input: dict[str, Any] = {}
self._reauth = False
self._system_data: System | None = None
async def _validate_input(
self,
data: dict[str, Any],
) -> dict[str, str]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
host = data[CONF_HOST]
websocket_client = WebSocketClient(
host,
data[CONF_PORT],
data[CONF_TOKEN],
)
async def async_handle_module(
module_name: str,
module: Any,
) -> None:
"""Handle data from the WebSocket client."""
_LOGGER.debug("Set new data for: %s", module_name)
if module_name == "system":
self._system_data = module
try:
async with asyncio.timeout(15):
await websocket_client.connect(
session=async_get_clientsession(self.hass)
)
self.hass.async_create_task(
websocket_client.listen(callback=async_handle_module)
)
response = await websocket_client.get_data(GetData(modules=["system"]))
_LOGGER.debug("Got response: %s", response)
if response is None:
raise CannotConnect("No data received")
while self._system_data is None:
await asyncio.sleep(0.2)
except AuthenticationException as exception:
_LOGGER.warning(
"Authentication error when connecting to %s: %s",
data[CONF_HOST],
exception,
)
raise InvalidAuth from exception
except (
ConnectionClosedException,
ConnectionErrorException,
) as exception:
_LOGGER.warning(
"Connection error when connecting to %s: %s", data[CONF_HOST], exception
)
raise CannotConnect from exception
except TimeoutError as exception:
_LOGGER.warning(
"Timed out connecting to %s: %s", data[CONF_HOST], exception
)
raise CannotConnect from exception
except ValueError as exception:
raise CannotConnect from exception
_LOGGER.debug("Got System data: %s", self._system_data)
return {"hostname": host, "uuid": self._system_data.uuid}
async def _async_get_info(
self,
user_input: dict[str, Any],
) -> tuple[dict[str, str], dict[str, str] | None]:
errors = {}
try:
info = await self._validate_input(user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return errors, info
return errors, None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors, info = await self._async_get_info(user_input)
if not errors and info is not None:
# Check if already configured
await self.async_set_unique_id(info["uuid"], raise_on_progress=False)
self._abort_if_unique_id_configured(updates={CONF_HOST: info["hostname"]})
return self.async_create_entry(title=info["hostname"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_authenticate(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle getting the api-key for authentication."""
errors: dict[str, str] = {}
if user_input is not None:
user_input = {**self._input, **user_input}
errors, info = await self._async_get_info(user_input)
if not errors and info is not None:
# Check if already configured
existing_entry = await self.async_set_unique_id(info["uuid"])
if self._reauth and existing_entry:
self.hass.config_entries.async_update_entry(
existing_entry, data=user_input
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
self._abort_if_unique_id_configured(
updates={CONF_HOST: info["hostname"]}
)
return self.async_create_entry(title=info["hostname"], data=user_input)
return self.async_show_form(
step_id="authenticate",
data_schema=STEP_AUTHENTICATE_DATA_SCHEMA,
description_placeholders={"name": self._name},
errors=errors,
)
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
properties = discovery_info.properties
host = properties.get("ip")
uuid = properties.get("uuid")
if host is None or uuid is None:
return self.async_abort(reason="unknown")
# Check if already configured
await self.async_set_unique_id(uuid)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._name = host
self._input = {
CONF_HOST: host,
CONF_PORT: properties.get("port"),
}
return await self.async_step_authenticate()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
self._name = entry_data[CONF_HOST]
self._input = {
CONF_HOST: entry_data[CONF_HOST],
CONF_PORT: entry_data[CONF_PORT],
}
self._reauth = True
return await self.async_step_authenticate()
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""