1185 lines
33 KiB
Python
1185 lines
33 KiB
Python
"""AsusRouter config flow module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import socket
|
|
from typing import Any
|
|
|
|
from asusrouter import AsusData
|
|
from asusrouter.error import (
|
|
AsusRouterAccessError,
|
|
AsusRouterConnectionError,
|
|
AsusRouterTimeoutError,
|
|
)
|
|
from asusrouter.modules.endpoint.error import AccessError
|
|
from asusrouter.modules.homeassistant import convert_to_ha_sensors_group
|
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
|
from homeassistant.const import (
|
|
CONF_HOST,
|
|
CONF_PASSWORD,
|
|
CONF_PORT,
|
|
CONF_SCAN_INTERVAL,
|
|
CONF_SSL,
|
|
CONF_USERNAME,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.data_entry_flow import FlowResult
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.device_registry import format_mac
|
|
import voluptuous as vol
|
|
|
|
from .bridge import ARBridge
|
|
from .const import (
|
|
ACCESS_POINT,
|
|
ALL_CLIENTS,
|
|
BASE,
|
|
CONF_CACHE_TIME,
|
|
CONF_CLIENT_DEVICE,
|
|
CONF_CLIENT_FILTER,
|
|
CONF_CLIENT_FILTER_LIST,
|
|
CONF_CLIENTS_IN_ATTR,
|
|
CONF_CONSIDER_HOME,
|
|
CONF_CREATE_DEVICES,
|
|
CONF_DEFAULT_CACHE_TIME,
|
|
CONF_DEFAULT_CLIENT_DEVICE,
|
|
CONF_DEFAULT_CLIENT_FILTER,
|
|
CONF_DEFAULT_CLIENTS_IN_ATTR,
|
|
CONF_DEFAULT_CONSIDER_HOME,
|
|
CONF_DEFAULT_CREATE_DEVICES,
|
|
CONF_DEFAULT_EVENT,
|
|
CONF_DEFAULT_HIDE_PASSWORDS,
|
|
CONF_DEFAULT_INTERFACES,
|
|
CONF_DEFAULT_INTERVALS,
|
|
CONF_DEFAULT_LATEST_CONNECTED,
|
|
CONF_DEFAULT_MODE,
|
|
CONF_DEFAULT_PORT,
|
|
CONF_DEFAULT_SCAN_INTERVAL,
|
|
CONF_DEFAULT_SPLIT_INTERVALS,
|
|
CONF_DEFAULT_SSL,
|
|
CONF_DEFAULT_TRACK_DEVICES,
|
|
CONF_DEFAULT_USERNAME,
|
|
CONF_HIDE_PASSWORDS,
|
|
CONF_INTERFACES,
|
|
CONF_INTERVAL,
|
|
CONF_INTERVAL_DEVICES,
|
|
CONF_INTERVALS,
|
|
CONF_LABELS_CLIENT_FILTER,
|
|
CONF_LABELS_INTERFACES,
|
|
CONF_LABELS_MODE,
|
|
CONF_LATEST_CONNECTED,
|
|
CONF_MODE,
|
|
CONF_SPLIT_INTERVALS,
|
|
CONF_TRACK_DEVICES,
|
|
CONF_VALUES_MODE,
|
|
CONFIGS,
|
|
DOMAIN,
|
|
ERRORS,
|
|
FIRMWARE,
|
|
INTERFACES,
|
|
MEDIA_BRIDGE,
|
|
METHOD,
|
|
NEXT,
|
|
RESULT_ACCESS_ERROR,
|
|
RESULT_CANNOT_RESOLVE,
|
|
RESULT_CONNECTION_ERROR,
|
|
RESULT_ERROR,
|
|
RESULT_LOGIN_BLOCKED,
|
|
RESULT_SUCCESS,
|
|
RESULT_TIMEOUT,
|
|
RESULT_UNKNOWN,
|
|
RESULT_WRONG_CREDENTIALS,
|
|
ROUTER,
|
|
STEP_CONNECTED_DEVICES,
|
|
STEP_CREDENTIALS,
|
|
STEP_EVENTS,
|
|
STEP_FIND,
|
|
STEP_FINISH,
|
|
STEP_INTERFACES,
|
|
STEP_INTERVALS,
|
|
STEP_OPERATION,
|
|
STEP_OPTIONS,
|
|
STEP_SECURITY,
|
|
UNIQUE_ID,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def _check_host(
|
|
host: str,
|
|
) -> str | None:
|
|
"""Get the IP address for the hostname."""
|
|
|
|
try:
|
|
return socket.gethostbyname(host)
|
|
except socket.gaierror:
|
|
return None
|
|
|
|
|
|
def _check_errors(
|
|
errors: dict[str, Any] | None = None,
|
|
) -> bool:
|
|
"""Check for errors."""
|
|
|
|
if errors is None:
|
|
return False
|
|
|
|
return bool(
|
|
BASE in errors
|
|
and errors[BASE] != RESULT_SUCCESS
|
|
and errors[BASE] != ""
|
|
)
|
|
|
|
|
|
async def _async_get_clients(
|
|
hass: HomeAssistant,
|
|
configs: dict[str, Any],
|
|
options: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
"""Return list of all the known clients."""
|
|
|
|
bridge = ARBridge(hass, configs, options)
|
|
|
|
try:
|
|
if not bridge.connected:
|
|
await bridge.async_connect()
|
|
clients = await bridge.api.async_get_data(AsusData.CLIENTS)
|
|
await bridge.async_disconnect()
|
|
|
|
return {
|
|
format_mac(mac): client.description.name
|
|
for mac, client in clients.items()
|
|
if client.description.name
|
|
}
|
|
except Exception as ex: # noqa: BLE001
|
|
_LOGGER.warning(
|
|
"Cannot get clients for %s: %s", configs[CONF_HOST], ex
|
|
)
|
|
return {}
|
|
|
|
|
|
async def _async_get_network_interfaces(
|
|
hass: HomeAssistant,
|
|
configs: dict[str, Any],
|
|
options: dict[str, Any],
|
|
) -> list[str]:
|
|
"""Return list of possible to monitor network interfaces."""
|
|
|
|
bridge = ARBridge(hass, configs, options)
|
|
|
|
try:
|
|
if not bridge.connected:
|
|
await bridge.async_connect()
|
|
labels = convert_to_ha_sensors_group(
|
|
await bridge.api.async_get_data(AsusData.NETWORK)
|
|
)
|
|
await bridge.async_disconnect()
|
|
_LOGGER.debug("Found network interfaces: %s", labels)
|
|
return labels
|
|
except Exception as ex: # noqa: BLE001
|
|
_LOGGER.warning(
|
|
"Cannot get available network interfaces for %s: %s",
|
|
configs[CONF_HOST],
|
|
ex,
|
|
)
|
|
return CONF_DEFAULT_INTERFACES
|
|
|
|
|
|
async def _async_check_connection( # noqa: C901, PLR0911, PLR0912
|
|
hass: HomeAssistant,
|
|
configs: dict[str, Any],
|
|
options: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Check connection to the device with provided configurations."""
|
|
|
|
configs_to_use = configs.copy()
|
|
if options:
|
|
configs_to_use.update(options)
|
|
if CONF_HOST not in configs_to_use:
|
|
return {
|
|
ERRORS: RESULT_ERROR,
|
|
}
|
|
host = configs_to_use[CONF_HOST]
|
|
|
|
result = {}
|
|
_LOGGER.debug("Setup initiated")
|
|
|
|
# Initialize bridge
|
|
bridge = ARBridge(hass, configs_to_use)
|
|
|
|
# Connect
|
|
try:
|
|
await bridge.async_connect()
|
|
# Access error
|
|
except AsusRouterAccessError as ex:
|
|
args = ex.args
|
|
# Wrong credentials
|
|
if args[1] == AccessError.CREDENTIALS:
|
|
_LOGGER.error(
|
|
"Error during connection to `%s`. Wrong credentials", host
|
|
)
|
|
return {
|
|
ERRORS: RESULT_WRONG_CREDENTIALS,
|
|
}
|
|
# Try again later / too many attempts
|
|
if args[1] == AccessError.TRY_AGAIN:
|
|
timeout = args[2].get("timeout")
|
|
_LOGGER.error(
|
|
"Device `%s` has reported block for the login "
|
|
"(to many wrong attempts were made). Please try again "
|
|
"in `%s` seconds",
|
|
host,
|
|
timeout,
|
|
)
|
|
return {
|
|
ERRORS: RESULT_LOGIN_BLOCKED,
|
|
}
|
|
# Reset required
|
|
if args[1] == AccessError.RESET_REQUIRED:
|
|
_LOGGER.error(
|
|
"Device `%s` requires a reset. Please reset the device. "
|
|
"You won't be able to login to the device until the reset "
|
|
"is done.",
|
|
host,
|
|
)
|
|
return {
|
|
ERRORS: RESULT_LOGIN_BLOCKED,
|
|
}
|
|
# Captcha required
|
|
if args[1] == AccessError.CAPTCHA:
|
|
_LOGGER.error(
|
|
"Device `%s` requires a captcha. Please login to the device "
|
|
"and complete the captcha. Integration cannot proceed with "
|
|
"the captcha request. You need either to disable captcha or "
|
|
"login via Web UI from the HA IP address, complete it and try "
|
|
"again. Sometimes, you can also reboot the device to fix this "
|
|
"issue.",
|
|
host,
|
|
)
|
|
return {
|
|
ERRORS: RESULT_LOGIN_BLOCKED,
|
|
}
|
|
# Another error
|
|
if args[1] == AccessError.ANOTHER:
|
|
_LOGGER.error("Device `%s` has reported `another` error.", host)
|
|
return {
|
|
ERRORS: RESULT_ERROR,
|
|
}
|
|
# Unknown error
|
|
if args[1] == AccessError.UNKNOWN:
|
|
_LOGGER.error("Device `%s` has reported `unknown` error.", host)
|
|
return {
|
|
ERRORS: RESULT_UNKNOWN,
|
|
}
|
|
# Anything else
|
|
_LOGGER.error(
|
|
"Error during connection to `%s`. Original exception: %s", host, ex
|
|
)
|
|
return {
|
|
ERRORS: RESULT_ACCESS_ERROR,
|
|
}
|
|
|
|
# Timeout error
|
|
except AsusRouterTimeoutError as ex:
|
|
_LOGGER.error(
|
|
"Timeout error during connection to `%s`: Original exception: %s",
|
|
host,
|
|
ex,
|
|
)
|
|
return {
|
|
ERRORS: RESULT_TIMEOUT,
|
|
}
|
|
|
|
# Connection error
|
|
except AsusRouterConnectionError as ex:
|
|
_LOGGER.error(
|
|
"Connection error during connection to `%s`: "
|
|
"Original exception: %s",
|
|
host,
|
|
ex,
|
|
)
|
|
return {
|
|
ERRORS: RESULT_CONNECTION_ERROR,
|
|
}
|
|
|
|
# Anything else
|
|
except Exception as ex: # noqa: BLE001
|
|
_LOGGER.error(
|
|
"Unknown error of type '%s' during connection to `%s`: %s",
|
|
type(ex),
|
|
host,
|
|
ex,
|
|
)
|
|
return {
|
|
ERRORS: RESULT_UNKNOWN,
|
|
}
|
|
# Cleanup, so no unclosed sessions will be reported
|
|
finally:
|
|
await bridge.async_clean()
|
|
|
|
# Serial number of the device is the best unique_id
|
|
# API provides it all the time for all the devices.
|
|
# MAC as an alternative might not be used / found on some
|
|
# older devices and some Merlin-builds of FW
|
|
result[UNIQUE_ID] = bridge.identity.serial
|
|
await bridge.async_disconnect()
|
|
for item in configs:
|
|
configs_to_use.pop(item)
|
|
|
|
result[CONFIGS] = configs_to_use
|
|
|
|
_LOGGER.debug("Setup successful")
|
|
|
|
return result
|
|
|
|
|
|
async def _async_process_step(
|
|
steps: dict[str, dict[str, Any]],
|
|
step: str | None = None,
|
|
errors: dict[str, Any] | None = None,
|
|
redirect: bool = False,
|
|
) -> FlowResult:
|
|
"""Universal step selector.
|
|
|
|
When the name of the last step is provided, the next step is initialized.
|
|
On errors the same step will repeat.
|
|
"""
|
|
|
|
if step and step in steps:
|
|
# Method description
|
|
description = steps[step]
|
|
# On errors or redirect, run the step method
|
|
if _check_errors(errors) or redirect:
|
|
if METHOD in description:
|
|
return await description[METHOD]()
|
|
raise ValueError(f"Step `{step}` is not properly defined")
|
|
# If the next step is defined, move to it
|
|
if NEXT in description and description[NEXT]:
|
|
return await _async_process_step(
|
|
steps, description[NEXT], redirect=True
|
|
)
|
|
raise ValueError(f"Step `{step}` is not properly defined")
|
|
raise ValueError(f"Step `{step}` cannot be found")
|
|
|
|
|
|
# FORMS ->
|
|
|
|
|
|
def _create_form_find(
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> vol.Schema:
|
|
"""Create a form for the 'find' step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
schema = {
|
|
vol.Required(
|
|
CONF_HOST, default=user_input.get(CONF_HOST, "")
|
|
): cv.string,
|
|
}
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
def _create_form_credentials(
|
|
user_input: dict[str, Any] | None = None,
|
|
mode: str = CONF_DEFAULT_MODE,
|
|
) -> vol.Schema:
|
|
"""Create a form for the 'credentials' step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
schema = {
|
|
vol.Required(
|
|
CONF_USERNAME,
|
|
default=user_input.get(CONF_USERNAME, CONF_DEFAULT_USERNAME),
|
|
): cv.string,
|
|
vol.Required(
|
|
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
|
|
): cv.string,
|
|
vol.Optional(
|
|
CONF_PORT, default=user_input.get(CONF_PORT, CONF_DEFAULT_PORT)
|
|
): cv.positive_int,
|
|
vol.Optional(
|
|
CONF_SSL, default=user_input.get(CONF_SSL, CONF_DEFAULT_SSL)
|
|
): cv.boolean,
|
|
}
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
def _create_form_operation(
|
|
user_input: dict[str, Any] | None = None,
|
|
mode: str = CONF_DEFAULT_MODE,
|
|
) -> vol.Schema:
|
|
"""Create a form for the 'operation' step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
schema = {
|
|
vol.Required(
|
|
CONF_MODE, default=user_input.get(CONF_MODE, mode)
|
|
): vol.In(
|
|
{
|
|
mode: CONF_LABELS_MODE.get(mode, mode)
|
|
for mode in CONF_VALUES_MODE
|
|
}
|
|
),
|
|
vol.Required(
|
|
CONF_SPLIT_INTERVALS,
|
|
default=user_input.get(
|
|
CONF_SPLIT_INTERVALS, CONF_DEFAULT_SPLIT_INTERVALS
|
|
),
|
|
): cv.boolean,
|
|
}
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
def _create_form_connected_devices(
|
|
user_input: dict[str, Any] | None = None,
|
|
mode: str = CONF_DEFAULT_MODE,
|
|
default: list[str] | None = None,
|
|
) -> vol.Schema:
|
|
"""Create a form for the `connected_devices` step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
if not default:
|
|
default = []
|
|
|
|
schema = {
|
|
vol.Required(
|
|
CONF_TRACK_DEVICES,
|
|
default=user_input.get(
|
|
CONF_TRACK_DEVICES, CONF_DEFAULT_TRACK_DEVICES
|
|
),
|
|
): cv.boolean,
|
|
vol.Required(
|
|
CONF_CLIENT_DEVICE,
|
|
default=user_input.get(
|
|
CONF_CLIENT_DEVICE, CONF_DEFAULT_CLIENT_DEVICE
|
|
),
|
|
): cv.boolean,
|
|
vol.Required(
|
|
CONF_CLIENTS_IN_ATTR,
|
|
default=user_input.get(
|
|
CONF_CLIENTS_IN_ATTR, CONF_DEFAULT_CLIENTS_IN_ATTR
|
|
),
|
|
): cv.boolean,
|
|
vol.Required(
|
|
CONF_CLIENT_FILTER,
|
|
default=user_input.get(
|
|
CONF_CLIENT_FILTER, CONF_DEFAULT_CLIENT_FILTER
|
|
),
|
|
): vol.In(CONF_LABELS_CLIENT_FILTER),
|
|
vol.Optional(
|
|
CONF_CLIENT_FILTER_LIST,
|
|
default=default,
|
|
): cv.multi_select(
|
|
dict(
|
|
sorted(
|
|
user_input[ALL_CLIENTS].items(), key=lambda item: item[1]
|
|
)
|
|
)
|
|
),
|
|
vol.Required(
|
|
CONF_LATEST_CONNECTED,
|
|
default=user_input.get(
|
|
CONF_LATEST_CONNECTED, CONF_DEFAULT_LATEST_CONNECTED
|
|
),
|
|
): cv.positive_int,
|
|
vol.Required(
|
|
CONF_INTERVAL_DEVICES,
|
|
default=user_input.get(
|
|
CONF_INTERVAL_DEVICES, CONF_DEFAULT_SCAN_INTERVAL
|
|
),
|
|
): cv.positive_int,
|
|
}
|
|
|
|
if mode in (ACCESS_POINT, ROUTER):
|
|
schema.update(
|
|
{
|
|
vol.Required(
|
|
CONF_CONSIDER_HOME,
|
|
default=user_input.get(
|
|
CONF_CONSIDER_HOME, CONF_DEFAULT_CONSIDER_HOME
|
|
),
|
|
): cv.positive_int,
|
|
}
|
|
)
|
|
|
|
if mode in (ACCESS_POINT, MEDIA_BRIDGE, ROUTER):
|
|
schema.update(
|
|
{
|
|
vol.Required(
|
|
CONF_CREATE_DEVICES,
|
|
default=user_input.get(
|
|
CONF_CREATE_DEVICES, CONF_DEFAULT_CREATE_DEVICES
|
|
),
|
|
): cv.boolean,
|
|
}
|
|
)
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
def _create_form_intervals(
|
|
user_input: dict[str, Any] | None = None,
|
|
mode: str = CONF_DEFAULT_MODE,
|
|
) -> vol.Schema:
|
|
"""Create a form for the 'intervals' step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
schema = {
|
|
vol.Required(
|
|
CONF_CACHE_TIME,
|
|
default=user_input.get(CONF_CACHE_TIME, CONF_DEFAULT_CACHE_TIME),
|
|
): cv.positive_int,
|
|
}
|
|
|
|
split = user_input.get(CONF_SPLIT_INTERVALS, CONF_DEFAULT_SPLIT_INTERVALS)
|
|
conf_scan_interval = user_input.get(
|
|
CONF_SCAN_INTERVAL, CONF_DEFAULT_SCAN_INTERVAL
|
|
)
|
|
|
|
if split is False:
|
|
schema.update(
|
|
{
|
|
vol.Optional(
|
|
CONF_SCAN_INTERVAL,
|
|
default=conf_scan_interval,
|
|
): cv.positive_int,
|
|
vol.Optional(
|
|
CONF_INTERVAL + FIRMWARE,
|
|
default=user_input.get(
|
|
CONF_INTERVAL + FIRMWARE,
|
|
CONF_DEFAULT_INTERVALS[CONF_INTERVAL + FIRMWARE],
|
|
),
|
|
): cv.positive_int,
|
|
}
|
|
)
|
|
elif split is True:
|
|
schema.update(
|
|
{
|
|
vol.Required(
|
|
conf,
|
|
default=user_input.get(
|
|
conf,
|
|
CONF_DEFAULT_INTERVALS.get(conf, conf_scan_interval),
|
|
),
|
|
): cv.positive_int
|
|
for conf in CONF_INTERVALS
|
|
}
|
|
)
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
def _create_form_interfaces(
|
|
user_input: dict[str, Any] | None = None,
|
|
default: list[str] | None = None,
|
|
) -> vol.Schema:
|
|
"""Create a form for the 'interfaces' step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
if not default:
|
|
default = []
|
|
|
|
schema = {
|
|
vol.Optional(
|
|
CONF_INTERFACES,
|
|
default=default,
|
|
): cv.multi_select(
|
|
{
|
|
interface: CONF_LABELS_INTERFACES.get(interface, interface)
|
|
for interface in user_input[INTERFACES]
|
|
}
|
|
),
|
|
}
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
def _create_form_events(
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> vol.Schema:
|
|
"""Create a form for the `events` step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
schema = {
|
|
vol.Optional(
|
|
event,
|
|
default=user_input.get(event, default_state),
|
|
): cv.boolean
|
|
for event, default_state in CONF_DEFAULT_EVENT.items()
|
|
}
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
def _create_form_security(
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> vol.Schema:
|
|
"""Create a form for the `security` step."""
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
schema = {
|
|
vol.Required(
|
|
CONF_HIDE_PASSWORDS,
|
|
default=user_input.get(
|
|
CONF_HIDE_PASSWORDS, CONF_DEFAULT_HIDE_PASSWORDS
|
|
),
|
|
): cv.boolean,
|
|
}
|
|
|
|
return vol.Schema(schema)
|
|
|
|
|
|
# <- FORMS
|
|
|
|
|
|
class ARFlowHandler(ConfigFlow, domain=DOMAIN):
|
|
"""Handle config flow for AsusRouter."""
|
|
|
|
VERSION = 5
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialise config flow."""
|
|
|
|
self._configs: dict[str, Any] = {}
|
|
self._options: dict[str, Any] = {}
|
|
self._unique_id: str | None = None
|
|
self._mode = CONF_DEFAULT_MODE
|
|
self.description_placeholders: dict[str, Any] = {}
|
|
|
|
# Steps description
|
|
self._steps: dict[str, dict[str, Any]] = {
|
|
STEP_FIND: {METHOD: self.async_step_find, NEXT: STEP_CREDENTIALS},
|
|
STEP_CREDENTIALS: {
|
|
METHOD: self.async_step_credentials,
|
|
NEXT: STEP_OPERATION,
|
|
},
|
|
STEP_OPERATION: {
|
|
METHOD: self.async_step_operation,
|
|
NEXT: STEP_OPTIONS,
|
|
},
|
|
STEP_OPTIONS: {METHOD: self.async_step_options},
|
|
}
|
|
|
|
# User setup
|
|
async def async_step_user(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Flow initiated by user."""
|
|
|
|
return await self.async_step_find(user_input)
|
|
|
|
# Find the device
|
|
async def async_step_find(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Find the device step."""
|
|
|
|
step_id = STEP_FIND
|
|
|
|
errors = {}
|
|
|
|
if user_input:
|
|
# Check if host can be resolved
|
|
ip = await self.hass.async_add_executor_job(
|
|
_check_host, user_input[CONF_HOST]
|
|
)
|
|
if not ip:
|
|
errors[BASE] = RESULT_CANNOT_RESOLVE
|
|
|
|
if not errors:
|
|
# Save host to configs
|
|
self._configs.update(user_input)
|
|
# Proceed to the next step
|
|
return await _async_process_step(self._steps, step_id, errors)
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_find(user_input),
|
|
errors=errors,
|
|
)
|
|
|
|
# Credentials & connection
|
|
async def async_step_credentials(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Credentials step."""
|
|
|
|
step_id = STEP_CREDENTIALS
|
|
|
|
errors = {}
|
|
|
|
if user_input:
|
|
# Check credentials and connection
|
|
result = await _async_check_connection(
|
|
self.hass, self._configs, user_input
|
|
)
|
|
# Show errors if any
|
|
if ERRORS in result:
|
|
errors[BASE] = result[ERRORS]
|
|
else:
|
|
# Save the checked settings to the options
|
|
self._options.update(result[CONFIGS])
|
|
# Set unique ID obtained from the device during the check
|
|
await self.async_set_unique_id(result[UNIQUE_ID])
|
|
self._abort_if_unique_id_configured(
|
|
updates={CONF_HOST: self._configs[CONF_HOST]}
|
|
)
|
|
# Proceed to the next step
|
|
return await _async_process_step(self._steps, step_id)
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_credentials(user_input),
|
|
errors=errors,
|
|
)
|
|
|
|
# Operation settings
|
|
async def async_step_operation(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select operation settings."""
|
|
|
|
step_id = STEP_OPERATION
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_operation(user_input, self._mode),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
# Save the operation mode
|
|
self._mode = user_input.get(CONF_MODE, CONF_DEFAULT_MODE)
|
|
|
|
# Proceed to the next step
|
|
return await _async_process_step(self._steps, step_id)
|
|
|
|
# Options
|
|
async def async_step_options(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select options to change."""
|
|
|
|
menu_options = [
|
|
STEP_INTERVALS,
|
|
STEP_INTERFACES,
|
|
STEP_EVENTS,
|
|
STEP_SECURITY,
|
|
STEP_FINISH,
|
|
]
|
|
|
|
# Mode-specific
|
|
if self._mode in (ACCESS_POINT, MEDIA_BRIDGE, ROUTER):
|
|
menu_options.insert(0, STEP_CONNECTED_DEVICES)
|
|
|
|
return self.async_show_menu(
|
|
step_id=STEP_OPTIONS,
|
|
menu_options=menu_options,
|
|
)
|
|
|
|
# Connected devices
|
|
async def async_step_connected_devices(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select connected devices settings."""
|
|
|
|
step_id = STEP_CONNECTED_DEVICES
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
user_input[ALL_CLIENTS] = await _async_get_clients(
|
|
self.hass, self._configs, self._options
|
|
)
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_connected_devices(
|
|
user_input, self._mode
|
|
),
|
|
)
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Time intervals
|
|
async def async_step_intervals(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select intervals."""
|
|
|
|
step_id = STEP_INTERVALS
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_intervals(user_input, self._mode),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Network monitoring
|
|
async def async_step_interfaces(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select interfaces for traffic monitoring."""
|
|
|
|
step_id = STEP_INTERFACES
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
user_input[INTERFACES] = await _async_get_network_interfaces(
|
|
self.hass, self._configs, self._options
|
|
)
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_interfaces(user_input),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# HA events
|
|
async def async_step_events(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Events step."""
|
|
|
|
step_id = STEP_EVENTS
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_events(user_input),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Security
|
|
async def async_step_security(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Security step."""
|
|
|
|
step_id = STEP_SECURITY
|
|
|
|
if not user_input:
|
|
user_input = {}
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_security(user_input),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Step Finish
|
|
async def async_step_finish(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Finish setup."""
|
|
|
|
return self.async_create_entry(
|
|
title=self._configs[CONF_HOST],
|
|
data=self._configs,
|
|
options=self._options,
|
|
)
|
|
|
|
@staticmethod
|
|
@callback
|
|
def async_get_options_flow(
|
|
config_entry: ConfigEntry,
|
|
) -> AROptionsFlowHandler:
|
|
"""Get the options flow."""
|
|
|
|
return AROptionsFlowHandler()
|
|
|
|
|
|
class AROptionsFlowHandler(OptionsFlow):
|
|
"""Options flow for AsusRouter."""
|
|
|
|
def __init__(
|
|
self,
|
|
) -> None:
|
|
"""Initialize options flow."""
|
|
|
|
async def async_step_init(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Options flow."""
|
|
|
|
self._selection: dict[str, Any] = {}
|
|
self._configs: dict[str, Any] = self.config_entry.data.copy()
|
|
self._host: str = self._configs[CONF_HOST]
|
|
self._options: dict[str, Any] = self.config_entry.options.copy()
|
|
self._mode = self._options.get(CONF_MODE, CONF_DEFAULT_MODE)
|
|
|
|
return await self.async_step_options(user_input)
|
|
|
|
async def async_step_options(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select options to change."""
|
|
|
|
menu_options = [
|
|
STEP_CREDENTIALS,
|
|
STEP_OPERATION,
|
|
STEP_INTERVALS,
|
|
STEP_INTERFACES,
|
|
STEP_EVENTS,
|
|
STEP_SECURITY,
|
|
STEP_FINISH,
|
|
]
|
|
|
|
# Mode-specific
|
|
if self._mode in (ACCESS_POINT, MEDIA_BRIDGE, ROUTER):
|
|
menu_options.insert(2, STEP_CONNECTED_DEVICES)
|
|
|
|
return self.async_show_menu(
|
|
step_id=STEP_OPTIONS,
|
|
menu_options=menu_options,
|
|
)
|
|
|
|
# Credentials
|
|
async def async_step_credentials(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select credentials."""
|
|
|
|
step_id = STEP_CREDENTIALS
|
|
|
|
errors = {}
|
|
|
|
if user_input:
|
|
if (
|
|
user_input[CONF_USERNAME] != self._options[CONF_USERNAME]
|
|
or user_input[CONF_PASSWORD] != self._options[CONF_PASSWORD]
|
|
or user_input[CONF_PORT] != self._options[CONF_PORT]
|
|
or user_input[CONF_SSL] != self._options[CONF_SSL]
|
|
):
|
|
self._options.update(user_input)
|
|
result = await _async_check_connection(
|
|
self.hass, self._configs, self._options
|
|
)
|
|
if ERRORS in result:
|
|
errors[BASE] = result[ERRORS]
|
|
else:
|
|
self._options.update(result[CONFIGS])
|
|
return await self.async_step_options()
|
|
return await self.async_step_options()
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_credentials(user_input, self._mode),
|
|
errors=errors,
|
|
)
|
|
|
|
# Operation mode
|
|
async def async_step_operation(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select operation mode."""
|
|
|
|
step_id = STEP_OPERATION
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_operation(user_input, self._mode),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
# Save the operation mode
|
|
self._mode = user_input.get(CONF_MODE, CONF_DEFAULT_MODE)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Connected devices
|
|
async def async_step_connected_devices(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select connected devices settings."""
|
|
|
|
step_id = STEP_CONNECTED_DEVICES
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
# Get the selected clients
|
|
selected = user_input.get(CONF_CLIENT_FILTER_LIST, []).copy()
|
|
# Get all the clients
|
|
all_clients = await _async_get_clients(
|
|
self.hass, self._configs, self._options
|
|
)
|
|
# If client was in the list, but cannot be found now, still add it
|
|
for client in selected:
|
|
if client not in all_clients:
|
|
all_clients[client] = client.upper()
|
|
# Save the clients as options
|
|
user_input[ALL_CLIENTS] = all_clients
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_connected_devices(
|
|
user_input, self._mode, default=selected
|
|
),
|
|
)
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Update intervals
|
|
async def async_step_intervals(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select intervals."""
|
|
|
|
step_id = STEP_INTERVALS
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_intervals(user_input, self._mode),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Interfaces to monitor
|
|
async def async_step_interfaces(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Step to select network interfaces."""
|
|
|
|
step_id = STEP_INTERFACES
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
if user_input.get(INTERFACES) is None:
|
|
user_input[INTERFACES] = []
|
|
selected = user_input[INTERFACES].copy()
|
|
interfaces = await _async_get_network_interfaces(
|
|
self.hass, self._configs, self._options
|
|
)
|
|
# If interface was tracked, but cannot be found now, still add it
|
|
for interface in interfaces:
|
|
if interface not in user_input[INTERFACES]:
|
|
user_input[INTERFACES].append(interface)
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_interfaces(
|
|
user_input, default=selected
|
|
),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# HA events
|
|
async def async_step_events(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Events step."""
|
|
|
|
step_id = STEP_EVENTS
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_events(user_input),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Security options
|
|
async def async_step_security(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Security step."""
|
|
|
|
step_id = STEP_SECURITY
|
|
|
|
if not user_input:
|
|
user_input = self._options.copy()
|
|
return self.async_show_form(
|
|
step_id=step_id,
|
|
data_schema=_create_form_security(user_input),
|
|
)
|
|
|
|
self._options.update(user_input)
|
|
|
|
return await self.async_step_options()
|
|
|
|
# Step Finish
|
|
async def async_step_finish(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> FlowResult:
|
|
"""Finish setup."""
|
|
|
|
return self.async_create_entry(
|
|
title=self.config_entry.title,
|
|
data=self._options,
|
|
)
|