From 3dc80136e788eb6b985a689e9dffd32308bd6b82 Mon Sep 17 00:00:00 2001 From: "Yanze (David) Wu" <133224895+David-Wu1119@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:34:43 -0700 Subject: [PATCH 01/24] Fix missing f-string prefix in local_todo error message (#182361) Co-authored-by: Claude Opus 5 --- homeassistant/components/local_todo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/local_todo/__init__.py b/homeassistant/components/local_todo/__init__.py index d4eaa0de3fc823..c1c87e70f72f74 100644 --- a/homeassistant/components/local_todo/__init__.py +++ b/homeassistant/components/local_todo/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LocalTodoConfigEntry) -> try: await store.async_load() except OSError as err: - raise ConfigEntryNotReady("Failed to load file {path}: {err}") from err + raise ConfigEntryNotReady(f"Failed to load file {path}: {err}") from err entry.runtime_data = store From a09f39b521671fd4f71316dae7600b47625c3bd6 Mon Sep 17 00:00:00 2001 From: rrooggiieerr Date: Wed, 16 Sep 2026 06:05:29 +0200 Subject: [PATCH 02/24] Add my-PV reauth flow (#181674) Co-authored-by: Robert Resch Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> --- homeassistant/components/my_pv/config_flow.py | 47 ++++++++ .../components/my_pv/quality_scale.yaml | 2 +- homeassistant/components/my_pv/strings.json | 14 ++- tests/components/my_pv/test_config_flow.py | 104 ++++++++++++++++++ 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/my_pv/config_flow.py b/homeassistant/components/my_pv/config_flow.py index 78811ab00af3a7..966bfdff2ff1d6 100644 --- a/homeassistant/components/my_pv/config_flow.py +++ b/homeassistant/components/my_pv/config_flow.py @@ -1,5 +1,6 @@ """Config flow for the my-PV integration.""" +from collections.abc import Mapping import logging from typing import Any, Final, override @@ -213,3 +214,49 @@ async def async_step_auth( errors=errors, description_placeholders=self.context["title_placeholders"], ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Confirm reauth dialog.""" + errors: dict[str, str] = {} + + reauth_entry = self._get_reauth_entry() + if user_input is not None: + user_input = {**reauth_entry.data, **user_input} + host = user_input[CONF_HOST] + password = user_input[CONF_PASSWORD] + + device = MyPVLocalDevice(host, password) + try: + if not await device.connect(): + errors[CONF_BASE] = "cannot_connect" + except MyPVAuthenticationError: + errors[CONF_PASSWORD] = "invalid_password" + finally: + await device.disconnect() + + if not errors: + await self.async_set_unique_id(device.serial_number) + self._abort_if_unique_id_mismatch() + data = { + CONF_PASSWORD: password, + } + return self.async_update_reload_and_abort( + reauth_entry, data_updates=data + ) + + data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, user_input or {}) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=data_schema, + errors=errors, + description_placeholders=self.context["title_placeholders"], + ) diff --git a/homeassistant/components/my_pv/quality_scale.yaml b/homeassistant/components/my_pv/quality_scale.yaml index 4c73587b9000d9..87a42b5e1a2c27 100644 --- a/homeassistant/components/my_pv/quality_scale.yaml +++ b/homeassistant/components/my_pv/quality_scale.yaml @@ -42,7 +42,7 @@ rules: integration-owner: done log-when-unavailable: todo parallel-updates: todo - reauthentication-flow: todo + reauthentication-flow: done test-coverage: todo # Gold tier rules diff --git a/homeassistant/components/my_pv/strings.json b/homeassistant/components/my_pv/strings.json index 26662bfd30604c..28a5a13ba383ff 100644 --- a/homeassistant/components/my_pv/strings.json +++ b/homeassistant/components/my_pv/strings.json @@ -6,7 +6,8 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unique_id_mismatch": "The device serial number does not match the original device." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -36,6 +37,15 @@ "description": "[%key:component::my_pv::common::discovery_description%]", "title": "{name}" }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "[%key:component::my_pv::common::password_description%]" + }, + "title": "{name} password" + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]" @@ -49,7 +59,7 @@ }, "exceptions": { "auth_error": { - "message": "Authentication failed. Remove and add the integration again with the current device password." + "message": "Authentication failed, please reauthenticate." }, "cannot_connect": { "message": "[%key:common::config_flow::error::cannot_connect%]" diff --git a/tests/components/my_pv/test_config_flow.py b/tests/components/my_pv/test_config_flow.py index 88d88da1ff011f..c5531c3474d05a 100644 --- a/tests/components/my_pv/test_config_flow.py +++ b/tests/components/my_pv/test_config_flow.py @@ -442,3 +442,107 @@ async def test_step_discovery_auth_wrong_password( CONF_PASSWORD: "test-password", } assert result["result"].unique_id == ELWA2_SERIAL_NUMBER + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_step_reauth( + hass: HomeAssistant, + mock_my_pv_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for reauth.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: "new-password"} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] == "new-password" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_step_reauth_wrong_password( + hass: HomeAssistant, + mock_my_pv_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for reauth with an incorrect password.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert not result["errors"] + + mock_my_pv_client.connect.side_effect = MyPVAuthenticationError() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: "wrong-password"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"]["password"] == "invalid_password" + + mock_my_pv_client.connect.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] == "new-password" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_step_reauth_cannot_connect( + hass: HomeAssistant, + mock_my_pv_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for reauth if we can not connect to device.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert not result["errors"] + + mock_my_pv_client.connect.return_value = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: "new-password"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == "cannot_connect" + + mock_my_pv_client.connect.return_value = True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] == "new-password" From d7dc840238ae33bee68d8a859c33883b0f1323ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2026 07:09:10 +0200 Subject: [PATCH 03/24] Do not log the tracked location in NWS (#182350) --- homeassistant/components/nws/coordinator.py | 7 +--- tests/components/nws/test_init.py | 36 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nws/coordinator.py b/homeassistant/components/nws/coordinator.py index 36b69ef224f5aa..17aeb04a447d4e 100644 --- a/homeassistant/components/nws/coordinator.py +++ b/homeassistant/components/nws/coordinator.py @@ -115,12 +115,7 @@ async def _async_check_location_change(self) -> None: self._location_entity_id, ) return - _LOGGER.info( - "NWS API updated: station %s at (%.4f, %.4f)", - new_nws.station, - coordinates.latitude, - coordinates.longitude, - ) + _LOGGER.info("NWS API updated: station %s", new_nws.station) self.nws = new_nws self.name = f"NWS observation station {new_nws.station}" runtime_data = self.config_entry.runtime_data diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py index 6d2f46ef317fbd..b2c0ef5590340c 100644 --- a/tests/components/nws/test_init.py +++ b/tests/components/nws/test_init.py @@ -1,5 +1,7 @@ """Tests for init module.""" +import logging + from pynws import NwsNoDataError import pytest @@ -195,6 +197,40 @@ async def test_location_change_updates_coordinates( assert coordinator.name == f"NWS observation station {new_station}" +async def test_location_change_does_not_log_coordinates( + hass: HomeAssistant, + mock_simple_nws, + location_entity_config: dict, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that the tracked location is not written to the log.""" + caplog.set_level(logging.INFO) + entity = location_entity_config["entry"] + hass.states.async_set( + entity.entity_id, + "home", + {ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0}, + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"]) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + caplog.clear() + hass.states.async_set( + entity.entity_id, + "away", + {ATTR_LATITUDE: 41.0, ATTR_LONGITUDE: -81.0}, + ) + await hass.async_block_till_done() + + assert "NWS API updated: station" in caplog.text + assert "41.0000" not in caplog.text + assert "-81.0000" not in caplog.text + + async def test_location_change_resets_api_success_time( hass: HomeAssistant, mock_simple_nws, location_entity_config: dict ) -> None: From 5fd990135b959a6c80c9263f2e8f33c86caaa2ef Mon Sep 17 00:00:00 2001 From: "Yanze (David) Wu" <133224895+David-Wu1119@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:09:45 -0700 Subject: [PATCH 04/24] Fix missing f-string prefix in wiz error message (#182363) Co-authored-by: Claude Opus 5 --- homeassistant/components/wiz/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 52b6996318587b..1bc3f1ea6a3acd 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WizConfigEntry) -> bool: # since its the wrong device. As soon as the device comes back # online the ip will get updated and setup will proceed. raise ConfigEntryNotReady( - "Found bulb {bulb.mac} at {ip_address}, expected {entry.unique_id}" + f"Found bulb {bulb.mac} at {ip_address}, expected {entry.unique_id}" ) coordinator = WizCoordinator(hass, entry, bulb) From 4e2e79a4adc569714eebf22cd170a36bface626b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 16 Sep 2026 06:10:09 +0100 Subject: [PATCH 05/24] Remove workarounds for copilot skills (#182353) --- .github/copilot-instructions.md | 1 + .../instructions/integrations.instructions.md | 51 ------------------ .pre-commit-config.yaml | 2 +- script/gen_copilot_instructions.py | 54 +------------------ 4 files changed, 4 insertions(+), 104 deletions(-) delete mode 100644 .github/instructions/integrations.instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 17898f85ba54c7..9779e09f479727 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,6 +6,7 @@ - Start review comments with a short, one-sentence summary of the suggested fix. - Do not comment on code style, formatting or linting issues. +- When reviewing changes under `homeassistant/components/` or `tests/components/`, use the `ha-integration-knowledge` skill as the primary reference. - Flag comments that over-explain straightforward code, narrate the obvious, or read like AI commentary (multi-sentence justifications for a single line). - A Pull Request with a dependency version bump should only contain changes required for the version bump. If the PR includes other changes, request that they are removed from the PR. - Check that the PR description is complete and filled in according to the PR template included below. Every section and checklist item from the template must be present, except the `## Breaking change` section which is optional. No content from the template should be missing, except for HTML comments and Markdown link reference definitions (lines of the form `[name]: url`), which do not render and cannot be verified from the description. Even unchecked checkboxes or empty sections must be present. This is a hard requirement. diff --git a/.github/instructions/integrations.instructions.md b/.github/instructions/integrations.instructions.md deleted file mode 100644 index 420ca7d65b1a5a..00000000000000 --- a/.github/instructions/integrations.instructions.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -applyTo: "homeassistant/components/**, tests/components/**" -excludeAgent: "cloud-agent" ---- - - - - -## File Locations -- **Integration code**: `./homeassistant/components//` -- **Integration tests**: `./tests/components//` - -## General guidelines - -- When looking for examples, prefer integrations with the platinum or gold quality scale level first. -- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries. -- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names. -- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely. -- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast. -- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining. -- Integrations should not implement fixes or workarounds for limitations in libraries. Instead, the library should be updated to fix the issue. - -The following platforms have extra guidelines: -- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection -- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues - -## Entity platforms - -- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`. -- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead. - -## Integration Quality Scale - -- When validating the quality scale rules, check them at https://developers.home-assistant.io/docs/core/integration-quality-scale/rules -- When implementing or reviewing an integration, always consider the quality scale rules, since they promote best practices. - -Template scale file: `./script/scaffold/templates/integration/integration/quality_scale.yaml` - -### How Rules Apply -1. **Check `manifest.json`**: Look for `"quality_scale"` key to determine integration level -2. **Bronze Rules**: Always required for any integration with quality scale -3. **Higher Tier Rules**: Only apply if integration targets that tier or higher -4. **Rule Status**: Check `quality_scale.yaml` in integration folder for: - - `done`: Rule implemented - - `exempt`: Rule doesn't apply (with reason in comment) - - `todo`: Rule needs implementation - - -## Testing Requirements - -- Tests should avoid interacting or mocking internal integration details. For more info, see https://developers.home-assistant.io/docs/development_testing/#writing-tests-for-integrations diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7496fcaffcdfbc..1d9a093a7e652c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -95,7 +95,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(AGENTS\.md|\.claude/skills/(?!github-pr-reviewer/).+/SKILL\.md|\.github/copilot-instructions\.md|script/gen_copilot_instructions\.py)$ + files: ^(AGENTS\.md|\.github/copilot-instructions\.md|script/gen_copilot_instructions\.py)$ - id: hassfest name: hassfest entry: script/run-in-env.sh python3 -m script.hassfest diff --git a/script/gen_copilot_instructions.py b/script/gen_copilot_instructions.py index 020d58252f8c13..0383253c94b9a1 100755 --- a/script/gen_copilot_instructions.py +++ b/script/gen_copilot_instructions.py @@ -1,8 +1,5 @@ #!/usr/bin/env python3 -"""Generate .github/copilot-instructions.md from AGENTS.md and skills. - -Necessary until copilot can handle skills. -""" +"""Generate .github/copilot-instructions.md from AGENTS.md and the PR template.""" from pathlib import Path import sys @@ -13,10 +10,6 @@ AGENTS_FILE = Path("AGENTS.md") OUTPUT_FILE = Path(".github/copilot-instructions.md") -INTEGRATION_SKILL_FILE = Path(".claude/skills/ha-integration-knowledge/SKILL.md") -INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE = Path( - ".github/instructions/integrations.instructions.md" -) PR_TEMPLATE_FILE = Path(".github/PULL_REQUEST_TEMPLATE.md") COPILOT_SPECIFIC_INSTRUCTIONS = """ @@ -24,6 +17,7 @@ - Start review comments with a short, one-sentence summary of the suggested fix. - Do not comment on code style, formatting or linting issues. +- When reviewing changes under `homeassistant/components/` or `tests/components/`, use the `ha-integration-knowledge` skill as the primary reference. - Flag comments that over-explain straightforward code, narrate the obvious, or read like AI commentary (multi-sentence justifications for a single line). - A Pull Request with a dependency version bump should only contain changes required for the version bump. If the PR includes other changes, request that they are removed from the PR. - Check that the PR description is complete and filled in according to the PR template included below. Every section and checklist item from the template must be present, except the `## Breaking change` section which is optional. No content from the template should be missing, except for HTML comments and Markdown link reference definitions (lines of the form `[name]: url`), which do not render and cannot be verified from the description. Even unchecked checkboxes or empty sections must be present. This is a hard requirement. @@ -37,41 +31,6 @@ ``` """ -INTEGRATION_PATH_SPECIFIC_INSTRUCTIONS = """--- -applyTo: "homeassistant/components/**, tests/components/**" -excludeAgent: "cloud-agent" ---- -""" - - -def _strip_frontmatter(text: str) -> str: - """Strip YAML frontmatter from the start of a markdown document.""" - if not text.startswith("---\n"): - return text - - end = text.find("\n---\n", 4) - if end == -1: - return text - - return text[end + len("\n---\n") :].lstrip("\n") - - -def generate_integration_path_specific_instructions() -> str: - """Generate instructions for integration paths.""" - if not INTEGRATION_SKILL_FILE.exists(): - print(f"Error: {INTEGRATION_SKILL_FILE} not found") - sys.exit(1) - - skill_content = _strip_frontmatter(INTEGRATION_SKILL_FILE.read_text()) - - return ( - INTEGRATION_PATH_SPECIFIC_INSTRUCTIONS - + "\n" - + GENERATED_MESSAGE - + "\n" - + skill_content - ) - def generate_output() -> str: """Generate the copilot-instructions.md content.""" @@ -119,22 +78,13 @@ def main(validate: bool = False) -> int: return 1 main_content = generate_output() - integration_path_specific_content = ( - generate_integration_path_specific_instructions() - ) if validate: check_file(OUTPUT_FILE, main_content) - check_file( - INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE, integration_path_specific_content - ) return 0 OUTPUT_FILE.write_text(main_content) print(f"Generated {OUTPUT_FILE}") - - INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE.write_text(integration_path_specific_content) - print(f"Generated {INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE}") return 0 From b796adea21e3e8ac6b3a56eb547b2bcff41538b5 Mon Sep 17 00:00:00 2001 From: "Yanze (David) Wu" <133224895+David-Wu1119@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:10:33 -0700 Subject: [PATCH 06/24] Fix missing f-string prefix in local_calendar error message (#182360) Co-authored-by: Claude Opus 5 --- homeassistant/components/local_calendar/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/local_calendar/__init__.py b/homeassistant/components/local_calendar/__init__.py index 23c0c6a62d1fa1..3d05a806fd6286 100644 --- a/homeassistant/components/local_calendar/__init__.py +++ b/homeassistant/components/local_calendar/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry( try: await store.async_load() except OSError as err: - raise ConfigEntryNotReady("Failed to load file {path}: {err}") from err + raise ConfigEntryNotReady(f"Failed to load file {path}: {err}") from err entry.runtime_data = store From 9a69397e7123c5ec7c55af6436686f6286df08c0 Mon Sep 17 00:00:00 2001 From: "Yanze (David) Wu" <133224895+David-Wu1119@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:11:26 -0700 Subject: [PATCH 07/24] Fix missing f-string prefix in prowl error message (#182362) Co-authored-by: Claude Opus 5 --- homeassistant/components/prowl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/prowl/__init__.py b/homeassistant/components/prowl/__init__.py index 4f744e25bf7de5..7bfe8bafdb43d3 100644 --- a/homeassistant/components/prowl/__init__.py +++ b/homeassistant/components/prowl/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except prowlpy.APIError as ex: if str(ex).startswith("Not accepted: exceeded rate limit"): raise ConfigEntryNotReady("Prowl API rate limit exceeded") from ex - raise ConfigEntryError("Failed to validate Prowl API key ({ex})") from ex + raise ConfigEntryError(f"Failed to validate Prowl API key ({ex})") from ex await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From 5dc13f35017e7df0dba66ff9e7c9cb25c27bc0b1 Mon Sep 17 00:00:00 2001 From: "Yanze (David) Wu" <133224895+David-Wu1119@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:12:08 -0700 Subject: [PATCH 08/24] Fix missing f-string prefix in egauge error message (#182359) Co-authored-by: Claude Opus 5 --- homeassistant/components/egauge/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/egauge/coordinator.py b/homeassistant/components/egauge/coordinator.py index e6077a692f2be8..956f4335113df4 100644 --- a/homeassistant/components/egauge/coordinator.py +++ b/homeassistant/components/egauge/coordinator.py @@ -98,7 +98,7 @@ async def _async_update_data(self) -> EgaugeData: EgaugeException, ) as err: # will raise ConfigEntryAuthFailed once reauth is implemented - raise ConfigEntryError("Error fetching device info: {err}") from err + raise ConfigEntryError(f"Error fetching device info: {err}") from err except ConnectError as err: raise UpdateFailed(f"Error fetching device info: {err}") from err From 79efc68236017e1ae8092d151cb934ebeba118ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 07:12:52 +0200 Subject: [PATCH 09/24] Update pnpm to 12.4.0 (#182357) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tests/e2e/package.json | 2 +- tests/e2e/pnpm-lock.yaml | 131 ++++++++++++++++++++++++++++----------- 2 files changed, 95 insertions(+), 38 deletions(-) diff --git a/tests/e2e/package.json b/tests/e2e/package.json index d739c15e5267de..19b919b61ea4d8 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "End-to-end browser tests for Home Assistant Core", "private": true, - "packageManager": "pnpm@12.3.4", + "packageManager": "pnpm@12.4.0", "scripts": { "test": "playwright test" }, diff --git a/tests/e2e/pnpm-lock.yaml b/tests/e2e/pnpm-lock.yaml index c52be4c7bff088..e27ae8fce77b8d 100644 --- a/tests/e2e/pnpm-lock.yaml +++ b/tests/e2e/pnpm-lock.yaml @@ -7,96 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.3.4 - version: 12.3.4 + specifier: 12.4.0 + version: 12.4.0 packages: - '@pnpm/exe.darwin-arm64@12.3.4': - resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} + '@pnpm/exe.android-arm64@12.4.0': + resolution: {integrity: sha512-sAwslzCw74OpqK2P9l39cgdrRWHSqf5wEjB58JEzeVX6wdLvaHyg9i/GeRK5Ou6PZNA3AkD4yLO3c/y7UqOf8w==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.4.0': + resolution: {integrity: sha512-Ps3Gz0OrqYjuRfhzeNqcvIow6QmNrU3BSzMxJu5rUCSl7inVUUFX2gZbNL9naQsNLoorKCdxUf4N+WI9Mr1WBg==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.4.0': + resolution: {integrity: sha512-75EYiF8GuiTsnvP4cbZsviMBjohXUYtg2zjD4gdfhqxlU1y9Nj+KdEsbH4jfgB/FgyJSYPB2rqfmvvgLnRlVug==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.3.4': - resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} + '@pnpm/exe.darwin-x64@12.4.0': + resolution: {integrity: sha512-b74TzBg8lxl0lqZImGOXpRbAGcqVKX+UMckl3wG+KH52yYHI86VBJP86HbJX30HJ/EFeC6Tgbz2E4b5hhKRvdA==} cpu: [x64] os: [darwin] - '@pnpm/exe.linux-arm64-musl@12.3.4': - resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} + '@pnpm/exe.freebsd-x64@12.4.0': + resolution: {integrity: sha512-A45d6axdQIFNyNTYZAC64wh5+6LJ6dNKhNAoNMNi/EC5y9CRfv0GL3ZYDBqpkmsN9KMAkVsFWizq/Ng6xtjnhw==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.4.0': + resolution: {integrity: sha512-yS6DNel7twfEUoW1yka1hpQrnhRe/s1JZ1MThVfgrmNQrNaPyHxQvAihm/GtL2CM6OeMV6z5532H/W/yDjQp5g==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.3.4': - resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} + '@pnpm/exe.linux-arm64@12.4.0': + resolution: {integrity: sha512-6npQUwq3D/WXbTgR4evApEKQ9ITznZ6Iz/dptcjBzA7+wSLTaYkK98Od2wsHCoPmEFb9tGtM+cvG82+wy0o9uA==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.3.4': - resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} + '@pnpm/exe.linux-ppc64@12.4.0': + resolution: {integrity: sha512-7shJ4WytyvBEdjrbIDqx2gDAH7sr0HBDooTmnNQ7DlzcLeeGi7Yqir7gJjOTm6s9S1cVnT56IwmqnnwQMP1HTQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.4.0': + resolution: {integrity: sha512-q03EGUFoe/oOU/67E3sEAePr3qjFu8xrsX+WOXTodcFXfO5FondC6TPs7BSwhTiV27j3jeDP56qhJP05pXn+vw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.4.0': + resolution: {integrity: sha512-GZ5YellCtNnaLe9zVj9l3avlw9CbSzExUFDh7v+tVbLfOOfkkRLpx2lsw1ytJ/nFWvxF2TO6M6Q9JDT7q8svRg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.4.0': + resolution: {integrity: sha512-c8YyjVL39L48tRg9i3iw3d90fN6q84UjxlgWBhplcBOpzCgmglDVkPMtEAVDC+t9iobvYzs2hJnmTqLaA87MVA==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.3.4': - resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} + '@pnpm/exe.linux-x64@12.4.0': + resolution: {integrity: sha512-SQVgRkcR4Xyqf8+VNbtY0rtcEnfDq48RhH30HWo2/UfqKEfle2rOMyGZOmN1DbMw4ZzG5mWYoC81O7ZqHFZcPw==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.3.4': - resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} + '@pnpm/exe.win32-arm64@12.4.0': + resolution: {integrity: sha512-ZamXPhx0X6APZJAIkcH+K9KAsKcTYwxEgOyTQ/NXE+2UHBT9bRnSQ91sBeY6ELNd58V5cmMAkXSW5xmU+2ry+w==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.3.4': - resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} + '@pnpm/exe.win32-x64@12.4.0': + resolution: {integrity: sha512-b8bLaprnpYi/2zN0M3H9RDAHuxCP3fmV5agPPwdp9fIdjNSUkV7V/RC3L4oWwbZvVgYEjjFLx1RuJOdltoKP6g==} cpu: [x64] os: [win32] - pnpm@12.3.4: - resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} + pnpm@12.4.0: + resolution: {integrity: sha512-N1NsJu1Aq0E0tlEeCfayfz67RWh0aPJAbKOAUnmk5coVjBkxNQrZd01qshCNcbPbrrOZQxWSlDdeTQU+jgVoXA==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.darwin-arm64@12.3.4': + '@pnpm/exe.android-arm64@12.4.0': + optional: true + + '@pnpm/exe.android-x64@12.4.0': + optional: true + + '@pnpm/exe.darwin-arm64@12.4.0': + optional: true + + '@pnpm/exe.darwin-x64@12.4.0': + optional: true + + '@pnpm/exe.freebsd-x64@12.4.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.4.0': + optional: true + + '@pnpm/exe.linux-arm64@12.4.0': optional: true - '@pnpm/exe.darwin-x64@12.3.4': + '@pnpm/exe.linux-ppc64@12.4.0': optional: true - '@pnpm/exe.linux-arm64-musl@12.3.4': + '@pnpm/exe.linux-riscv64@12.4.0': optional: true - '@pnpm/exe.linux-arm64@12.3.4': + '@pnpm/exe.linux-s390x@12.4.0': optional: true - '@pnpm/exe.linux-x64-musl@12.3.4': + '@pnpm/exe.linux-x64-musl@12.4.0': optional: true - '@pnpm/exe.linux-x64@12.3.4': + '@pnpm/exe.linux-x64@12.4.0': optional: true - '@pnpm/exe.win32-arm64@12.3.4': + '@pnpm/exe.win32-arm64@12.4.0': optional: true - '@pnpm/exe.win32-x64@12.3.4': + '@pnpm/exe.win32-x64@12.4.0': optional: true - pnpm@12.3.4: + pnpm@12.4.0: optionalDependencies: - '@pnpm/exe.darwin-arm64': 12.3.4 - '@pnpm/exe.darwin-x64': 12.3.4 - '@pnpm/exe.linux-arm64': 12.3.4 - '@pnpm/exe.linux-arm64-musl': 12.3.4 - '@pnpm/exe.linux-x64': 12.3.4 - '@pnpm/exe.linux-x64-musl': 12.3.4 - '@pnpm/exe.win32-arm64': 12.3.4 - '@pnpm/exe.win32-x64': 12.3.4 + '@pnpm/exe.android-arm64': 12.4.0 + '@pnpm/exe.android-x64': 12.4.0 + '@pnpm/exe.darwin-arm64': 12.4.0 + '@pnpm/exe.darwin-x64': 12.4.0 + '@pnpm/exe.freebsd-x64': 12.4.0 + '@pnpm/exe.linux-arm64': 12.4.0 + '@pnpm/exe.linux-arm64-musl': 12.4.0 + '@pnpm/exe.linux-ppc64': 12.4.0 + '@pnpm/exe.linux-riscv64': 12.4.0 + '@pnpm/exe.linux-s390x': 12.4.0 + '@pnpm/exe.linux-x64': 12.4.0 + '@pnpm/exe.linux-x64-musl': 12.4.0 + '@pnpm/exe.win32-arm64': 12.4.0 + '@pnpm/exe.win32-x64': 12.4.0 --- lockfileVersion: '9.0' From e583ba1408ab97333c7873ab3b6d4bad92d43fcd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2026 07:13:32 +0200 Subject: [PATCH 10/24] Do not log IBANs and account numbers in FinTS (#182342) --- homeassistant/components/fints/sensor.py | 16 ++-- tests/components/fints/test_sensor.py | 110 +++++++++++++++++++++++ 2 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 tests/components/fints/test_sensor.py diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index c1afe9e3b7f4ce..7a1ec4a347e8bf 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -87,28 +87,24 @@ def setup_platform( for account in balance_accounts: if config[CONF_ACCOUNTS] and account.iban not in account_config: - _LOGGER.debug("Skipping account %s for bank %s", account.iban, fints_name) + _LOGGER.debug("Skipping account for bank %s", fints_name) continue if not (account_name := account_config.get(account.iban)): account_name = f"{fints_name} - {account.iban}" accounts.append(FinTsAccount(client, account, account_name)) - _LOGGER.debug("Creating account %s for bank %s", account.iban, fints_name) + _LOGGER.debug("Creating account for bank %s", fints_name) for account in holdings_accounts: if config[CONF_HOLDINGS] and account.accountnumber not in holdings_config: - _LOGGER.debug( - "Skipping holdings %s for bank %s", account.accountnumber, fints_name - ) + _LOGGER.debug("Skipping holdings for bank %s", fints_name) continue account_name = holdings_config.get(account.accountnumber) if not account_name: account_name = f"{fints_name} - {account.accountnumber}" accounts.append(FinTsHoldingsAccount(client, account, account_name)) - _LOGGER.debug( - "Creating holdings %s for bank %s", account.accountnumber, fints_name - ) + _LOGGER.debug("Creating holdings for bank %s", fints_name) add_entities(accounts, True) @@ -216,9 +212,7 @@ def detect_accounts(self) -> tuple[list, list]: else: _LOGGER.warning( - "Could not determine type of account %s from %s", - account.iban, - self.client.user_id, + "Could not determine type of account for bank %s", self.name ) return balance_accounts, holdings_accounts diff --git a/tests/components/fints/test_sensor.py b/tests/components/fints/test_sensor.py new file mode 100644 index 00000000000000..26cbce6aef01d3 --- /dev/null +++ b/tests/components/fints/test_sensor.py @@ -0,0 +1,110 @@ +"""Tests for the FinTS sensor platform.""" + +import logging +from unittest.mock import MagicMock, patch + +from fints.client import BankIdentifier, FinTSOperations +import pytest + +from homeassistant.components.fints.sensor import SEPAAccount +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +BANK_INFORMATION = { + "bank_identifier": BankIdentifier(country_identifier="280", bank_code="50010517"), + "currency": "EUR", + "customer_id": "0815", + "owner_name": ["SURNAME, FIRSTNAME"], + "subaccount_number": None, + "supported_operations": { + FinTSOperations.GET_BALANCE: True, + FinTSOperations.GET_HOLDINGS: True, + FinTSOperations.GET_SEPA_ACCOUNTS: True, + }, +} + +# GIRO2 and DEPOT2 are deliberately left out of the configuration below, so the +# platform skips them. UNKNOWN has no type and matches neither config. +ACCOUNT_TYPES = {"GIRO1": 5, "GIRO2": 5, "DEPOT1": 33, "DEPOT2": 33, "UNKNOWN": None} + +CONFIG = { + "sensor": { + "platform": "fints", + "bank_identification_number": "12345678", + "username": "user", + "pin": "1234", + "url": "https://example.com", + "name": "Test Bank", + "accounts": [{"account": "GIRO1", "name": "Checking"}], + "holdings": [{"account": "DEPOT1", "name": "Depot"}], + } +} + + +def _sepa_account(identifier: str) -> SEPAAccount: + """Build a SEPA account with the same IBAN and account number.""" + return SEPAAccount( + iban=identifier, + bic="BANCODELTEST", + accountnumber=identifier, + subaccount=None, + blz="12345", + ) + + +@pytest.fixture +def mock_bank() -> MagicMock: + """Return a bank that serves one of every account we care about.""" + bank = MagicMock() + bank.get_sepa_accounts.return_value = [ + _sepa_account(identifier) for identifier in ACCOUNT_TYPES + ] + bank.get_information.return_value = { + "accounts": [ + BANK_INFORMATION + | {"account_number": identifier, "iban": identifier, "type": account_type} + for identifier, account_type in ACCOUNT_TYPES.items() + ] + } + bank.get_balance.return_value.amount.amount = 1234.56 + bank.get_balance.return_value.amount.currency = "EUR" + bank.get_holdings.return_value = [] + return bank + + +async def test_setup_platform( + hass: HomeAssistant, mock_bank: MagicMock, caplog: pytest.LogCaptureFixture +) -> None: + """Only configured accounts get a sensor, and the rest are skipped.""" + caplog.set_level(logging.DEBUG) + + with patch( + "homeassistant.components.fints.sensor.FinTS3PinTanClient", + return_value=mock_bank, + ): + assert await async_setup_component(hass, "sensor", CONFIG) + await hass.async_block_till_done() + + assert hass.states.get("sensor.checking").state == "1234.56" + assert hass.states.get("sensor.depot").state == "0" + + assert "Skipping account for bank Test Bank" in caplog.text + assert "Skipping holdings for bank Test Bank" in caplog.text + assert "Could not determine type of account for bank Test Bank" in caplog.text + + +async def test_account_identifiers_are_not_logged( + hass: HomeAssistant, mock_bank: MagicMock, caplog: pytest.LogCaptureFixture +) -> None: + """No IBAN or account number ends up in the log.""" + caplog.set_level(logging.DEBUG) + + with patch( + "homeassistant.components.fints.sensor.FinTS3PinTanClient", + return_value=mock_bank, + ): + assert await async_setup_component(hass, "sensor", CONFIG) + await hass.async_block_till_done() + + for identifier in ACCOUNT_TYPES: + assert identifier not in caplog.text From f529a36625b5b4578de9563afe4c37d4fc3c6fb9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 07:13:50 +0200 Subject: [PATCH 11/24] Update uv to 0.12.11 (#182355) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e43d5573226b55..b93ae1f7d53da2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -72,7 +72,7 @@ standard-telnetlib==3.13.0 typing-extensions>=4.16.0,<5.0 ulid-transform==2.2.9 urllib3>=2.0 -uv==0.12.10 +uv==0.12.11 webrtc-models==0.3.0 yarl==1.24.5 zeroconf==0.151.3 diff --git a/pyproject.toml b/pyproject.toml index be9900f7f7c55a..76ba37c13ee60d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dependencies = [ "typing-extensions>=4.16.0,<5.0", "ulid-transform==2.2.9", "urllib3>=2.0", - "uv==0.12.10", + "uv==0.12.11", "probatio==0.12.1", "yarl==1.24.5", "webrtc-models==0.3.0", diff --git a/requirements.txt b/requirements.txt index 9438564e1673bf..1120b50925f1e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,7 +57,7 @@ standard-telnetlib==3.13.0 typing-extensions>=4.16.0,<5.0 ulid-transform==2.2.9 urllib3>=2.0 -uv==0.12.10 +uv==0.12.11 webrtc-models==0.3.0 yarl==1.24.5 zeroconf==0.151.3 From 4ddbfcb047a2b6050a0021d7527ec3111ff5ac74 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 16 Sep 2026 00:14:36 -0500 Subject: [PATCH 12/24] Bump voip-utils (#182339) --- homeassistant/components/voip/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index a191b2289b4d08..5a782a367ef0a0 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["voip_utils"], "quality_scale": "internal", - "requirements": ["voip-utils==0.4.3"] + "requirements": ["voip-utils==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 03937333354dcb..fa2783101b68f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3418,7 +3418,7 @@ vizaio==0.7.0 vobject==0.9.9 # homeassistant.components.voip -voip-utils==0.4.3 +voip-utils==0.5.0 # homeassistant.components.volkszaehler volkszaehler==0.4.0 From 5dae5ff1f33989389e9e58494ce115d365d14bcb Mon Sep 17 00:00:00 2001 From: Anthony <210036686+Herbertmt978@users.noreply.github.com> Date: Wed, 16 Sep 2026 06:15:38 +0100 Subject: [PATCH 13/24] Bump aioaxlevpp to 0.1.1 (#182336) --- homeassistant/components/axle_energy/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/axle_energy/manifest.json b/homeassistant/components/axle_energy/manifest.json index 0f180300da7f7b..e84c39a084d3ec 100644 --- a/homeassistant/components/axle_energy/manifest.json +++ b/homeassistant/components/axle_energy/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["aioaxlevpp==0.1.0"] + "requirements": ["aioaxlevpp==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index fa2783101b68f8..09cece56b8680a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aioasuswrt==1.5.4 aioautomower==3.0.0 # homeassistant.components.axle_energy -aioaxlevpp==0.1.0 +aioaxlevpp==0.1.1 # homeassistant.components.azure_devops aioazuredevops==2.2.2 From 365fef6659292565e7e033861518b6b5832b7da6 Mon Sep 17 00:00:00 2001 From: dbl-0 Date: Tue, 15 Sep 2026 23:18:56 -0600 Subject: [PATCH 14/24] Write the backup store with private=True so the backup password is not world-readable (#182259) Co-authored-by: Claude Opus 5 (1M context) --- homeassistant/components/backup/store.py | 7 +- .../backup/snapshots/test_store.ambr | 152 ++++++++++++++++-- .../backup/snapshots/test_websocket.ambr | 32 ++-- tests/components/backup/test_store.py | 51 ++++++ 4 files changed, 211 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index 6215c88574ffff..b8c350f5aa1b9d 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -14,7 +14,7 @@ STORE_DELAY_SAVE = 30 STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 7 +STORAGE_VERSION_MINOR = 8 class StoredBackupData(TypedDict): @@ -39,6 +39,8 @@ def __init__(self, hass: HomeAssistant) -> None: STORAGE_KEY, max_readable_version=self._MAX_READABLE_VERSION, minor_version=STORAGE_VERSION_MINOR, + # The store persists the automatic backup password in plaintext. + private=True, ) @override @@ -85,6 +87,9 @@ async def _async_migrate_func( for backup in data["backups"]: backup["failed_addons"] = [] backup["failed_folders"] = [] + # Version 1.8 does not change the data. The minor version was bumped + # so that stores written before the store was made private are + # rewritten once, with mode 0600, on the first load. # Note: We allow reading data with major version 2 in which the unused key # data["config"]["schedule"]["state"] will be removed. The bump to 2 is diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index b82bb7c650fa58..d2f1b45934be12 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -44,7 +44,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -93,7 +93,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -142,7 +142,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -191,7 +191,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -250,7 +250,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -309,7 +309,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -362,7 +362,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -415,7 +415,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -468,7 +468,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -521,7 +521,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -577,7 +577,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -633,7 +633,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -695,7 +695,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -757,7 +757,131 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data7] + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), + 'failed_agent_ids': list([ + 'test.remote', + ]), + 'failed_folders': list([ + 'ssl', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + }), + }), + 'automatic_backups_configured': True, + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': 'hunter2', + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 8, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data7].1 + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), + 'failed_agent_ids': list([ + 'test.remote', + ]), + 'failed_folders': list([ + 'ssl', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + }), + }), + 'automatic_backups_configured': True, + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': 'hunter2', + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 8, 'version': 1, }) # --- diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 2b1d7399a7fd6a..2fd9453aef290b 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -1330,7 +1330,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1446,7 +1446,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1562,7 +1562,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1692,7 +1692,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -1969,7 +1969,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2083,7 +2083,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2197,7 +2197,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2313,7 +2313,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2431,7 +2431,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2547,7 +2547,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2667,7 +2667,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2791,7 +2791,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -2907,7 +2907,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -3023,7 +3023,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -3139,7 +3139,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- @@ -3255,7 +3255,7 @@ }), }), 'key': 'backup', - 'minor_version': 7, + 'minor_version': 8, 'version': 1, }) # --- diff --git a/tests/components/backup/test_store.py b/tests/components/backup/test_store.py index a016ab36f3d730..63e7cbb55045e3 100644 --- a/tests/components/backup/test_store.py +++ b/tests/components/backup/test_store.py @@ -302,6 +302,57 @@ def mock_delay_save() -> Generator[None]: "minor_version": 7, "version": 1, }, + { + "data": { + "backups": [ + { + "backup_id": "abc123", + "failed_addons": [ + { + "name": "Test add-on", + "slug": "test_addon", + "version": "1.0.0", + } + ], + "failed_agent_ids": ["test.remote"], + "failed_folders": ["ssl"], + } + ], + "config": { + "agents": { + "test.remote": { + "protected": True, + "retention": {"copies": None, "days": None}, + } + }, + "automatic_backups_configured": True, + "create_backup": { + "agent_ids": [], + "include_addons": None, + "include_all_addons": False, + "include_database": True, + "include_folders": None, + "name": None, + "password": "hunter2", + }, + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "retention": { + "copies": None, + "days": None, + }, + "schedule": { + "days": [], + "recurrence": "never", + "state": "never", + "time": None, + }, + }, + }, + "key": DOMAIN, + "minor_version": 8, + "version": 1, + }, ], ) async def test_store_migration( From c72b4e32de570ffeb6eb6df07c81f64c57161d8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:20:54 -0500 Subject: [PATCH 15/24] Bump yarl to 1.25.1 (#182345) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b93ae1f7d53da2..3eb7b74ba0d3f3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -74,7 +74,7 @@ ulid-transform==2.2.9 urllib3>=2.0 uv==0.12.11 webrtc-models==0.3.0 -yarl==1.24.5 +yarl==1.25.1 zeroconf==0.151.3 # Constrain pycryptodome to avoid vulnerability diff --git a/pyproject.toml b/pyproject.toml index 76ba37c13ee60d..fd1ebb770134b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ dependencies = [ "urllib3>=2.0", "uv==0.12.11", "probatio==0.12.1", - "yarl==1.24.5", + "yarl==1.25.1", "webrtc-models==0.3.0", "zeroconf==0.151.3", ] diff --git a/requirements.txt b/requirements.txt index 1120b50925f1e6..9cae7538b02e05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -59,5 +59,5 @@ ulid-transform==2.2.9 urllib3>=2.0 uv==0.12.11 webrtc-models==0.3.0 -yarl==1.24.5 +yarl==1.25.1 zeroconf==0.151.3 From 454318469bb4e5067f08a816a5c9e0a74e836cec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2026 07:21:44 +0200 Subject: [PATCH 16/24] Do not log the Start.ca API key (#182279) --- homeassistant/components/startca/sensor.py | 2 +- tests/components/startca/test_sensor.py | 26 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index afab0e2203ab06..bf11dc84b50adf 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -152,7 +152,7 @@ async def async_setup_platform( ts_data = StartcaData(websession, apikey, bandwidthcap) ret = await ts_data.async_update() if ret is False: - _LOGGER.error("Invalid Start.ca API key: %s", apikey) + _LOGGER.error("Invalid Start.ca API key") return name = config[CONF_NAME] diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index ef7b704a718841..6630e9b03e19f8 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -2,6 +2,8 @@ from http import HTTPStatus +import pytest + from homeassistant.components.startca.sensor import StartcaData from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, UnitOfInformation from homeassistant.core import HomeAssistant @@ -227,3 +229,27 @@ async def test_bad_json_decode( result = await scd.async_update() assert result is False + + +async def test_invalid_api_key( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that setup stops when Start.ca rejects the key.""" + config = { + "platform": "startca", + "api_key": "NOTAKEY", + "total_bandwidth": 400, + "monitored_variables": ["usage"], + } + aioclient_mock.get( + "https://www.start.ca/support/usage/api?key=NOTAKEY", + status=HTTPStatus.NOT_FOUND, + ) + + await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() + + assert hass.states.get("sensor.start_ca_usage") is None + assert "Invalid Start.ca API key" in caplog.text From 7f991e018caff050d616cf07e278ba60745b17ed Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Wed, 16 Sep 2026 01:25:59 -0400 Subject: [PATCH 17/24] Bump aioipp to 0.19.3 and log IPP parse errors at warning level (#182334) Co-authored-by: Claude Opus 5 (1M context) --- homeassistant/components/ipp/config_flow.py | 4 ++-- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- .../ipp/snapshots/test_diagnostics.ambr | 18 ++++++++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 6d555d1f999aa3..70a647227e73d9 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -78,7 +78,7 @@ async def async_step_user( _LOGGER.debug("IPP Connection/Response Error", exc_info=True) return self._show_setup_form({"base": "cannot_connect"}) except IPPParseError: - _LOGGER.debug("IPP Parse Error", exc_info=True) + _LOGGER.warning("IPP Parse Error", exc_info=True) return self.async_abort(reason="parse_error") except IPPVersionNotSupportedError: return self.async_abort(reason="ipp_version_error") @@ -147,7 +147,7 @@ async def async_step_zeroconf( _LOGGER.debug("IPP Connection/Response Error", exc_info=True) return self.async_abort(reason="cannot_connect") except IPPParseError: - _LOGGER.debug("IPP Parse Error", exc_info=True) + _LOGGER.warning("IPP Parse Error", exc_info=True) return self.async_abort(reason="parse_error") except IPPVersionNotSupportedError: return self.async_abort(reason="ipp_version_error") diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index bc69d43c3bc354..4294b20490e75f 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["deepmerge", "pyipp"], - "requirements": ["aioipp==0.19.0"], + "requirements": ["aioipp==0.19.3"], "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 09cece56b8680a..de884e26fc2d19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aioimmich==0.17.0 aioindiallsky==0.1.1 # homeassistant.components.ipp -aioipp==0.19.0 +aioipp==0.19.3 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/tests/components/ipp/snapshots/test_diagnostics.ambr b/tests/components/ipp/snapshots/test_diagnostics.ambr index e7817211f9f1c5..29966d46fabdf5 100644 --- a/tests/components/ipp/snapshots/test_diagnostics.ambr +++ b/tests/components/ipp/snapshots/test_diagnostics.ambr @@ -14,11 +14,15 @@ }), 'info': dict({ 'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF', + 'icons': list([ + ]), 'location': None, 'manufacturer': 'TEST', 'model': 'HA-1000 Series', 'more_info': 'http://192.168.1.31:80/PRESENTATION/BONJOUR', 'name': 'Test HA-1000 Series', + 'pages_per_minute': None, + 'pages_per_minute_color': None, 'printer_info': 'Test HA-1000 Series', 'printer_name': 'Test Printer', 'printer_uri_supported': list([ @@ -30,6 +34,8 @@ 'uuid': 'cfe92100-67c4-11d4-a45f-f8d027761251', 'version': '20.23.06HA', }), + 'input_trays': list([ + ]), 'markers': list([ dict({ 'color': '#000000', @@ -77,11 +83,23 @@ 'name': 'Yellow ink', }), ]), + 'output_trays': list([ + ]), 'state': dict({ 'message': None, 'printer_state': 'idle', 'reasons': None, }), + 'status': dict({ + 'accepting_jobs': None, + 'alerts': list([ + ]), + 'media_ready': list([ + ]), + 'queued_jobs': None, + 'supported': list([ + ]), + }), 'uris': list([ dict({ 'authentication': None, From 7819e3ad3c6679b3099e1ddbcd04f9e3a796414e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 16 Sep 2026 15:26:20 +1000 Subject: [PATCH 18/24] Bump teslemetry-stream to 0.13.2 (#182311) --- homeassistant/components/teslemetry/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 4b3f9e5a40f94f..77ec1881f49a83 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -12,6 +12,6 @@ "requirements": [ "aiopowerwall==0.3.2", "tesla-fleet-api==1.13.0", - "teslemetry-stream==0.13.1" + "teslemetry-stream==0.13.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index de884e26fc2d19..a950ee85f2f5b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3260,7 +3260,7 @@ tesla-powerwall==0.5.3 tesla-wall-connector==1.2.0 # homeassistant.components.teslemetry -teslemetry-stream==0.13.1 +teslemetry-stream==0.13.2 # homeassistant.components.tessie tessie-api==0.1.3 From 32ad04c584bafda784d1a19357142d7b3af776b8 Mon Sep 17 00:00:00 2001 From: Rasad Regmi <123284094+rasadregmi@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:11:56 +0545 Subject: [PATCH 19/24] Add missing stabilized binary sensor description for Xiaomi BLE scales (#182328) --- .../components/xiaomi_ble/binary_sensor.py | 3 ++ .../xiaomi_ble/test_binary_sensor.py | 31 ++++++++++++++++++- tests/components/xiaomi_ble/test_sensor.py | 8 ++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 14ba8062807333..9d8a32e63845dd 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -101,6 +101,9 @@ key=ExtendedBinarySensorDeviceClass.PRY_THE_DOOR, device_class=BinarySensorDeviceClass.TAMPER, ), + ExtendedBinarySensorDeviceClass.STABILIZED: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.STABILIZED, + ), ExtendedBinarySensorDeviceClass.TOOTHBRUSH: BinarySensorEntityDescription( key=ExtendedBinarySensorDeviceClass.TOOTHBRUSH, ), diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index e48679438a54d7..4c5ab407d4eeef 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from . import make_advertisement +from . import MISCALE_V2_SERVICE_INFO, make_advertisement from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.bluetooth import ( @@ -175,6 +175,35 @@ async def test_opening(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_miscale_v2_stabilized_binary_sensor(hass: HomeAssistant) -> None: + """Test MiScale V2 stabilized binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="50:FB:19:1B:B5:DC", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + inject_bluetooth_service_info_bleak(hass, MISCALE_V2_SERVICE_INFO) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + stabilized_sensor = hass.states.get( + "binary_sensor.mi_body_composition_scale_b5dc_stabilized" + ) + assert stabilized_sensor.state == STATE_ON + assert ( + stabilized_sensor.attributes[ATTR_FRIENDLY_NAME] + == "Mi Body Composition Scale (B5DC) Stabilized" + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_opening_problem_sensors(hass: HomeAssistant) -> None: """Test setting up a opening binary sensor with additional problem sensors.""" entry = MockConfigEntry( diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index d382dbaf8d00ab..b07c07800d2891 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -692,7 +692,7 @@ async def test_miscale_v1_uuid(hass: HomeAssistant) -> None: inject_bluetooth_service_info_bleak(hass, MISCALE_V1_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 mass_non_stabilized_sensor = hass.states.get( "sensor.mi_smart_scale_b5dc_weight_non_stabilized" @@ -734,7 +734,7 @@ async def test_miscale_v2_uuid(hass: HomeAssistant) -> None: inject_bluetooth_service_info_bleak(hass, MISCALE_V2_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 mass_non_stabilized_sensor = hass.states.get( "sensor.mi_body_composition_scale_b5dc_weight_non_stabilized" @@ -844,7 +844,7 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: inject_bluetooth_service_info_bleak(hass, MISCALE_V1_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 mass_non_stabilized_sensor = hass.states.get( "sensor.mi_smart_scale_b5dc_weight_non_stabilized" @@ -895,7 +895,7 @@ async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: inject_bluetooth_service_info_bleak(hass, MISCALE_V1_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 mass_non_stabilized_sensor = hass.states.get( "sensor.mi_smart_scale_b5dc_weight_non_stabilized" From 89b4a9a46b9fa7fc1ac131c4e074a4bd751a63bf Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 16 Sep 2026 08:20:09 +0200 Subject: [PATCH 20/24] KNX: Validate entity_category for read-only platforms (#182141) --- homeassistant/components/knx/const.py | 5 + homeassistant/components/knx/schema.py | 6 +- .../components/knx/storage/config_store.py | 6 +- .../knx/storage/entity_store_schema.py | 60 ++++++------ .../components/knx/storage/migration.py | 15 ++- homeassistant/components/knx/validation.py | 41 ++++++++- .../fixtures/config_store_binarysensor.json | 2 +- .../config_store_entity_category.json | 69 ++++++++++++++ .../config_store_entity_category_v2_4.json | 69 ++++++++++++++ .../knx/fixtures/config_store_light.json | 2 +- tests/components/knx/test_config_store.py | 91 ++++++++++++++++++- tests/components/knx/test_sensor.py | 3 +- 12 files changed, 331 insertions(+), 38 deletions(-) create mode 100644 tests/components/knx/fixtures/config_store_entity_category.json create mode 100644 tests/components/knx/fixtures/config_store_entity_category_v2_4.json diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 5f89df8c228522..5a690b5af5afdb 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -198,6 +198,11 @@ class FanZeroMode(StrEnum): Platform.WEATHER, } +# read-only platforms raising when added with `EntityCategory.CONFIG` +PLATFORMS_WITHOUT_CONFIG_CATEGORY: Final = frozenset( + {Platform.BINARY_SENSOR, Platform.SENSOR} +) + SUPPORTED_PLATFORMS_UI: Final = { Platform.BINARY_SENSOR, Platform.BUTTON, diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 5a80850e175f38..31e50c7eb9a449 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -48,7 +48,6 @@ Platform, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from homeassistant.util import slugify from .const import ( @@ -78,6 +77,7 @@ from .validation import ( backwards_compatible_xknx_climate_enum_member, dpt_base_type_validator, + entity_category_validator, ga_list_validator, ga_validator, numeric_type_validator, @@ -260,7 +260,9 @@ def _entity_base_schema(platform: Platform) -> probatio.Schema: probatio.Optional(CONF_DEFAULT_ENTITY_ID): probatio.All( cv.entity_id, cv.entity_domain(platform) ), - probatio.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + probatio.Optional(CONF_ENTITY_CATEGORY): entity_category_validator( + platform + ), probatio.Optional(CONF_UNIQUE_ID): probatio.All( cv.string, probatio.Length(min=1) ), diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 2b1da794031326..35d4c422a8e705 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_VERSION: Final = 2 -STORAGE_VERSION_MINOR: Final = 4 +STORAGE_VERSION_MINOR: Final = 5 STORAGE_KEY: Final = f"{DOMAIN}/config_store.json" type KNXPlatformStoreModel = dict[str, dict[str, Any]] # unique_id: configuration @@ -80,6 +80,10 @@ async def _async_migrate_func( # version 2.4 introduced in 2026.5 migration.migrate_2_3_to_2_4(old_data) + if old_major_version <= 2 and old_minor_version < 5: + # version 2.5 introduced in 2026.10 + migration.migrate_2_4_to_2_5(old_data) + return old_data diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index 2616d0a795f811..933faf90375169 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -30,7 +30,6 @@ Platform, ) from homeassistant.helpers import selector -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from homeassistant.helpers.typing import VolDictType from ..const import ( @@ -54,7 +53,11 @@ SelectConf, ) from ..dpt import get_supported_dpts, raw_payload_length -from ..validation import validate_number_attributes, validate_sensor_attributes +from ..validation import ( + entity_category_validator, + validate_number_attributes, + validate_sensor_attributes, +) from .const import ( CONF_ALWAYS_CALLBACK, CONF_COLOR, @@ -140,30 +143,33 @@ SyncStateSelector, ) -BASE_ENTITY_SCHEMA = probatio.All( - { - probatio.Optional(CONF_NAME, default=None): probatio.Maybe(str), - probatio.Optional(CONF_DEVICE_INFO, default=None): probatio.Maybe(str), - probatio.Optional(CONF_ENTITY_CATEGORY, default=None): probatio.Any( - ENTITY_CATEGORIES_SCHEMA, probatio.SetTo(None) - ), - }, - probatio.Any( - probatio.Schema( - { - probatio.Required(CONF_NAME): probatio.All(str, probatio.IsTrue()), - }, - extra=probatio.ALLOW_EXTRA, - ), - probatio.Schema( - { - probatio.Required(CONF_DEVICE_INFO): str, - }, - extra=probatio.ALLOW_EXTRA, + +def base_entity_schema(platform: Platform) -> probatio.All: + """Return the base entity schema for a platform.""" + return probatio.All( + { + probatio.Optional(CONF_NAME, default=None): probatio.Maybe(str), + probatio.Optional(CONF_DEVICE_INFO, default=None): probatio.Maybe(str), + probatio.Optional( + CONF_ENTITY_CATEGORY, default=None + ): entity_category_validator(platform), + }, + probatio.Any( + probatio.Schema( + { + probatio.Required(CONF_NAME): probatio.All(str, probatio.IsTrue()), + }, + extra=probatio.ALLOW_EXTRA, + ), + probatio.Schema( + { + probatio.Required(CONF_DEVICE_INFO): str, + }, + extra=probatio.ALLOW_EXTRA, + ), + msg="One of `Device` or `Name` is required", ), - msg="One of `Device` or `Name` is required", - ), -) + ) BINARY_SENSOR_KNX_SCHEMA = probatio.Schema( @@ -1045,7 +1051,9 @@ def _sensor_attribute_sub_validator(config: dict) -> dict: { probatio.Required(CONF_DATA): probatio.Schema( { - probatio.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, + probatio.Required(CONF_ENTITY): base_entity_schema( + platform + ), probatio.Required(DOMAIN): knx_schema, }, extra=probatio.PREVENT_EXTRA, # restrict in data key for yaml edit diff --git a/homeassistant/components/knx/storage/migration.py b/homeassistant/components/knx/storage/migration.py index e4c33e319d1630..eaf5b1c071f459 100644 --- a/homeassistant/components/knx/storage/migration.py +++ b/homeassistant/components/knx/storage/migration.py @@ -2,9 +2,9 @@ from typing import Any -from homeassistant.const import Platform +from homeassistant.const import CONF_ENTITY_CATEGORY, EntityCategory, Platform -from ..const import CONF_RESPOND_TO_READ +from ..const import CONF_RESPOND_TO_READ, PLATFORMS_WITHOUT_CONFIG_CATEGORY from . import const as store_const @@ -60,3 +60,14 @@ def migrate_2_2_to_2_3(data: dict[str, Any]) -> None: def migrate_2_3_to_2_4(data: dict[str, Any]) -> None: """Migrate from schema 2.3 to schema 2.4.""" data.setdefault("expose", {}) + + +def migrate_2_4_to_2_5(data: dict[str, Any]) -> None: + """Migrate from schema 2.4 to schema 2.5.""" + for platform in PLATFORMS_WITHOUT_CONFIG_CATEGORY: + for entity in data.get("entities", {}).get(platform, {}).values(): + # `EntityCategory.CONFIG` is not valid for these platforms + # so these entities were never set up + entity_data = entity[store_const.CONF_ENTITY] + if entity_data.get(CONF_ENTITY_CATEGORY) == EntityCategory.CONFIG: + entity_data[CONF_ENTITY_CATEGORY] = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index dd76188646b398..a7221c05b782a4 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -20,10 +20,15 @@ DEVICE_CLASS_UNITS, STATE_CLASS_UNITS, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_UNIT_OF_MEASUREMENT, + EntityCategory, + Platform, +) from homeassistant.helpers import config_validation as cv -from .const import NumberConf +from .const import PLATFORMS_WITHOUT_CONFIG_CATEGORY, NumberConf from .dpt import DPTInfo, get_supported_dpts @@ -51,6 +56,38 @@ def dpt_value_validator(value: Any) -> str | int: sensor_type_validator = probatio.Any(numeric_type_validator, string_type_validator) +def entity_category_validator( + platform: Platform, +) -> Callable[[Any], EntityCategory | None]: + """Validate the entity category is supported by the platform. + + Works for both, UI and YAML configuration schema. + """ + valid_categories = set(EntityCategory) + if platform in PLATFORMS_WITHOUT_CONFIG_CATEGORY: + valid_categories -= {EntityCategory.CONFIG} + + def validate(value: Any) -> EntityCategory | None: + """Validate the entity category.""" + if value is None or value == "": # UI sends an empty value to clear it + return None + try: + entity_category = EntityCategory(value) + except ValueError: + raise probatio.Invalid( + f"'{value}' is not a valid entity category" + ) from None + if entity_category not in valid_categories: + _options = ", ".join(sorted(valid_categories)) + raise probatio.Invalid( + f"Entity category '{entity_category}' is not supported by the" + f" {platform} platform. Valid options are: {_options}" + ) + return entity_category + + return validate + + def ga_validator(value: Any) -> str | int: """Validate that value is parsable as GroupAddress or InternalGroupAddress.""" if not isinstance(value, (str, int)): diff --git a/tests/components/knx/fixtures/config_store_binarysensor.json b/tests/components/knx/fixtures/config_store_binarysensor.json index 03d5446ace115d..250283920a1712 100644 --- a/tests/components/knx/fixtures/config_store_binarysensor.json +++ b/tests/components/knx/fixtures/config_store_binarysensor.json @@ -1,6 +1,6 @@ { "version": 2, - "minor_version": 4, + "minor_version": 5, "key": "knx/config_store.json", "data": { "entities": { diff --git a/tests/components/knx/fixtures/config_store_entity_category.json b/tests/components/knx/fixtures/config_store_entity_category.json new file mode 100644 index 00000000000000..1b67d06633008e --- /dev/null +++ b/tests/components/knx/fixtures/config_store_entity_category.json @@ -0,0 +1,69 @@ +{ + "version": 2, + "minor_version": 5, + "key": "knx/config_store.json", + "data": { + "entities": { + "sensor": { + "knx_es_01KC2F5CP5S4QCE3FZ49EF7CSJ": { + "entity": { + "name": "Test sensor", + "device_info": null, + "entity_category": "diagnostic" + }, + "knx": { + "ga_sensor": { + "state": "1/1/1", + "dpt": "7.600", + "passive": [] + }, + "always_callback": false, + "sync_state": true + } + } + }, + "binary_sensor": { + "knx_es_01KC2F5CP5S4QCE3FZ49EF7CSK": { + "entity": { + "name": "Test binary sensor", + "device_info": null, + "entity_category": "diagnostic" + }, + "knx": { + "ga_sensor": { + "state": "2/2/2", + "dpt": "1", + "passive": [] + }, + "invert": false, + "ignore_internal_state": false, + "context_timeout": 0.0, + "reset_after": 0.0, + "sync_state": true + } + } + }, + "switch": { + "knx_es_01KC2F5CP5S4QCE3FZ49EF7CSL": { + "entity": { + "name": "Test switch", + "device_info": null, + "entity_category": "config" + }, + "knx": { + "ga_switch": { + "write": "3/3/3", + "state": null, + "passive": [] + }, + "invert": false, + "respond_to_read": false, + "sync_state": true + } + } + } + }, + "expose": {}, + "time_server": {} + } +} diff --git a/tests/components/knx/fixtures/config_store_entity_category_v2_4.json b/tests/components/knx/fixtures/config_store_entity_category_v2_4.json new file mode 100644 index 00000000000000..060ee4ed278724 --- /dev/null +++ b/tests/components/knx/fixtures/config_store_entity_category_v2_4.json @@ -0,0 +1,69 @@ +{ + "version": 2, + "minor_version": 4, + "key": "knx/config_store.json", + "data": { + "entities": { + "sensor": { + "knx_es_01KC2F5CP5S4QCE3FZ49EF7CSJ": { + "entity": { + "name": "Test sensor", + "device_info": null, + "entity_category": "config" + }, + "knx": { + "ga_sensor": { + "state": "1/1/1", + "dpt": "7.600", + "passive": [] + }, + "always_callback": false, + "sync_state": true + } + } + }, + "binary_sensor": { + "knx_es_01KC2F5CP5S4QCE3FZ49EF7CSK": { + "entity": { + "name": "Test binary sensor", + "device_info": null, + "entity_category": "config" + }, + "knx": { + "ga_sensor": { + "state": "2/2/2", + "dpt": "1", + "passive": [] + }, + "invert": false, + "ignore_internal_state": false, + "context_timeout": 0.0, + "reset_after": 0.0, + "sync_state": true + } + } + }, + "switch": { + "knx_es_01KC2F5CP5S4QCE3FZ49EF7CSL": { + "entity": { + "name": "Test switch", + "device_info": null, + "entity_category": "config" + }, + "knx": { + "ga_switch": { + "write": "3/3/3", + "state": null, + "passive": [] + }, + "invert": false, + "respond_to_read": false, + "sync_state": true + } + } + } + }, + "expose": {}, + "time_server": {} + } +} diff --git a/tests/components/knx/fixtures/config_store_light.json b/tests/components/knx/fixtures/config_store_light.json index 634ac9290afe35..d82c57983653bd 100644 --- a/tests/components/knx/fixtures/config_store_light.json +++ b/tests/components/knx/fixtures/config_store_light.json @@ -1,6 +1,6 @@ { "version": 2, - "minor_version": 4, + "minor_version": 5, "key": "knx/config_store.json", "data": { "entities": { diff --git a/tests/components/knx/test_config_store.py b/tests/components/knx/test_config_store.py index 04f7618c8e3caa..4036571d2d479e 100644 --- a/tests/components/knx/test_config_store.py +++ b/tests/components/knx/test_config_store.py @@ -120,6 +120,73 @@ async def test_create_entity_error( assert res["result"]["error_base"].startswith("value must be one of") +@pytest.mark.parametrize( + ("platform", "knx_data", "read_response"), + [ + pytest.param( + Platform.SENSOR, + {"ga_sensor": {"state": "1/2/3", "dpt": "5.001"}}, + (0,), + id="sensor", + ), + pytest.param( + Platform.BINARY_SENSOR, + {"ga_sensor": {"state": "1/2/3", "dpt": "1"}}, + 0, + id="binary_sensor", + ), + ], +) +async def test_create_entity_unsupported_entity_category( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + entity_registry: er.EntityRegistry, + create_ui_entity: KnxEntityGenerator, + platform: Platform, + knx_data: dict[str, Any], + read_response: int | tuple[int, ...], +) -> None: + """Test read-only platforms reject `EntityCategory.CONFIG`.""" + await knx.setup_integration() + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "knx/create_entity", + "platform": platform, + "data": { + "entity": { + "name": "Test config category", + "entity_category": EntityCategory.CONFIG, + }, + "knx": knx_data, + }, + } + ) + res = await client.receive_json() + assert res["success"], res + assert not res["result"]["success"] + assert res["result"]["errors"][0]["path"] == ["data", "entity", "entity_category"] + assert "is not supported by the" in res["result"]["error_base"] + assert KNX_CONFIG_STORAGE_KEY not in hass_storage + + entity_entry = await create_ui_entity( + platform=platform, + entity_data={ + "name": "Test diagnostic category", + "entity_category": EntityCategory.DIAGNOSTIC, + }, + knx_data=knx_data, + ) + await knx.assert_read("1/2/3", response=read_response) + assert ( + entity_registry.async_get(entity_entry.entity_id).entity_category + is EntityCategory.DIAGNOSTIC + ) + + async def test_update_entity( hass: HomeAssistant, knx: KNXTestKit, @@ -710,12 +777,12 @@ async def test_migration_1_to_2( assert hass_storage[KNX_CONFIG_STORAGE_KEY] == new_data -async def test_migration_2_1_to_2_4( +async def test_migration_2_1_to_2_5( hass: HomeAssistant, knx: KNXTestKit, hass_storage: dict[str, Any], ) -> None: - """Test migration from schema 2.1 to schema 2.4.""" + """Test migration from schema 2.1 to schema 2.5.""" await knx.setup_integration( config_store_fixture="config_store_binarysensor_v2_1.json", state_updater=False, @@ -724,3 +791,23 @@ async def test_migration_2_1_to_2_4( hass, "config_store_binarysensor.json", "knx" ) assert hass_storage[KNX_CONFIG_STORAGE_KEY] == new_data + + +async def test_migration_2_4_to_2_5( + hass: HomeAssistant, + knx: KNXTestKit, + hass_storage: dict[str, Any], +) -> None: + """Test migration from schema 2.4 to schema 2.5.""" + await knx.setup_integration( + config_store_fixture="config_store_entity_category_v2_4.json", + state_updater=False, + ) + new_data = await async_load_json_object_fixture( + hass, "config_store_entity_category.json", "knx" + ) + assert hass_storage[KNX_CONFIG_STORAGE_KEY] == new_data + + # entities that could not be set up before are now created + assert hass.states.get("sensor.test_sensor") + assert hass.states.get("binary_sensor.test_binary_sensor") diff --git a/tests/components/knx/test_sensor.py b/tests/components/knx/test_sensor.py index 7a18bce7a19b33..bf280e56e84747 100644 --- a/tests/components/knx/test_sensor.py +++ b/tests/components/knx/test_sensor.py @@ -247,6 +247,7 @@ async def test_always_callback(hass: HomeAssistant, knx: KNXTestKit) -> None: {"state_class": "total_increasing"}, # invalid for temperature DPT {"unit_of_measurement": "invalid"}, {"device_class": "energy", "unit_of_measurement": "invalid"}, + {"entity_category": "config"}, # sensors can not be added as config entities ], ) async def test_sensor_yaml_attribute_validation( @@ -255,7 +256,7 @@ async def test_sensor_yaml_attribute_validation( knx: KNXTestKit, attribute_config: dict[str, Any], ) -> None: - """Test creating a sensor with invalid unit, state_class or device_class.""" + """Test creating a sensor with invalid attributes.""" with caplog.at_level(logging.ERROR): await knx.setup_integration( { From 0dd1bd07b9b3aca9c77a429338d23efe47d72a60 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2026 08:42:58 +0200 Subject: [PATCH 21/24] Do not log the Xiaomi router password (#182370) --- .../components/xiaomi/device_tracker.py | 7 +-- .../components/xiaomi/test_device_tracker.py | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 0877a702b12a2c..f6b5448d00a5f9 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -168,11 +168,10 @@ def _get_token(host, username, password): return result["token"] except KeyError: error_message = ( - "Xiaomi token cannot be refreshed, response from " - "url: [%s] \nwith parameter: [%s] \nwas: [%s]" + "Xiaomi token cannot be refreshed, response from url: [%s] was: [%s]" ) - _LOGGER.exception(error_message, url, data, result) + _LOGGER.exception(error_message, url, result) return None - _LOGGER.error("Invalid response: [%s] at url: [%s] with data [%s]", res, url, data) + _LOGGER.error("Invalid response: [%s] at url: [%s]", res, url) return None diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index e3cc1898ce903c..4fb0d9dd540fe6 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -4,6 +4,7 @@ import logging from unittest.mock import MagicMock, call, patch +import pytest import requests from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN @@ -16,6 +17,8 @@ INVALID_USERNAME = "bob" TOKEN_TIMEOUT_USERNAME = "tok" +ERROR_STATUS_USERNAME = "err" +PASSWORD = "passwordTest" URL_AUTHORIZE = "http://192.168.0.1/cgi-bin/luci/api/xqsystem/login" URL_LIST_END = "api/misystem/devicelist" @@ -51,6 +54,9 @@ def raise_for_status(self): # pylint: disable-next=global-statement global FIRST_CALL # noqa: PLW0603 + if data and data.get("username", None) == ERROR_STATUS_USERNAME: + # deliver a response the router could not serve + return MockResponse({}, HTTPStatus.INTERNAL_SERVER_ERROR) if data and data.get("username", None) == INVALID_USERNAME: # deliver an invalid token return MockResponse({"code": "401", "msg": "Invalid token"}, 200) @@ -258,3 +264,52 @@ async def test_token_timed_out(mock_get, mock_post, hass: HomeAssistant) -> None assert len(scanner.scan_devices()) == 2 assert scanner.get_device_name("23:83:BF:F6:38:A0") == "Device1" assert scanner.get_device_name("1D:98:EC:5E:D5:A6") == "Device2" + + +@patch("requests.get", side_effect=mocked_requests) +@patch("requests.post", side_effect=mocked_requests) +async def test_rejected_login_does_not_log_password( + mock_get: MagicMock, + mock_post: MagicMock, + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that a router refusing the login keeps the password out of the log.""" + config = { + DEVICE_TRACKER_DOMAIN: xiaomi.PLATFORM_SCHEMA( + { + CONF_PLATFORM: DEVICE_TRACKER_DOMAIN, + CONF_HOST: "192.168.0.1", + CONF_USERNAME: INVALID_USERNAME, + CONF_PASSWORD: PASSWORD, + } + ) + } + + assert get_scanner(hass, config) is None + + assert "Xiaomi token cannot be refreshed" in caplog.text + assert PASSWORD not in caplog.text + + +@patch("requests.get", side_effect=mocked_requests) +@patch("requests.post", side_effect=mocked_requests) +async def test_error_response_does_not_log_password( + mock_get, mock_post, hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test that a non-OK response keeps the password out of the log.""" + config = { + DEVICE_TRACKER_DOMAIN: xiaomi.PLATFORM_SCHEMA( + { + CONF_PLATFORM: DEVICE_TRACKER_DOMAIN, + CONF_HOST: "192.168.0.1", + CONF_USERNAME: ERROR_STATUS_USERNAME, + CONF_PASSWORD: PASSWORD, + } + ) + } + + assert get_scanner(hass, config) is None + + assert "Invalid response" in caplog.text + assert PASSWORD not in caplog.text From b11318cca34bb317a50872dc5a4bf351a364d7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Sva=C5=A1ek?= Date: Wed, 16 Sep 2026 08:43:44 +0200 Subject: [PATCH 22/24] Bump neopool-modbus to 4.8.0 (#182349) --- homeassistant/components/neopool/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/neopool/manifest.json b/homeassistant/components/neopool/manifest.json index 236112f836aaa4..e9c76351fab248 100644 --- a/homeassistant/components/neopool/manifest.json +++ b/homeassistant/components/neopool/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["neopool_modbus"], "quality_scale": "platinum", - "requirements": ["neopool-modbus==4.7.0"] + "requirements": ["neopool-modbus==4.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index a950ee85f2f5b2..fd9804e582bc39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ nad-receiver==0.4.0 ndms2-client==0.1.2 # homeassistant.components.neopool -neopool-modbus==4.7.0 +neopool-modbus==4.8.0 # homeassistant.components.ness_alarm nessclient==1.3.1 From ce63d1de238daad26bd1c2d5ce1f1641579cd4eb Mon Sep 17 00:00:00 2001 From: Marco <46717884+marcodutto@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:29:06 +0200 Subject: [PATCH 23/24] Add second Salda Smarty code owner (#182379) --- CODEOWNERS | 4 ++-- homeassistant/components/smarty/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b1bef3c7924466..955895265893bd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1742,8 +1742,8 @@ CLAUDE.md @home-assistant/core /tests/components/smartthings/ @joostlek /homeassistant/components/smarttub/ @mdz /tests/components/smarttub/ @mdz -/homeassistant/components/smarty/ @z0mbieprocess -/tests/components/smarty/ @z0mbieprocess +/homeassistant/components/smarty/ @z0mbieprocess @marcodutto +/tests/components/smarty/ @z0mbieprocess @marcodutto /homeassistant/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST /homeassistant/components/smlight/ @tl-sl diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index fb102a8f9e9f8e..00788a116a7359 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -1,7 +1,7 @@ { "domain": "smarty", "name": "Salda Smarty", - "codeowners": ["@z0mbieprocess"], + "codeowners": ["@z0mbieprocess", "@marcodutto"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smarty", "integration_type": "hub", From 7fd85f54e4d46464405cae379ab3da0295d0ff49 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 16 Sep 2026 10:43:05 +0200 Subject: [PATCH 24/24] Add async_retry_migration to Config Entries (#182054) Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com> --- homeassistant/config_entries.py | 24 +++++ tests/test_config_entries.py | 164 ++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 94645ea1f16973..80c4792156a93d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2527,6 +2527,30 @@ async def async_setup(self, entry_id: str, _lock: bool = True) -> bool: entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap] ) + async def async_retry_migration(self, entry_id: str) -> None: + """Retry migration for a config entry. + + This is only intended for repair flows created to handle + non-recoverable migration errors. + """ + entry = self.async_get_known_entry(entry_id) + if entry.state is not ConfigEntryState.MIGRATION_ERROR: + raise OperationNotAllowed( + f"The config entry '{entry.title}' ({entry.domain}) with entry_id" + f" '{entry.entry_id}' cannot retry the migration as it is not in the" + f" state {ConfigEntryState.MIGRATION_ERROR} but is in the state {entry.state}" + ) + if entry.disabled_by: + raise OperationNotAllowed( + f"The config entry '{entry.title}' ({entry.domain}) with entry_id" + f" '{entry.entry_id}' cannot retry the migration as it is disabled by" + f" {entry.disabled_by}. Please enable the config entry and retry." + ) + + # Config entry was never loaded so we can set state and start setup to try again + entry._async_set_state(self.hass, ConfigEntryState.NOT_LOADED, None) # noqa: SLF001 + await self.async_setup(entry_id) + async def async_unload(self, entry_id: str, _lock: bool = True) -> bool: """Unload a config entry.""" entry = self.async_get_known_entry(entry_id) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 32adc595682cf3..afe1735c4f1488 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3729,6 +3729,170 @@ async def test_entry_reload_not_loaded( assert entry.state is config_entries.ConfigEntryState.LOADED +async def test_async_retry_migration( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, +) -> None: + """Test that we can recover from a migration error.""" + entry = MockConfigEntry(domain="comp") + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + async_migrate_entry = AsyncMock(side_effect=[False, False, True]) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + async_migrate_entry=async_migrate_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 2 + MINOR_VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Test user step.""" + return self.async_create_entry(title="title", data={}) + + with mock_config_flow("comp", TestFlow): + result = await async_setup_component(hass, "comp", {}) + await hass.async_block_till_done() + + assert result is True + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + + with mock_config_flow("comp", TestFlow): + await manager.async_retry_migration(entry.entry_id) + + assert len(async_unload_entry.mock_calls) == 0 + assert len(async_setup.mock_calls) == 1 + assert len(async_setup_entry.mock_calls) == 0 + assert len(async_migrate_entry.mock_calls) == 2 + assert entry.version == 1 + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + + with mock_config_flow("comp", TestFlow): + await manager.async_retry_migration(entry.entry_id) + + assert len(async_unload_entry.mock_calls) == 0 + assert len(async_setup.mock_calls) == 1 + assert len(async_setup_entry.mock_calls) == 1 + assert len(async_migrate_entry.mock_calls) == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + + +@pytest.mark.parametrize( + "state", + [ + config_entries.ConfigEntryState.NOT_LOADED, + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.FAILED_UNLOAD, + ], +) +async def test_async_retry_migration_fails( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + state: config_entries.ConfigEntryState, +) -> None: + """Test we can't use async_retry_migration. + + With other states than `MIGRATION_ERROR`. + """ + entry = MockConfigEntry(domain="comp", state=state) + entry.add_to_hass(hass) + + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): + await manager.async_retry_migration(entry.entry_id) + + assert entry.state is state + + +async def test_async_retry_migration_on_disabled_entry( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, +) -> None: + """Test we can't use async_retry_migration. + + On a disabled entry that is in `MIGRATION_ERROR` state. + """ + entry = MockConfigEntry(domain="comp") + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + async_migrate_entry = AsyncMock(side_effect=[False, True]) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + async_migrate_entry=async_migrate_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 2 + MINOR_VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Test user step.""" + return self.async_create_entry(title="title", data={}) + + with mock_config_flow("comp", TestFlow): + result = await async_setup_component(hass, "comp", {}) + await hass.async_block_till_done() + + assert result is True + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + + with pytest.raises(config_entries.OperationNotAllowed): + await manager.async_set_disabled_by( + entry.entry_id, disabled_by=config_entries.ConfigEntryDisabler.USER + ) + # Disabled by user is set even if it's not allowed to reload after + assert entry.disabled_by is config_entries.ConfigEntryDisabler.USER + + with pytest.raises( + config_entries.OperationNotAllowed, + match=( + " cannot retry the migration as it is disabled by user." + " Please enable the config entry and retry." + ), + ): + await manager.async_retry_migration(entry.entry_id) + + with pytest.raises(config_entries.OperationNotAllowed): + await manager.async_set_disabled_by(entry.entry_id, disabled_by=None) + assert entry.disabled_by is None + + with mock_config_flow("comp", TestFlow): + await manager.async_retry_migration(entry.entry_id) + + assert entry.state is config_entries.ConfigEntryState.LOADED + + @pytest.mark.parametrize( "state", [