From f297f41dfa6da334ffcd594a31858c5447f48586 Mon Sep 17 00:00:00 2001 From: Colin Haven <10933733+virtitnerd@users.noreply.github.com> Date: Sat, 23 May 2026 21:11:44 -0400 Subject: [PATCH 1/4] fix: guard config flow against empty account list If get_linked_accounts() returns [] (login succeeds but no accounts on the profile), the user_step and reconfigure_step now return an error instead of silently falling through to an empty selection screen where no_accounts_selected fires in an unbreakable loop. Co-Authored-By: Claude Sonnet 4.6 --- .../national_grid/config_flow.py | 20 ++++++++-- tests/test_config_flow.py | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/custom_components/national_grid/config_flow.py b/custom_components/national_grid/config_flow.py index 42a9b5c..2756888 100644 --- a/custom_components/national_grid/config_flow.py +++ b/custom_components/national_grid/config_flow.py @@ -56,10 +56,17 @@ async def async_step_user( _LOGGER.exception(exception) _errors["base"] = "unknown" else: - await self.async_set_unique_id(slugify(self._username)) - self._abort_if_unique_id_configured() + if not self._accounts: + _LOGGER.error( + "Login succeeded but no accounts returned for %s", + self._username, + ) + _errors["base"] = "no_accounts_found" + else: + await self.async_set_unique_id(slugify(self._username)) + self._abort_if_unique_id_configured() - return await self.async_step_select_accounts() + return await self.async_step_select_accounts() return self.async_show_form( step_id="user", @@ -135,6 +142,13 @@ async def async_step_reconfigure( except NationalGridError as exception: _LOGGER.exception(exception) _errors["base"] = "unknown" + else: + if not self._accounts: + _LOGGER.error( + "Reconfigure: login succeeded but no accounts returned for %s", + reconfigure_entry.data[CONF_USERNAME], + ) + _errors["base"] = "no_accounts_found" current_selection = reconfigure_entry.data.get(CONF_SELECTED_ACCOUNTS, []) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index fe54132..b423d6b 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -528,6 +528,44 @@ async def test_reconfigure_unknown_error(hass: HomeAssistant) -> None: assert result["errors"]["base"] == "unknown" +async def test_user_step_no_accounts_found(hass: HomeAssistant) -> None: + """Test user step shows error when login succeeds but no accounts are returned.""" + with patch(PATCH_CLIENT) as mock_cls: + client = mock_cls.return_value + client.__aenter__ = AsyncMock(return_value=client) + client.__aexit__ = AsyncMock(return_value=False) + client.get_linked_accounts = AsyncMock(return_value=[]) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "no_accounts_found" + + +async def test_reconfigure_no_accounts_found(hass: HomeAssistant) -> None: + """Test reconfigure shows error when login succeeds but no accounts are returned.""" + entry = _make_reconfigure_entry(hass) + + with patch(PATCH_CLIENT) as mock_cls: + client = mock_cls.return_value + client.__aenter__ = AsyncMock(return_value=client) + client.__aexit__ = AsyncMock(return_value=False) + client.get_linked_accounts = AsyncMock(return_value=[]) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == "no_accounts_found" + + async def test_already_configured(hass: HomeAssistant) -> None: """Test that duplicate unique_id aborts.""" entry = MockConfigEntry( From 7568e062d7ea7359cf99c8aaac7199dac627f9c2 Mon Sep 17 00:00:00 2001 From: Colin Haven <10933733+virtitnerd@users.noreply.github.com> Date: Sat, 23 May 2026 21:12:48 -0400 Subject: [PATCH 2/4] fix: add no_accounts_found translation key Adds the error string referenced by the empty-account guard added in the previous commit. Co-Authored-By: Claude Sonnet 4.6 --- custom_components/national_grid/strings.json | 3 ++- custom_components/national_grid/translations/en.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/national_grid/strings.json b/custom_components/national_grid/strings.json index 0b12e49..1aecc9f 100644 --- a/custom_components/national_grid/strings.json +++ b/custom_components/national_grid/strings.json @@ -32,7 +32,8 @@ "auth": "Invalid username or password.", "connection": "Unable to connect to National Grid.", "unknown": "An unexpected error occurred.", - "no_accounts_selected": "Please select at least one account." + "no_accounts_selected": "Please select at least one account.", + "no_accounts_found": "Login succeeded but no National Grid accounts were found on this profile." }, "abort": { "already_configured": "This account is already configured.", diff --git a/custom_components/national_grid/translations/en.json b/custom_components/national_grid/translations/en.json index 0b12e49..1aecc9f 100644 --- a/custom_components/national_grid/translations/en.json +++ b/custom_components/national_grid/translations/en.json @@ -32,7 +32,8 @@ "auth": "Invalid username or password.", "connection": "Unable to connect to National Grid.", "unknown": "An unexpected error occurred.", - "no_accounts_selected": "Please select at least one account." + "no_accounts_selected": "Please select at least one account.", + "no_accounts_found": "Login succeeded but no National Grid accounts were found on this profile." }, "abort": { "already_configured": "This account is already configured.", From 9fc9b6f95aac9b26dfea991fd602e7ac24c11ec2 Mon Sep 17 00:00:00 2001 From: Colin Haven <10933733+virtitnerd@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:17 -0400 Subject: [PATCH 3/4] fix: use async_abort for no_accounts_found instead of form error Showing a form error when login succeeds but the profile has no accounts implies the user can fix it by editing credentials. An abort page is semantically correct: the flow cannot proceed and there is nothing to retry. Moves the no_accounts_found key from config.error to config.abort in both strings files. Co-Authored-By: Claude Sonnet 4.6 --- custom_components/national_grid/config_flow.py | 11 +++++------ custom_components/national_grid/strings.json | 4 ++-- .../national_grid/translations/en.json | 4 ++-- tests/test_config_flow.py | 13 ++++++------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/custom_components/national_grid/config_flow.py b/custom_components/national_grid/config_flow.py index 2756888..a8eaff3 100644 --- a/custom_components/national_grid/config_flow.py +++ b/custom_components/national_grid/config_flow.py @@ -61,12 +61,11 @@ async def async_step_user( "Login succeeded but no accounts returned for %s", self._username, ) - _errors["base"] = "no_accounts_found" - else: - await self.async_set_unique_id(slugify(self._username)) - self._abort_if_unique_id_configured() + return self.async_abort(reason="no_accounts_found") + await self.async_set_unique_id(slugify(self._username)) + self._abort_if_unique_id_configured() - return await self.async_step_select_accounts() + return await self.async_step_select_accounts() return self.async_show_form( step_id="user", @@ -148,7 +147,7 @@ async def async_step_reconfigure( "Reconfigure: login succeeded but no accounts returned for %s", reconfigure_entry.data[CONF_USERNAME], ) - _errors["base"] = "no_accounts_found" + return self.async_abort(reason="no_accounts_found") current_selection = reconfigure_entry.data.get(CONF_SELECTED_ACCOUNTS, []) diff --git a/custom_components/national_grid/strings.json b/custom_components/national_grid/strings.json index 1aecc9f..1986aad 100644 --- a/custom_components/national_grid/strings.json +++ b/custom_components/national_grid/strings.json @@ -32,11 +32,11 @@ "auth": "Invalid username or password.", "connection": "Unable to connect to National Grid.", "unknown": "An unexpected error occurred.", - "no_accounts_selected": "Please select at least one account.", - "no_accounts_found": "Login succeeded but no National Grid accounts were found on this profile." + "no_accounts_selected": "Please select at least one account." }, "abort": { "already_configured": "This account is already configured.", + "no_accounts_found": "Login succeeded but no National Grid accounts were found on this profile.", "reauth_successful": "Re-authentication successful.", "reconfigure_successful": "Account selection updated successfully." } diff --git a/custom_components/national_grid/translations/en.json b/custom_components/national_grid/translations/en.json index 1aecc9f..1986aad 100644 --- a/custom_components/national_grid/translations/en.json +++ b/custom_components/national_grid/translations/en.json @@ -32,11 +32,11 @@ "auth": "Invalid username or password.", "connection": "Unable to connect to National Grid.", "unknown": "An unexpected error occurred.", - "no_accounts_selected": "Please select at least one account.", - "no_accounts_found": "Login succeeded but no National Grid accounts were found on this profile." + "no_accounts_selected": "Please select at least one account." }, "abort": { "already_configured": "This account is already configured.", + "no_accounts_found": "Login succeeded but no National Grid accounts were found on this profile.", "reauth_successful": "Re-authentication successful.", "reconfigure_successful": "Account selection updated successfully." } diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index b423d6b..ff828c5 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -529,7 +529,7 @@ async def test_reconfigure_unknown_error(hass: HomeAssistant) -> None: async def test_user_step_no_accounts_found(hass: HomeAssistant) -> None: - """Test user step shows error when login succeeds but no accounts are returned.""" + """Test user step aborts when login succeeds but no accounts are returned.""" with patch(PATCH_CLIENT) as mock_cls: client = mock_cls.return_value client.__aenter__ = AsyncMock(return_value=client) @@ -545,13 +545,12 @@ async def test_user_step_no_accounts_found(hass: HomeAssistant) -> None: }, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"]["base"] == "no_accounts_found" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_accounts_found" async def test_reconfigure_no_accounts_found(hass: HomeAssistant) -> None: - """Test reconfigure shows error when login succeeds but no accounts are returned.""" + """Test reconfigure aborts when login succeeds but no accounts are returned.""" entry = _make_reconfigure_entry(hass) with patch(PATCH_CLIENT) as mock_cls: @@ -562,8 +561,8 @@ async def test_reconfigure_no_accounts_found(hass: HomeAssistant) -> None: result = await entry.start_reconfigure_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["errors"]["base"] == "no_accounts_found" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_accounts_found" async def test_already_configured(hass: HomeAssistant) -> None: From 9f0eac8fe2c697c308acf7a137ae417d7c47d3ad Mon Sep 17 00:00:00 2001 From: Colin Haven <10933733+virtitnerd@users.noreply.github.com> Date: Sun, 24 May 2026 15:48:13 -0400 Subject: [PATCH 4/4] fix: remove username from error log to avoid PII in logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log messages recording "no accounts returned" passed the username (an email address) as a format argument, which would appear in HA log files and diagnostics exports. Drop the argument — the message is diagnostic without identifying the account holder. Co-Authored-By: Claude Sonnet 4.6 --- custom_components/national_grid/config_flow.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/custom_components/national_grid/config_flow.py b/custom_components/national_grid/config_flow.py index a8eaff3..3683b9c 100644 --- a/custom_components/national_grid/config_flow.py +++ b/custom_components/national_grid/config_flow.py @@ -57,10 +57,7 @@ async def async_step_user( _errors["base"] = "unknown" else: if not self._accounts: - _LOGGER.error( - "Login succeeded but no accounts returned for %s", - self._username, - ) + _LOGGER.error("Login succeeded but no accounts returned") return self.async_abort(reason="no_accounts_found") await self.async_set_unique_id(slugify(self._username)) self._abort_if_unique_id_configured() @@ -144,8 +141,7 @@ async def async_step_reconfigure( else: if not self._accounts: _LOGGER.error( - "Reconfigure: login succeeded but no accounts returned for %s", - reconfigure_entry.data[CONF_USERNAME], + "Reconfigure: login succeeded but no accounts returned" ) return self.async_abort(reason="no_accounts_found")