101101 0x0BA9 : bytes .fromhex ("0105000b99990a70d6c297bacc" )
102102} # Flags (0x01, 0x05, 0x00), Model (0x0b, 0x99, 0x99) - unknown model ID, MAC (0x0a, 0x70, 0xd6, 0xc2, 0x97, 0xba, 0xcc)
103103
104+ BLE_MANUFACTURER_DATA_FOR_CLEAR_TEST = {
105+ 0x0BA9 : bytes .fromhex ("0105000b30100a00eeddccbbaa" )
106+ } # Flags (0x01, 0x05, 0x00), Model (0x0b, 0x30, 0x10), MAC (0x0a, 0x00, 0xee, 0xdd, 0xcc, 0xbb, 0xaa)
107+ # Device WiFi MAC: 00eeddccbbaa (little-endian) -> AABBCCDDEE00 (reversed to big-endian)
108+
104109BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak (
105110 name = "ShellyPlus2PM-C049EF8873E8" ,
106111 address = "AA:BB:CC:DD:EE:FF" ,
121126 tx_power = - 127 ,
122127)
123128
129+ BLE_DISCOVERY_INFO_FOR_CLEAR_TEST = BluetoothServiceInfoBleak (
130+ name = "ShellyPlus2PM-AABBCCDDEE00" ,
131+ address = "AA:BB:CC:DD:EE:00" ,
132+ rssi = - 60 ,
133+ manufacturer_data = BLE_MANUFACTURER_DATA_FOR_CLEAR_TEST ,
134+ service_uuids = [],
135+ service_data = {},
136+ source = "local" ,
137+ device = generate_ble_device (
138+ address = "AA:BB:CC:DD:EE:00" ,
139+ name = "ShellyPlus2PM-AABBCCDDEE00" ,
140+ ),
141+ advertisement = generate_advertisement_data (
142+ manufacturer_data = BLE_MANUFACTURER_DATA_FOR_CLEAR_TEST ,
143+ ),
144+ time = 0 ,
145+ connectable = True ,
146+ tx_power = - 127 ,
147+ )
148+
124149BLE_DISCOVERY_INFO_NO_RPC = BluetoothServiceInfoBleak (
125150 name = "ShellyPlus2PM-C049EF8873E8" ,
126151 address = "AA:BB:CC:DD:EE:FF" ,
@@ -2077,6 +2102,87 @@ async def test_bluetooth_discovery(
20772102 assert len (mock_setup_entry .mock_calls ) == 1
20782103
20792104
2105+ async def test_bluetooth_provisioning_clears_match_history (
2106+ hass : HomeAssistant ,
2107+ mock_setup_entry : AsyncMock ,
2108+ mock_setup : AsyncMock ,
2109+ ) -> None :
2110+ """Test bluetooth provisioning clears match history at discovery start and after successful provisioning."""
2111+ # Inject BLE device so it's available in the bluetooth scanner
2112+ inject_bluetooth_service_info_bleak (hass , BLE_DISCOVERY_INFO_FOR_CLEAR_TEST )
2113+
2114+ with patch (
2115+ "homeassistant.components.shelly.config_flow.async_clear_address_from_match_history" ,
2116+ ) as mock_clear :
2117+ result = await hass .config_entries .flow .async_init (
2118+ DOMAIN ,
2119+ data = BLE_DISCOVERY_INFO_FOR_CLEAR_TEST ,
2120+ context = {"source" : config_entries .SOURCE_BLUETOOTH },
2121+ )
2122+
2123+ assert result ["type" ] is FlowResultType .FORM
2124+ assert result ["step_id" ] == "bluetooth_confirm"
2125+
2126+ # Confirm
2127+ with patch (
2128+ "homeassistant.components.shelly.config_flow.async_scan_wifi_networks" ,
2129+ return_value = [{"ssid" : "MyNetwork" , "rssi" : - 50 , "auth" : 2 }],
2130+ ):
2131+ result = await hass .config_entries .flow .async_configure (
2132+ result ["flow_id" ], {}
2133+ )
2134+
2135+ # Select network
2136+ result = await hass .config_entries .flow .async_configure (
2137+ result ["flow_id" ],
2138+ {CONF_SSID : "MyNetwork" },
2139+ )
2140+
2141+ # Reset mock to only count calls during provisioning
2142+ mock_clear .reset_mock ()
2143+
2144+ # Enter password and provision
2145+ with (
2146+ patch (
2147+ "homeassistant.components.shelly.config_flow.async_provision_wifi" ,
2148+ ),
2149+ patch (
2150+ "homeassistant.components.shelly.config_flow.async_lookup_device_by_name" ,
2151+ return_value = ("1.1.1.1" , 80 ),
2152+ ),
2153+ patch (
2154+ "homeassistant.components.shelly.config_flow.get_info" ,
2155+ return_value = MOCK_DEVICE_INFO ,
2156+ ),
2157+ ):
2158+ result = await hass .config_entries .flow .async_configure (
2159+ result ["flow_id" ],
2160+ {CONF_PASSWORD : "my_password" },
2161+ )
2162+
2163+ # Provisioning happens in background, shows progress
2164+ assert result ["type" ] is FlowResultType .SHOW_PROGRESS
2165+ await hass .async_block_till_done ()
2166+
2167+ # Complete provisioning by configuring the progress step
2168+ result = await hass .config_entries .flow .async_configure (result ["flow_id" ])
2169+
2170+ # Provisioning should complete and create entry
2171+ assert result ["type" ] is FlowResultType .CREATE_ENTRY
2172+ assert result ["result" ].unique_id == "AABBCCDDEE00"
2173+
2174+ # Verify match history was cleared once during provisioning
2175+ # Only count calls with our test device's address to avoid interference from other tests
2176+ our_device_calls = [
2177+ call
2178+ for call in mock_clear .call_args_list
2179+ if len (call .args ) > 1
2180+ and call .args [1 ] == BLE_DISCOVERY_INFO_FOR_CLEAR_TEST .address
2181+ ]
2182+ assert our_device_calls
2183+ mock_clear .assert_called_with (hass , BLE_DISCOVERY_INFO_FOR_CLEAR_TEST .address )
2184+
2185+
20802186@pytest .mark .usefixtures ("mock_zeroconf" )
20812187async def test_bluetooth_discovery_no_rpc_over_ble (
20822188 hass : HomeAssistant ,
@@ -2092,6 +2198,88 @@ async def test_bluetooth_discovery_no_rpc_over_ble(
20922198 assert result ["reason" ] == "invalid_discovery_info"
20932199
20942200
2201+ async def test_bluetooth_factory_reset_rediscovery (
2202+ hass : HomeAssistant ,
2203+ mock_setup_entry : AsyncMock ,
2204+ mock_setup : AsyncMock ,
2205+ ) -> None :
2206+ """Test device can be rediscovered after factory reset when RPC-over-BLE is re-enabled."""
2207+ # First discovery: device is already provisioned (no RPC-over-BLE)
2208+ # Inject the device without RPC so it's in the bluetooth scanner
2209+ inject_bluetooth_service_info_bleak (hass , BLE_DISCOVERY_INFO_NO_RPC )
2210+
2211+ result = await hass .config_entries .flow .async_init (
2212+ DOMAIN ,
2213+ data = BLE_DISCOVERY_INFO_NO_RPC ,
2214+ context = {"source" : config_entries .SOURCE_BLUETOOTH },
2215+ )
2216+
2217+ # Should abort because RPC-over-BLE is not enabled
2218+ assert result ["type" ] is FlowResultType .ABORT
2219+ assert result ["reason" ] == "invalid_discovery_info"
2220+
2221+ # Simulate factory reset: device now advertises with RPC-over-BLE enabled
2222+ # Inject the updated advertisement
2223+ inject_bluetooth_service_info_bleak (hass , BLE_DISCOVERY_INFO )
2224+
2225+ # Second discovery: device after factory reset (RPC-over-BLE now enabled)
2226+ # Wait for automatic discovery to happen
2227+ await hass .async_block_till_done ()
2228+
2229+ # Find the flow that was automatically created
2230+ flows = hass .config_entries .flow .async_progress ()
2231+ assert len (flows ) == 1
2232+ result = flows [0 ]
2233+
2234+ # Should successfully start config flow since match history was cleared
2235+ assert result ["step_id" ] == "bluetooth_confirm"
2236+ assert (
2237+ result ["context" ]["title_placeholders" ]["name" ] == "ShellyPlus2PM-C049EF8873E8"
2238+ )
2239+
2240+ with patch (
2241+ "homeassistant.components.shelly.config_flow.async_scan_wifi_networks" ,
2242+ return_value = [{"ssid" : "MyNetwork" , "rssi" : - 50 , "auth" : 2 }],
2243+ ):
2244+ result = await hass .config_entries .flow .async_configure (result ["flow_id" ], {})
2245+
2246+ # Select network
2247+ result = await hass .config_entries .flow .async_configure (
2248+ result ["flow_id" ],
2249+ {CONF_SSID : "MyNetwork" },
2250+ )
2251+
2252+ # Enter password and provision
2253+ with (
2254+ patch (
2255+ "homeassistant.components.shelly.config_flow.async_provision_wifi" ,
2256+ ),
2257+ patch (
2258+ "homeassistant.components.shelly.config_flow.async_lookup_device_by_name" ,
2259+ return_value = ("1.1.1.1" , 80 ),
2260+ ),
2261+ patch (
2262+ "homeassistant.components.shelly.config_flow.get_info" ,
2263+ return_value = MOCK_DEVICE_INFO ,
2264+ ),
2265+ ):
2266+ result = await hass .config_entries .flow .async_configure (
2267+ result ["flow_id" ],
2268+ {CONF_PASSWORD : "my_password" },
2269+ )
2270+
2271+ # Provisioning happens in background
2272+ assert result ["type" ] is FlowResultType .SHOW_PROGRESS
2273+ await hass .async_block_till_done ()
2274+
2275+ # Complete provisioning
2276+ result = await hass .config_entries .flow .async_configure (result ["flow_id" ])
2277+
2278+ # Provisioning should complete and create entry
2279+ assert result ["type" ] is FlowResultType .CREATE_ENTRY
2280+ assert result ["result" ].unique_id == "C049EF8873E8"
2281+
2282+
20952283@pytest .mark .usefixtures ("mock_zeroconf" )
20962284async def test_bluetooth_discovery_invalid_name (
20972285 hass : HomeAssistant ,
@@ -2184,6 +2372,41 @@ async def test_bluetooth_discovery_already_configured(
21842372 assert result ["reason" ] == "already_configured"
21852373
21862374
2375+ async def test_bluetooth_discovery_already_configured_clears_match_history (
2376+ hass : HomeAssistant ,
2377+ ) -> None :
2378+ """Test bluetooth discovery clears match history when device already configured."""
2379+ # Inject BLE device so it's available in the bluetooth scanner
2380+ inject_bluetooth_service_info_bleak (hass , BLE_DISCOVERY_INFO )
2381+
2382+ entry = MockConfigEntry (
2383+ domain = DOMAIN ,
2384+ unique_id = "C049EF8873E8" , # MAC from device name - uppercase no colons
2385+ data = {
2386+ CONF_HOST : "1.1.1.1" ,
2387+ CONF_MODEL : MODEL_PLUS_2PM ,
2388+ CONF_SLEEP_PERIOD : 0 ,
2389+ CONF_GEN : 2 ,
2390+ },
2391+ )
2392+ entry .add_to_hass (hass )
2393+
2394+ with patch (
2395+ "homeassistant.components.shelly.config_flow.async_clear_address_from_match_history"
2396+ ) as mock_clear :
2397+ result = await hass .config_entries .flow .async_init (
2398+ DOMAIN ,
2399+ data = BLE_DISCOVERY_INFO ,
2400+ context = {"source" : config_entries .SOURCE_BLUETOOTH },
2401+ )
2402+
2403+ assert result ["type" ] is FlowResultType .ABORT
2404+ assert result ["reason" ] == "already_configured"
2405+
2406+ # Verify match history was cleared to allow rediscovery if factory reset
2407+ mock_clear .assert_called_once_with (hass , BLE_DISCOVERY_INFO .address )
2408+
2409+
21872410@pytest .mark .usefixtures ("mock_zeroconf" )
21882411async def test_bluetooth_discovery_no_ble_device (
21892412 hass : HomeAssistant ,
0 commit comments