1
0
mirror of https://github.com/home-assistant/core.git synced 2026-06-06 15:36:51 +01:00
Files
core/homeassistant/components/nut/config_flow.py
T
2026-04-30 21:14:48 +02:00

356 lines
13 KiB
Python

"""Config flow for Network UPS Tools (NUT) integration."""
from collections.abc import Mapping
import logging
from typing import Any
from aionut import NUTError, NUTLoginError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_ALIAS,
CONF_BASE,
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from . import PyNUTData, _unique_id_from_status
from .const import DEFAULT_HOST, DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
REAUTH_SCHEMA = {vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str}
PASSWORD_NOT_CHANGED = "__**password_not_changed**__"
def _base_schema(
nut_config: Mapping[str, Any],
use_password_not_changed: bool = False,
) -> vol.Schema:
"""Generate base schema."""
base_schema = {
vol.Optional(CONF_HOST, default=nut_config.get(CONF_HOST) or DEFAULT_HOST): str,
vol.Optional(CONF_PORT, default=nut_config.get(CONF_PORT) or DEFAULT_PORT): int,
vol.Optional(
CONF_USERNAME, default=nut_config.get(CONF_USERNAME, vol.UNDEFINED)
): str,
vol.Optional(
CONF_PASSWORD,
default=PASSWORD_NOT_CHANGED if use_password_not_changed else vol.UNDEFINED,
): str,
}
return vol.Schema(base_schema)
def _ups_schema(ups_list: dict[str, str]) -> vol.Schema:
"""UPS selection schema."""
return vol.Schema({vol.Required(CONF_ALIAS): vol.In(ups_list)})
async def validate_input(data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from _base_schema with values provided by the user.
"""
host = data[CONF_HOST]
port = data[CONF_PORT]
alias = data.get(CONF_ALIAS)
username = data.get(CONF_USERNAME)
password = data.get(CONF_PASSWORD)
nut_data = PyNUTData(host, port, alias, username, password, persistent=False)
status = await nut_data.async_update()
if not alias and not nut_data.ups_list:
raise AbortFlow("no_ups_found")
return {"ups_list": nut_data.ups_list, "available_resources": status}
def _check_host_port_alias_match(
first: Mapping[str, Any], second: Mapping[str, Any]
) -> bool:
"""Check if first and second have the same host, port and alias."""
if first[CONF_HOST] != second[CONF_HOST] or first[CONF_PORT] != second[CONF_PORT]:
return False
first_alias = first.get(CONF_ALIAS)
second_alias = second.get(CONF_ALIAS)
if (first_alias is None and second_alias is None) or (
first_alias is not None
and second_alias is not None
and first_alias == second_alias
):
return True
return False
def _format_host_port_alias(user_input: Mapping[str, Any]) -> str:
"""Format a host, port, and alias so it can be used for comparison or display."""
host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
alias = user_input.get(CONF_ALIAS)
if alias:
return f"{alias}@{host}:{port}"
return f"{host}:{port}"
class NutConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Network UPS Tools (NUT)."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the nut config flow."""
self.nut_config: dict[str, Any] = {}
self.ups_list: dict[str, str] | None = None
self.title: str | None = None
self.reauth_entry: ConfigEntry | None = None
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Prepare configuration for a discovered nut device."""
await self._async_handle_discovery_without_unique_id()
self.nut_config = {
CONF_HOST: discovery_info.host or DEFAULT_HOST,
CONF_PORT: discovery_info.port or DEFAULT_PORT,
}
self.context["title_placeholders"] = self.nut_config.copy()
return await self.async_step_user()
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user input."""
errors: dict[str, str] = {}
placeholders: dict[str, str] = {}
nut_config = self.nut_config
if user_input is not None:
nut_config.update(user_input)
info, errors, placeholders = await self._async_validate_or_error(nut_config)
if not errors:
if len(info["ups_list"]) > 1:
self.ups_list = info["ups_list"]
return await self.async_step_ups()
if self._host_port_alias_already_configured(nut_config):
return self.async_abort(reason="already_configured")
if unique_id := _unique_id_from_status(info["available_resources"]):
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
title = _format_host_port_alias(nut_config)
return self.async_create_entry(title=title, data=nut_config)
return self.async_show_form(
step_id="user",
data_schema=_base_schema(nut_config),
errors=errors,
description_placeholders=placeholders,
)
async def async_step_ups(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle selecting the NUT device alias."""
errors: dict[str, str] = {}
placeholders: dict[str, str] = {}
nut_config = self.nut_config
if user_input is not None:
self.nut_config.update(user_input)
if self._host_port_alias_already_configured(nut_config):
return self.async_abort(reason="already_configured")
info, errors, placeholders = await self._async_validate_or_error(nut_config)
if not errors:
if unique_id := _unique_id_from_status(info["available_resources"]):
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
title = _format_host_port_alias(nut_config)
return self.async_create_entry(title=title, data=nut_config)
return self.async_show_form(
step_id="ups",
data_schema=_ups_schema(self.ups_list or {}),
errors=errors,
description_placeholders=placeholders,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
errors: dict[str, str] = {}
placeholders: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
nut_config = self.nut_config
if user_input is not None:
nut_config.update(user_input)
info, errors, placeholders = await self._async_validate_or_error(nut_config)
if not errors:
if len(info["ups_list"]) > 1:
self.ups_list = info["ups_list"]
return await self.async_step_reconfigure_ups()
if not _check_host_port_alias_match(
reconfigure_entry.data,
nut_config,
) and (self._host_port_alias_already_configured(nut_config)):
return self.async_abort(reason="already_configured")
if unique_id := _unique_id_from_status(info["available_resources"]):
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_mismatch(reason="unique_id_mismatch")
if nut_config[CONF_PASSWORD] == PASSWORD_NOT_CHANGED:
nut_config.pop(CONF_PASSWORD)
new_title = _format_host_port_alias(nut_config)
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
unique_id=unique_id,
title=new_title,
data_updates=nut_config,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=_base_schema(
reconfigure_entry.data,
use_password_not_changed=True,
),
errors=errors,
description_placeholders=placeholders,
)
async def async_step_reconfigure_ups(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle selecting the NUT device alias."""
errors: dict[str, str] = {}
placeholders: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
nut_config = self.nut_config
if user_input is not None:
self.nut_config.update(user_input)
if not _check_host_port_alias_match(
reconfigure_entry.data,
nut_config,
) and (self._host_port_alias_already_configured(nut_config)):
return self.async_abort(reason="already_configured")
info, errors, placeholders = await self._async_validate_or_error(nut_config)
if not errors:
if unique_id := _unique_id_from_status(info["available_resources"]):
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_mismatch(reason="unique_id_mismatch")
if nut_config[CONF_PASSWORD] == PASSWORD_NOT_CHANGED:
nut_config.pop(CONF_PASSWORD)
new_title = _format_host_port_alias(nut_config)
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
unique_id=unique_id,
title=new_title,
data_updates=nut_config,
)
return self.async_show_form(
step_id="reconfigure_ups",
data_schema=_ups_schema(self.ups_list or {}),
errors=errors,
description_placeholders=placeholders,
)
def _host_port_alias_already_configured(self, user_input: dict[str, Any]) -> bool:
"""See if we already have a nut entry matching user input configured."""
existing_host_port_aliases = {
_format_host_port_alias(entry.data)
for entry in self._async_current_entries()
if CONF_HOST in entry.data
}
return _format_host_port_alias(user_input) in existing_host_port_aliases
async def _async_validate_or_error(
self, config: dict[str, Any]
) -> tuple[dict[str, Any], dict[str, str], dict[str, str]]:
errors: dict[str, str] = {}
info: dict[str, Any] = {}
description_placeholders: dict[str, str] = {}
try:
info = await validate_input(config)
except NUTLoginError:
errors[CONF_PASSWORD] = "invalid_auth"
except NUTError as ex:
errors[CONF_BASE] = "cannot_connect"
description_placeholders["error"] = str(ex)
except AbortFlow:
raise
except Exception:
_LOGGER.exception("Unexpected exception")
errors[CONF_BASE] = "unknown"
return info, errors, description_placeholders
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth input."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
reauth_data = reauth_entry.data
description_placeholders: dict[str, str] = {
CONF_HOST: reauth_data[CONF_HOST],
CONF_PORT: reauth_data[CONF_PORT],
}
if user_input is not None:
new_config = {
**reauth_data,
# Username/password are optional and some servers
# use ip based authentication and will fail if
# username/password are provided
CONF_USERNAME: user_input.get(CONF_USERNAME),
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
}
_, errors, placeholders = await self._async_validate_or_error(new_config)
if not errors:
return self.async_update_reload_and_abort(reauth_entry, data=new_config)
description_placeholders.update(placeholders)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(REAUTH_SCHEMA),
errors=errors,
description_placeholders=description_placeholders,
)