* 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
306 lines
10 KiB
Python
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."""
|