@@ -857,6 +857,40 @@ async def test_discovery_via_usb_zha_ignored_updates(hass: HomeAssistant) -> Non
857857 }
858858
859859
860+ async def test_discovery_via_usb_same_device_already_setup (hass : HomeAssistant ) -> None :
861+ """Test discovery aborting if ZHA is already setup."""
862+ MockConfigEntry (
863+ domain = DOMAIN ,
864+ data = {CONF_DEVICE : {CONF_DEVICE_PATH : "/dev/serial/by-id/usb-device123" }},
865+ ).add_to_hass (hass )
866+
867+ # Discovery info with the same device but different path format
868+ discovery_info = UsbServiceInfo (
869+ device = "/dev/ttyUSB0" ,
870+ pid = "AAAA" ,
871+ vid = "AAAA" ,
872+ serial_number = "1234" ,
873+ description = "zigbee radio" ,
874+ manufacturer = "test" ,
875+ )
876+
877+ with patch (
878+ "homeassistant.components.zha.config_flow.usb.get_serial_by_id" ,
879+ return_value = "/dev/serial/by-id/usb-device123" ,
880+ ) as mock_get_serial_by_id :
881+ result = await hass .config_entries .flow .async_init (
882+ DOMAIN , context = {"source" : SOURCE_USB }, data = discovery_info
883+ )
884+ await hass .async_block_till_done ()
885+
886+ # Verify get_serial_by_id was called to normalize the path
887+ assert mock_get_serial_by_id .mock_calls == [call ("/dev/ttyUSB0" )]
888+
889+ # Should abort since it's the same device
890+ assert result ["type" ] is FlowResultType .ABORT
891+ assert result ["reason" ] == "single_instance_allowed"
892+
893+
860894@patch ("homeassistant.components.zha.async_setup_entry" , AsyncMock (return_value = True ))
861895@patch (f"zigpy_znp.{ PROBE_FUNCTION_PATH } " , AsyncMock (return_value = True ))
862896async def test_legacy_zeroconf_discovery_already_setup (hass : HomeAssistant ) -> None :
@@ -890,6 +924,39 @@ async def test_legacy_zeroconf_discovery_already_setup(hass: HomeAssistant) -> N
890924 assert confirm_result ["step_id" ] == "choose_migration_strategy"
891925
892926
927+ async def test_zeroconf_discovery_via_socket_already_setup_with_ip_match (
928+ hass : HomeAssistant ,
929+ ) -> None :
930+ """Test zeroconf discovery aborting when ZHA is already setup with socket and one IP matches."""
931+ MockConfigEntry (
932+ domain = DOMAIN ,
933+ data = {CONF_DEVICE : {CONF_DEVICE_PATH : "socket://192.168.1.101:6638" }},
934+ ).add_to_hass (hass )
935+
936+ service_info = ZeroconfServiceInfo (
937+ ip_address = ip_address ("192.168.1.100" ),
938+ ip_addresses = [
939+ ip_address ("192.168.1.100" ),
940+ ip_address ("192.168.1.101" ), # Matches config entry
941+ ],
942+ hostname = "tube-zigbee-gw.local." ,
943+ name = "mock_name" ,
944+ port = 6638 ,
945+ properties = {"name" : "tube_123456" },
946+ type = "mock_type" ,
947+ )
948+
949+ # Discovery should abort due to single instance check
950+ result = await hass .config_entries .flow .async_init (
951+ DOMAIN , context = {"source" : SOURCE_ZEROCONF }, data = service_info
952+ )
953+ await hass .async_block_till_done ()
954+
955+ # Should abort since one of the advertised IPs matches existing socket path
956+ assert result ["type" ] is FlowResultType .ABORT
957+ assert result ["reason" ] == "single_instance_allowed"
958+
959+
893960@patch (
894961 "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type" ,
895962 mock_detect_radio_type (radio_type = RadioType .deconz ),
@@ -2289,34 +2356,28 @@ async def test_config_flow_serial_resolution_oserror(
22892356) -> None :
22902357 """Test that OSError during serial port resolution is handled."""
22912358
2292- result = await hass .config_entries .flow .async_init (
2293- DOMAIN ,
2294- context = {"source" : "manual_pick_radio_type" },
2295- data = {CONF_RADIO_TYPE : RadioType .ezsp .description },
2296- )
2297-
2298- result = await hass .config_entries .flow .async_configure (
2299- result ["flow_id" ],
2300- user_input = {zigpy .config .CONF_DEVICE_PATH : "/dev/ttyUSB33" },
2359+ discovery_info = UsbServiceInfo (
2360+ device = "/dev/ttyZIGBEE" ,
2361+ pid = "AAAA" ,
2362+ vid = "AAAA" ,
2363+ serial_number = "1234" ,
2364+ description = "zigbee radio" ,
2365+ manufacturer = "test" ,
23012366 )
23022367
2303- assert result ["type" ] is FlowResultType .MENU
2304- assert result ["step_id" ] == "choose_setup_strategy"
2305-
23062368 with (
23072369 patch (
2308- "homeassistant.components.usb.get_serial_by_id" ,
2370+ "homeassistant.components.zha.config_flow. usb.get_serial_by_id" ,
23092371 side_effect = OSError ("Test error" ),
23102372 ),
23112373 ):
2312- setup_result = await hass .config_entries .flow .async_configure (
2313- result ["flow_id" ],
2314- user_input = {"next_step_id" : config_flow .SETUP_STRATEGY_RECOMMENDED },
2374+ result_init = await hass .config_entries .flow .async_init (
2375+ DOMAIN , context = {"source" : SOURCE_USB }, data = discovery_info
23152376 )
23162377
2317- assert setup_result ["type" ] is FlowResultType .ABORT
2318- assert setup_result ["reason" ] == "cannot_resolve_path"
2319- assert setup_result ["description_placeholders" ] == {"path" : "/dev/ttyUSB33 " }
2378+ assert result_init ["type" ] is FlowResultType .ABORT
2379+ assert result_init ["reason" ] == "cannot_resolve_path"
2380+ assert result_init ["description_placeholders" ] == {"path" : "/dev/ttyZIGBEE " }
23202381
23212382
23222383@patch ("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee" )
0 commit comments