Offer retry when Teslemetry Powerwall approval window expires (#181334)

This commit is contained in:
Brett Adams
2026-09-21 19:40:52 +02:00
committed by GitHub
parent 71e382e3a8
commit c945eab58a
3 changed files with 79 additions and 0 deletions
@@ -451,6 +451,7 @@ class EnergySiteSubentryFlowHandler(ConfigSubentryFlow):
self._discovered_host: str = ""
self._site_id: int | None = None
self._site_name: str = ""
self._approval_expired: bool = False
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -603,6 +604,11 @@ class EnergySiteSubentryFlowHandler(ConfigSubentryFlow):
if user_input is None:
return self.async_show_form(step_id="pair")
if self._approval_expired:
# The user saw the expired-window notice and submitted to try again.
self._approval_expired = False
return await self._async_begin_pairing()
try:
client = await self._find_authorized_client()
except PowerwallLookupError:
@@ -618,6 +624,10 @@ class EnergySiteSubentryFlowHandler(ConfigSubentryFlow):
return await self.async_step_credentials()
if client.state == AuthorizedClientState.PENDING_VERIFICATION:
return self.async_show_form(step_id="pair", errors={"base": "key_pending"})
if client.state == AuthorizedClientState.PENDING_VERIFICATION_TIMEOUT:
# Surface the expiry; the user's next submit reopens the window.
self._approval_expired = True
return self.async_show_form(step_id="pair", errors={"base": "key_expired"})
# An unrecognized state reported as pending would trap the user forever.
LOGGER.debug("Unrecognized authorized-client state: %s", client.state)
return self.async_show_form(step_id="pair", errors={"base": "cannot_connect"})
@@ -58,6 +58,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_password": "[%key:common::config_flow::error::invalid_auth%]",
"key_expired": "The approval window on your Powerwall closed before the key was approved. Submit to open a new window, flick the On/Off switch on your primary Powerwall off and back on to approve it, then submit again.",
"key_not_approved": "Your Powerwall system rejected Home Assistant's key because it has not been approved. Flick the On/Off switch on your primary Powerwall off and back on to approve it, then submit again.",
"key_not_registered": "Home Assistant's key is no longer registered on your Powerwall system. Restart setup to register it again.",
"key_pending": "Home Assistant's key has not been approved yet. Flick the On/Off switch on your primary Powerwall off and back on to approve it, then submit again."
@@ -2195,6 +2195,11 @@ async def test_add_authorized_client_failure_aborts(hass: HomeAssistant) -> None
[
pytest.param(InvalidResponse(), "cannot_connect", id="lookup_failure"),
pytest.param(_empty_clients(), "key_not_registered", id="key_not_registered"),
pytest.param(
_own_key_clients(AuthorizedClientState.PENDING_VERIFICATION_TIMEOUT),
"key_expired",
id="key_expired",
),
pytest.param(_own_key_clients("gremlin"), "cannot_connect", id="unknown_state"),
],
)
@@ -2228,6 +2233,69 @@ async def test_pair_step_second_lookup_errors(
assert result["errors"] == {"base": expected_error}
@pytest.mark.usefixtures("mock_rsa_key")
async def test_pair_step_timeout_retry_reopens_window_and_succeeds(
hass: HomeAssistant,
) -> None:
"""Submitting the expired form reopens the window and pairing can complete."""
entry = await _setup_account_no_subentry(hass)
client = _mock_powerwall_client()
with (
patch(
"tesla_fleet_api.teslemetry.energysite.TeslemetryEnergySite.find_authorized_clients",
new=AsyncMock(
side_effect=[
_empty_clients(),
_own_key_clients(
AuthorizedClientState.PENDING_VERIFICATION_TIMEOUT
),
_own_key_clients(
AuthorizedClientState.PENDING_VERIFICATION_TIMEOUT
),
_own_key_clients(AuthorizedClientState.VERIFIED),
]
),
),
patch(
"tesla_fleet_api.teslemetry.energysite.TeslemetryEnergySite.add_authorized_client",
new=AsyncMock(),
) as mock_add,
patch(
"homeassistant.components.teslemetry.config_flow.PowerwallClient",
return_value=client,
),
patch.object(hass.config_entries, "async_schedule_reload"),
):
result = await _start_add_flow_select_site(hass, entry)
assert result["step_id"] == "pair"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"], {}
)
assert result["step_id"] == "pair"
assert result["errors"] == {"base": "key_expired"}
# Submitting the expired form re-registers to open a fresh window.
result = await hass.config_entries.subentries.async_configure(
result["flow_id"], {}
)
assert result["step_id"] == "pair"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"], {}
)
assert result["step_id"] == "credentials"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"], {CONF_HOST: HOST, CONF_PASSWORD: PASSWORD}
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert mock_add.await_count == 2
@pytest.mark.usefixtures("mock_rsa_key")
@pytest.mark.parametrize(
("patch_target", "error"),