Skip to content

Commit cfab789

Browse files
Add hardware Zigbee flow strategy (home-assistant#153190)
1 parent 8191742 commit cfab789

File tree

5 files changed

+422
-53
lines changed

5 files changed

+422
-53
lines changed

homeassistant/components/homeassistant_hardware/firmware_config_flow.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ class PickedFirmwareType(StrEnum):
6161
ZIGBEE = "zigbee"
6262

6363

64+
class ZigbeeFlowStrategy(StrEnum):
65+
"""Zigbee setup strategies that can be picked."""
66+
67+
ADVANCED = "advanced"
68+
RECOMMENDED = "recommended"
69+
70+
6471
class ZigbeeIntegration(StrEnum):
6572
"""Zigbee integrations that can be picked."""
6673

@@ -73,6 +80,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
7380

7481
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
7582
_picked_firmware_type: PickedFirmwareType
83+
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
7684

7785
def __init__(self, *args: Any, **kwargs: Any) -> None:
7886
"""Instantiate base flow."""
@@ -395,12 +403,14 @@ async def async_step_zigbee_intent_recommended(
395403
) -> ConfigFlowResult:
396404
"""Select recommended installation type."""
397405
self._zigbee_integration = ZigbeeIntegration.ZHA
406+
self._zigbee_flow_strategy = ZigbeeFlowStrategy.RECOMMENDED
398407
return await self._async_continue_picked_firmware()
399408

400409
async def async_step_zigbee_intent_custom(
401410
self, user_input: dict[str, Any] | None = None
402411
) -> ConfigFlowResult:
403412
"""Select custom installation type."""
413+
self._zigbee_flow_strategy = ZigbeeFlowStrategy.ADVANCED
404414
return await self.async_step_zigbee_integration()
405415

406416
async def async_step_zigbee_integration(
@@ -521,6 +531,7 @@ async def async_step_continue_zigbee(
521531
"flow_control": "hardware",
522532
},
523533
"radio_type": "ezsp",
534+
"flow_strategy": self._zigbee_flow_strategy,
524535
},
525536
)
526537
return self._continue_zha_flow(result)

homeassistant/components/zha/config_flow.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
from homeassistant.components.file_upload import process_uploaded_file
2121
from homeassistant.components.hassio import AddonError, AddonState
2222
from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon
23+
from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
24+
ZigbeeFlowStrategy,
25+
)
2326
from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware
2427
from homeassistant.config_entries import (
2528
SOURCE_IGNORE,
@@ -163,6 +166,7 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]:
163166
class BaseZhaFlow(ConfigEntryBaseFlow):
164167
"""Mixin for common ZHA flow steps and forms."""
165168

169+
_flow_strategy: ZigbeeFlowStrategy | None = None
166170
_hass: HomeAssistant
167171
_title: str
168172

@@ -373,6 +377,12 @@ async def async_step_choose_setup_strategy(
373377
self, user_input: dict[str, Any] | None = None
374378
) -> ConfigFlowResult:
375379
"""Choose how to set up the integration from scratch."""
380+
if self._flow_strategy == ZigbeeFlowStrategy.RECOMMENDED:
381+
# Fast path: automatically form a new network
382+
return await self.async_step_setup_strategy_recommended()
383+
if self._flow_strategy == ZigbeeFlowStrategy.ADVANCED:
384+
# Advanced path: let the user choose
385+
return await self.async_step_setup_strategy_advanced()
376386

377387
# Allow onboarding for new users to just create a new network automatically
378388
if (
@@ -406,6 +416,12 @@ async def async_step_choose_migration_strategy(
406416
self, user_input: dict[str, Any] | None = None
407417
) -> ConfigFlowResult:
408418
"""Choose how to deal with the current radio's settings during migration."""
419+
if self._flow_strategy == ZigbeeFlowStrategy.RECOMMENDED:
420+
# Fast path: automatically migrate everything
421+
return await self.async_step_migration_strategy_recommended()
422+
if self._flow_strategy == ZigbeeFlowStrategy.ADVANCED:
423+
# Advanced path: let the user choose
424+
return await self.async_step_migration_strategy_advanced()
409425
return self.async_show_menu(
410426
step_id="choose_migration_strategy",
411427
menu_options=[
@@ -867,6 +883,7 @@ async def async_step_hardware(
867883
radio_type = self._radio_mgr.parse_radio_type(discovery_data["radio_type"])
868884
device_settings = discovery_data["port"]
869885
device_path = device_settings[CONF_DEVICE_PATH]
886+
self._flow_strategy = discovery_data.get("flow_strategy")
870887

871888
await self._set_unique_id_and_update_ignored_flow(
872889
unique_id=f"{name}_{radio_type.name}_{device_path}",

homeassistant/components/zha/radio_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
from homeassistant import config_entries
3030
from homeassistant.components import usb
31+
from homeassistant.components.homeassistant_hardware.firmware_config_flow import (
32+
ZigbeeFlowStrategy,
33+
)
3134
from homeassistant.core import HomeAssistant
3235
from homeassistant.exceptions import HomeAssistantError
3336
from homeassistant.helpers.service_info.usb import UsbServiceInfo
@@ -74,6 +77,7 @@
7477
vol.Required("name"): str,
7578
vol.Required("port"): DEVICE_SCHEMA,
7679
vol.Required("radio_type"): str,
80+
vol.Optional("flow_strategy"): vol.All(str, vol.Coerce(ZigbeeFlowStrategy)),
7781
}
7882
)
7983

tests/components/homeassistant_hardware/test_config_flow.py

Lines changed: 116 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,8 @@ async def consume_progress_flow(
364364
return result
365365

366366

367-
async def test_config_flow_recommended(hass: HomeAssistant) -> None:
368-
"""Test the config flow with recommended installation type for Zigbee."""
367+
async def test_config_flow_zigbee_recommended(hass: HomeAssistant) -> None:
368+
"""Test flow with recommended Zigbee installation type."""
369369
init_result = await hass.config_entries.flow.async_init(
370370
TEST_DOMAIN, context={"source": "hardware"}
371371
)
@@ -418,37 +418,28 @@ async def test_config_flow_recommended(hass: HomeAssistant) -> None:
418418
assert zha_flow["context"]["source"] == "hardware"
419419
assert zha_flow["step_id"] == "confirm"
420420

421+
progress_zha_flows = hass.config_entries.flow._async_progress_by_handler(
422+
handler="zha",
423+
match_context=None,
424+
)
421425

422-
@pytest.mark.parametrize(
423-
("zigbee_integration", "zha_flows"),
424-
[
425-
(
426-
"zigbee_integration_zha",
427-
[
428-
{
429-
"context": {
430-
"confirm_only": True,
431-
"source": "hardware",
432-
"title_placeholders": {
433-
"name": "Some Hardware Name",
434-
},
435-
"unique_id": "Some Hardware Name_ezsp_/dev/SomeDevice123",
436-
},
437-
"flow_id": ANY,
438-
"handler": "zha",
439-
"step_id": "confirm",
440-
}
441-
],
442-
),
443-
("zigbee_integration_other", []),
444-
],
445-
)
446-
async def test_config_flow_zigbee_custom(
447-
hass: HomeAssistant,
448-
zigbee_integration: str,
449-
zha_flows: list[ConfigFlowResult],
450-
) -> None:
451-
"""Test the config flow with custom installation type selected for Zigbee."""
426+
assert len(progress_zha_flows) == 1
427+
428+
progress_zha_flow = progress_zha_flows[0]
429+
assert progress_zha_flow.init_data == {
430+
"name": "Some Hardware Name",
431+
"port": {
432+
"path": "/dev/SomeDevice123",
433+
"baudrate": 115200,
434+
"flow_control": "hardware",
435+
},
436+
"radio_type": "ezsp",
437+
"flow_strategy": "recommended",
438+
}
439+
440+
441+
async def test_config_flow_zigbee_custom_zha(hass: HomeAssistant) -> None:
442+
"""Test flow with custom Zigbee installation type and ZHA selected."""
452443
init_result = await hass.config_entries.flow.async_init(
453444
TEST_DOMAIN, context={"source": "hardware"}
454445
)
@@ -479,7 +470,7 @@ async def test_config_flow_zigbee_custom(
479470

480471
pick_result = await hass.config_entries.flow.async_configure(
481472
pick_result["flow_id"],
482-
user_input={"next_step_id": zigbee_integration},
473+
user_input={"next_step_id": "zigbee_integration_zha"},
483474
)
484475

485476
assert pick_result["type"] is FlowResultType.SHOW_PROGRESS
@@ -503,7 +494,98 @@ async def test_config_flow_zigbee_custom(
503494

504495
# Ensure a ZHA discovery flow has been created
505496
flows = hass.config_entries.flow.async_progress()
506-
assert flows == zha_flows
497+
assert flows == [
498+
{
499+
"context": {
500+
"confirm_only": True,
501+
"source": "hardware",
502+
"title_placeholders": {
503+
"name": "Some Hardware Name",
504+
},
505+
"unique_id": "Some Hardware Name_ezsp_/dev/SomeDevice123",
506+
},
507+
"flow_id": ANY,
508+
"handler": "zha",
509+
"step_id": "confirm",
510+
}
511+
]
512+
513+
progress_zha_flows = hass.config_entries.flow._async_progress_by_handler(
514+
handler="zha",
515+
match_context=None,
516+
)
517+
518+
assert len(progress_zha_flows) == 1
519+
520+
progress_zha_flow = progress_zha_flows[0]
521+
assert progress_zha_flow.init_data == {
522+
"name": "Some Hardware Name",
523+
"port": {
524+
"path": "/dev/SomeDevice123",
525+
"baudrate": 115200,
526+
"flow_control": "hardware",
527+
},
528+
"radio_type": "ezsp",
529+
"flow_strategy": "advanced",
530+
}
531+
532+
533+
async def test_config_flow_zigbee_custom_other(hass: HomeAssistant) -> None:
534+
"""Test flow with custom Zigbee installation type and Other selected."""
535+
init_result = await hass.config_entries.flow.async_init(
536+
TEST_DOMAIN, context={"source": "hardware"}
537+
)
538+
539+
assert init_result["type"] is FlowResultType.MENU
540+
assert init_result["step_id"] == "pick_firmware"
541+
542+
with mock_firmware_info(
543+
probe_app_type=ApplicationType.SPINEL,
544+
flash_app_type=ApplicationType.EZSP,
545+
):
546+
# Pick the menu option: we are flashing the firmware
547+
pick_result = await hass.config_entries.flow.async_configure(
548+
init_result["flow_id"],
549+
user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE},
550+
)
551+
552+
assert pick_result["type"] is FlowResultType.MENU
553+
assert pick_result["step_id"] == "zigbee_installation_type"
554+
555+
pick_result = await hass.config_entries.flow.async_configure(
556+
pick_result["flow_id"],
557+
user_input={"next_step_id": "zigbee_intent_custom"},
558+
)
559+
560+
assert pick_result["type"] is FlowResultType.MENU
561+
assert pick_result["step_id"] == "zigbee_integration"
562+
563+
pick_result = await hass.config_entries.flow.async_configure(
564+
pick_result["flow_id"],
565+
user_input={"next_step_id": "zigbee_integration_other"},
566+
)
567+
568+
assert pick_result["type"] is FlowResultType.SHOW_PROGRESS
569+
assert pick_result["progress_action"] == "install_firmware"
570+
assert pick_result["step_id"] == "install_zigbee_firmware"
571+
572+
create_result = await consume_progress_flow(
573+
hass,
574+
flow_id=pick_result["flow_id"],
575+
valid_step_ids=("install_zigbee_firmware",),
576+
)
577+
578+
assert create_result["type"] is FlowResultType.CREATE_ENTRY
579+
580+
config_entry = create_result["result"]
581+
assert config_entry.data == {
582+
"firmware": "ezsp",
583+
"device": TEST_DEVICE,
584+
"hardware": TEST_HARDWARE_NAME,
585+
}
586+
587+
flows = hass.config_entries.flow.async_progress()
588+
assert flows == []
507589

508590

509591
async def test_config_flow_firmware_index_download_fails_but_not_required(

0 commit comments

Comments
 (0)