Compare commits

...

3 Commits

Author SHA1 Message Date
J. Nick Koston
813177c4ee Merge remote-tracking branch 'upstream/dev' into shelly_ble_provision_fail_path 2025-12-10 13:55:01 +01:00
J. Nick Koston
1d2d238154 one more failure path 2025-12-06 17:28:15 -06:00
J. Nick Koston
4ec5eeeafc Show device name and error in Shelly BLE WiFi scan failure message 2025-12-06 17:21:39 -06:00
3 changed files with 36 additions and 8 deletions

View File

@@ -217,6 +217,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
device_name: str = ""
wifi_networks: list[ShellyWiFiNetwork] = []
selected_ssid: str = ""
ble_scan_error: str = ""
provision_error: str = ""
_provision_task: asyncio.Task | None = None
_provision_result: ConfigFlowResult | None = None
disable_ap_after_provision: bool = True
@@ -788,12 +790,13 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._async_ensure_ble_connected()
self.wifi_networks = await device.wifi_scan()
except (DeviceConnectionError, RpcCallError) as err:
LOGGER.debug("Failed to scan WiFi networks via BLE: %s", err)
LOGGER.debug("Failed to scan WiFi networks via BLE: %r", err)
# "Writing is not permitted" error means device rejects BLE writes
# and BLE provisioning is disabled - user must use Shelly app
if "not permitted" in str(err):
await self._async_disconnect_ble()
return self.async_abort(reason="ble_not_permitted")
self.ble_scan_error = repr(err)
return await self.async_step_wifi_scan_failed()
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception during WiFi scan")
@@ -839,7 +842,13 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
# User wants to retry - go back to wifi_scan
return await self.async_step_wifi_scan()
return self.async_show_form(step_id="wifi_scan_failed")
return self.async_show_form(
step_id="wifi_scan_failed",
description_placeholders={
"name": self.context["title_placeholders"]["name"],
"error": self.ble_scan_error,
},
)
@asynccontextmanager
async def _async_provision_context(
@@ -927,8 +936,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
sta_enable=True,
)
except (DeviceConnectionError, RpcCallError) as err:
LOGGER.debug("Failed to provision WiFi via BLE: %s", err)
LOGGER.debug("Failed to provision WiFi via BLE: %r", err)
# BLE connection/communication failed - allow retry from network selection
self.provision_error = repr(err)
return None
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception during WiFi provisioning")
@@ -977,6 +987,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
else:
LOGGER.debug("BLE fallback also failed - provisioning unsuccessful")
# Store failure info and return None - provision_done will handle redirect
self.provision_error = "Device not found after provisioning"
return None
else:
state.host, state.port = result
@@ -1092,7 +1103,11 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="provision_failed",
description_placeholders={"ssid": self.selected_ssid},
description_placeholders={
"name": self.context["title_placeholders"]["name"],
"ssid": self.selected_ssid,
"error": self.provision_error,
},
)
async def async_step_provision_done(

View File

@@ -57,7 +57,7 @@
}
},
"provision_failed": {
"description": "The device did not connect to {ssid}. This may be due to an incorrect password or the network being out of range. Would you like to try again?"
"description": "{name} did not connect to {ssid}: {error}. This may be due to an incorrect password or the network being out of range. Would you like to try again?"
},
"reauth_confirm": {
"data": {
@@ -112,7 +112,7 @@
"description": "Select a WiFi network and enter the password to provision the device."
},
"wifi_scan_failed": {
"description": "Failed to scan for WiFi networks via Bluetooth. The device may be out of range or Bluetooth connection failed. Would you like to try again?"
"description": "Failed to scan for WiFi networks on {name} via Bluetooth: {error}. The device may be out of range or Bluetooth connection failed. Would you like to try again?"
}
}
},

View File

@@ -3670,7 +3670,9 @@ async def test_bluetooth_wifi_scan_failure(
) -> None:
"""Test WiFi scan failure via BLE."""
# Configure mock BLE device to fail first, then succeed
mock_ble_rpc_device.wifi_scan.side_effect = DeviceConnectionError
mock_ble_rpc_device.wifi_scan.side_effect = DeviceConnectionError(
"Connection timed out"
)
# Inject BLE device so it's available in the bluetooth scanner
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO)
@@ -3689,6 +3691,10 @@ async def test_bluetooth_wifi_scan_failure(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "wifi_scan_failed"
assert result["description_placeholders"] == {
"name": "ShellyPlus2PM-C049EF8873E8",
"error": "DeviceConnectionError('Connection timed out')",
}
# Now configure success for retry
mock_ble_rpc_device.wifi_scan.side_effect = None
@@ -3863,7 +3869,9 @@ async def test_bluetooth_wifi_provision_failure(
{"ssid": "MyNetwork", "rssi": -50, "auth": 2}
]
# First provisioning attempt fails
mock_ble_rpc_device.wifi_setconfig.side_effect = DeviceConnectionError
mock_ble_rpc_device.wifi_setconfig.side_effect = DeviceConnectionError(
"BLE connection lost"
)
# Inject BLE device so it's available in the bluetooth scanner
inject_bluetooth_service_info_bleak(hass, BLE_DISCOVERY_INFO)
@@ -3896,6 +3904,11 @@ async def test_bluetooth_wifi_provision_failure(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "provision_failed"
assert result["description_placeholders"] == {
"name": "ShellyPlus2PM-C049EF8873E8",
"ssid": "MyNetwork",
"error": "DeviceConnectionError('BLE connection lost')",
}
# Reset wifi_setconfig for retry - now it succeeds
mock_ble_rpc_device.wifi_setconfig.side_effect = None