Improve unifi options flow UX and remove advanced mode dependency (#170501)

This commit is contained in:
Franck Nijhof
2026-05-13 20:17:53 +02:00
committed by GitHub
parent ccc454f8f9
commit a07034c13b
4 changed files with 108 additions and 295 deletions
+61 -143
View File
@@ -30,6 +30,7 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import SectionConfig, section
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import DiscoveryInfoType
@@ -43,6 +44,7 @@ from .const import (
CONF_DETECTION_TIME,
CONF_DPI_RESTRICTIONS,
CONF_IGNORE_WIRED_BUG,
CONF_MORE_OPTIONS,
CONF_SITE_ID,
CONF_SSID_FILTER,
CONF_TRACK_CLIENTS,
@@ -253,53 +255,23 @@ class UnifiOptionsFlowHandler(OptionsFlow):
self.hub = self.config_entry.runtime_data
self.options[CONF_BLOCK_CLIENT] = self.hub.config.option_block_clients
if self.show_advanced_options:
return await self.async_step_configure_entity_sources()
return await self.async_step_simple_options()
async def async_step_simple_options(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""For users without advanced settings enabled."""
if user_input is not None:
more_options = user_input.pop(CONF_MORE_OPTIONS, {})
user_input.update(more_options)
self.options.update(user_input)
return await self._update_options()
return self.async_create_entry(title="", data=self.options)
clients_to_block = {}
for client in self.hub.api.clients.values():
clients_to_block[client.mac] = (
f"{client.name or client.hostname} ({client.mac})"
)
return self.async_show_form(
step_id="simple_options",
data_schema=vol.Schema(
{
vol.Optional(
CONF_TRACK_CLIENTS,
default=self.hub.config.option_track_clients,
): bool,
vol.Optional(
CONF_TRACK_DEVICES,
default=self.hub.config.option_track_devices,
): bool,
vol.Optional(
CONF_BLOCK_CLIENT, default=self.options[CONF_BLOCK_CLIENT]
): cv.multi_select(clients_to_block),
}
),
last_step=True,
)
async def async_step_configure_entity_sources(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select sources for entities."""
if user_input is not None:
self.options.update(user_input)
return await self.async_step_device_tracker()
selected_clients_to_block = [
client
for client in self.options.get(CONF_BLOCK_CLIENT, [])
if client in clients_to_block
]
clients = {
client.mac: f"{client.name or client.hostname} ({client.mac})"
@@ -311,29 +283,6 @@ class UnifiOptionsFlowHandler(OptionsFlow):
if mac not in clients
}
return self.async_show_form(
step_id="configure_entity_sources",
data_schema=vol.Schema(
{
vol.Optional(
CONF_CLIENT_SOURCE,
default=self.options.get(CONF_CLIENT_SOURCE, []),
): cv.multi_select(
dict(sorted(clients.items(), key=operator.itemgetter(1)))
),
}
),
last_step=False,
)
async def async_step_device_tracker(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the device tracker options."""
if user_input is not None:
self.options.update(user_input)
return await self.async_step_client_control()
ssids = (
{wlan.name for wlan in self.hub.api.wlans.values()}
| {
@@ -356,107 +305,76 @@ class UnifiOptionsFlowHandler(OptionsFlow):
]
return self.async_show_form(
step_id="device_tracker",
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_TRACK_CLIENTS,
default=self.hub.config.option_track_clients,
): bool,
vol.Optional(
CONF_TRACK_WIRED_CLIENTS,
default=self.hub.config.option_track_wired_clients,
): bool,
vol.Optional(
CONF_TRACK_DEVICES,
default=self.hub.config.option_track_devices,
): bool,
vol.Optional(
CONF_SSID_FILTER, default=selected_ssids_to_filter
): cv.multi_select(ssid_filter),
vol.Optional(
CONF_DETECTION_TIME,
default=int(
self.hub.config.option_detection_time.total_seconds()
),
): int,
vol.Optional(
CONF_IGNORE_WIRED_BUG,
default=self.hub.config.option_ignore_wired_bug,
): bool,
}
),
last_step=False,
)
async def async_step_client_control(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage configuration of network access controlled clients."""
if user_input is not None:
self.options.update(user_input)
return await self.async_step_statistics_sensors()
clients_to_block = {}
for client in self.hub.api.clients.values():
clients_to_block[client.mac] = (
f"{client.name or client.hostname} ({client.mac})"
)
selected_clients_to_block = [
client
for client in self.options.get(CONF_BLOCK_CLIENT, [])
if client in clients_to_block
]
return self.async_show_form(
step_id="client_control",
data_schema=vol.Schema(
{
vol.Optional(
CONF_BLOCK_CLIENT, default=selected_clients_to_block
): cv.multi_select(clients_to_block),
vol.Optional(
CONF_DPI_RESTRICTIONS,
default=self.options.get(
CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS
vol.Required(CONF_MORE_OPTIONS): section(
vol.Schema(
{
vol.Optional(
CONF_CLIENT_SOURCE,
default=self.options.get(CONF_CLIENT_SOURCE, []),
): cv.multi_select(
dict(
sorted(
clients.items(),
key=operator.itemgetter(1),
)
)
),
vol.Optional(
CONF_TRACK_WIRED_CLIENTS,
default=self.hub.config.option_track_wired_clients,
): bool,
vol.Optional(
CONF_SSID_FILTER,
default=selected_ssids_to_filter,
): cv.multi_select(ssid_filter),
vol.Optional(
CONF_DETECTION_TIME,
default=int(
self.hub.config.option_detection_time.total_seconds()
),
): int,
vol.Optional(
CONF_IGNORE_WIRED_BUG,
default=self.hub.config.option_ignore_wired_bug,
): bool,
vol.Optional(
CONF_DPI_RESTRICTIONS,
default=self.options.get(
CONF_DPI_RESTRICTIONS,
DEFAULT_DPI_RESTRICTIONS,
),
): bool,
vol.Optional(
CONF_ALLOW_BANDWIDTH_SENSORS,
default=self.hub.config.option_allow_bandwidth_sensors,
): bool,
vol.Optional(
CONF_ALLOW_UPTIME_SENSORS,
default=self.hub.config.option_allow_uptime_sensors,
): bool,
}
),
): bool,
}
),
last_step=False,
)
async def async_step_statistics_sensors(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the statistics sensors options."""
if user_input is not None:
self.options.update(user_input)
return await self._update_options()
return self.async_show_form(
step_id="statistics_sensors",
data_schema=vol.Schema(
{
vol.Optional(
CONF_ALLOW_BANDWIDTH_SENSORS,
default=self.hub.config.option_allow_bandwidth_sensors,
): bool,
vol.Optional(
CONF_ALLOW_UPTIME_SENSORS,
default=self.hub.config.option_allow_uptime_sensors,
): bool,
SectionConfig(collapsed=True),
),
}
),
last_step=True,
)
async def _update_options(self) -> ConfigFlowResult:
"""Update config entry options."""
return self.async_create_entry(title="", data=self.options)
async def _async_discover_unifi(hass: HomeAssistant) -> str | None:
"""Discover UniFi Network address."""
+1
View File
@@ -33,6 +33,7 @@ CONF_IGNORE_WIRED_BUG = "ignore_wired_bug"
CONF_TRACK_CLIENTS = "track_clients"
CONF_TRACK_DEVICES = "track_devices"
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
CONF_MORE_OPTIONS = "more_options"
CONF_SSID_FILTER = "ssid_filter"
DEFAULT_ALLOW_BANDWIDTH_SENSORS = False
+29 -61
View File
@@ -164,74 +164,42 @@
"integration_not_setup": "UniFi integration is not set up"
},
"step": {
"client_control": {
"init": {
"data": {
"block_client": "Network access controlled clients",
"dpi_restrictions": "Allow control of DPI restriction groups",
"poe_clients": "Allow PoE control of clients"
"track_clients": "Track network clients",
"track_devices": "Track network devices (Ubiquiti devices)"
},
"data_description": {
"block_client": "Select clients whose network access you want to control via switches.",
"dpi_restrictions": "Enable switches to control DPI restriction groups.",
"poe_clients": "Enable switches to control PoE power for clients."
},
"description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.",
"title": "UniFi Network options 2/3"
},
"configure_entity_sources": {
"data": {
"client_source": "Create entities from network clients"
},
"data_description": {
"client_source": "Select which network clients to create entities from."
},
"description": "Select sources to create entities from",
"title": "UniFi Network Entity Sources"
},
"device_tracker": {
"data": {
"detection_time": "Time in seconds from last seen until considered away",
"ignore_wired_bug": "Disable UniFi Network wired bug logic",
"ssid_filter": "Select SSIDs to track wireless clients on",
"track_clients": "Track network clients",
"track_devices": "Track network devices (Ubiquiti devices)",
"track_wired_clients": "Include wired network clients"
},
"data_description": {
"detection_time": "Number of seconds since last seen before a client is considered away.",
"ignore_wired_bug": "Disable workaround for a UniFi Network bug that sometimes reports wired clients as wireless.",
"ssid_filter": "Only track wireless clients connected to selected SSIDs.",
"track_clients": "Create device tracker entities for network clients.",
"track_devices": "Create device tracker entities for Ubiquiti network devices.",
"track_wired_clients": "Include wired clients in device tracking."
"track_devices": "Create device tracker entities for Ubiquiti network devices."
},
"description": "Configure device tracking",
"title": "UniFi Network options 1/3"
},
"simple_options": {
"data": {
"block_client": "[%key:component::unifi::options::step::client_control::data::block_client%]",
"track_clients": "[%key:component::unifi::options::step::device_tracker::data::track_clients%]",
"track_devices": "[%key:component::unifi::options::step::device_tracker::data::track_devices%]"
},
"data_description": {
"block_client": "[%key:component::unifi::options::step::client_control::data_description::block_client%]",
"track_clients": "[%key:component::unifi::options::step::device_tracker::data_description::track_clients%]",
"track_devices": "[%key:component::unifi::options::step::device_tracker::data_description::track_devices%]"
},
"description": "Configure UniFi Network integration"
},
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Bandwidth usage sensors for network clients",
"allow_uptime_sensors": "Uptime sensors for network clients"
},
"data_description": {
"allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients.",
"allow_uptime_sensors": "Create uptime sensors for network clients."
},
"description": "Configure statistics sensors",
"title": "UniFi Network options 3/3"
"sections": {
"more_options": {
"data": {
"allow_bandwidth_sensors": "Bandwidth usage sensors for network clients",
"allow_uptime_sensors": "Uptime sensors for network clients",
"client_source": "Create entities from network clients",
"detection_time": "Time in seconds from last seen until considered away",
"dpi_restrictions": "Allow control of DPI restriction groups",
"ignore_wired_bug": "Disable UniFi Network wired bug logic",
"ssid_filter": "Select SSIDs to track wireless clients on",
"track_wired_clients": "Include wired network clients"
},
"data_description": {
"allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients.",
"allow_uptime_sensors": "Create uptime sensors for network clients.",
"client_source": "Select which network clients to create entities from.",
"detection_time": "Number of seconds since last seen before a client is considered away.",
"dpi_restrictions": "Enable switches to control DPI restriction groups.",
"ignore_wired_bug": "Disable workaround for a UniFi Network bug that sometimes reports wired clients as wireless.",
"ssid_filter": "Only track wireless clients connected to selected SSIDs.",
"track_wired_clients": "Include wired clients in device tracking."
},
"name": "More options"
}
}
}
}
},
+17 -91
View File
@@ -15,6 +15,7 @@ from homeassistant.components.unifi.const import (
CONF_DETECTION_TIME,
CONF_DPI_RESTRICTIONS,
CONF_IGNORE_WIRED_BUG,
CONF_MORE_OPTIONS,
CONF_SITE_ID,
CONF_SSID_FILTER,
CONF_TRACK_CLIENTS,
@@ -377,70 +378,33 @@ async def test_reauth_flow_update_configuration_on_not_loaded_entry(
@pytest.mark.parametrize("device_payload", [DEVICES])
@pytest.mark.parametrize("wlan_payload", [WLANS])
@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS])
async def test_advanced_option_flow(
async def test_option_flow(
hass: HomeAssistant, config_entry_setup: MockConfigEntry
) -> None:
"""Test advanced config flow options."""
"""Test config flow options."""
config_entry = config_entry_setup
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context={"show_advanced_options": True}
)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "configure_entity_sources"
assert not result["last_step"]
assert list(result["data_schema"].schema[CONF_CLIENT_SOURCE].options.keys()) == [
"00:00:00:00:00:01"
]
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_CLIENT_SOURCE: ["00:00:00:00:00:01"]},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "device_tracker"
assert not result["last_step"]
assert list(result["data_schema"].schema[CONF_SSID_FILTER].options.keys()) == [
"",
"SSID 1",
"SSID 2",
"SSID 2_IOT",
"SSID 3",
"SSID 4",
]
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_TRACK_CLIENTS: False,
CONF_TRACK_WIRED_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_SSID_FILTER: ["SSID 1", "SSID 2_IOT", "SSID 3", "SSID 4"],
CONF_DETECTION_TIME: 100,
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "client_control"
assert not result["last_step"]
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
CONF_DPI_RESTRICTIONS: False,
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "statistics_sensors"
assert result["step_id"] == "init"
assert result["last_step"]
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_ALLOW_BANDWIDTH_SENSORS: True,
CONF_ALLOW_UPTIME_SENSORS: True,
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
CONF_MORE_OPTIONS: {
CONF_CLIENT_SOURCE: ["00:00:00:00:00:01"],
CONF_TRACK_WIRED_CLIENTS: False,
CONF_SSID_FILTER: ["SSID 1", "SSID 2_IOT", "SSID 3", "SSID 4"],
CONF_DETECTION_TIME: 100,
CONF_DPI_RESTRICTIONS: False,
CONF_ALLOW_BANDWIDTH_SENSORS: True,
CONF_ALLOW_UPTIME_SENSORS: True,
},
},
)
@@ -460,44 +424,6 @@ async def test_advanced_option_flow(
}
@pytest.mark.parametrize("client_payload", [CLIENTS])
async def test_simple_option_flow(
hass: HomeAssistant, config_entry_setup: MockConfigEntry
) -> None:
"""Test simple config flow options."""
config_entry = config_entry_setup
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context={"show_advanced_options": False}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "simple_options"
assert result["last_step"]
with patch(
"homeassistant.config_entries.ConfigEntries.async_schedule_reload"
) as mock_reload:
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
}
# Check there is no reload on updating these options
assert mock_reload.call_count == 0
async def test_discover_unifi_positive(hass: HomeAssistant) -> None:
"""Verify positive run of UniFi discovery."""
with patch("socket.gethostbyname", return_value="192.168.1.1"):