From 3f11af808493bea2f38e4bdd4f4c1e3248495b03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:59:02 +0100 Subject: [PATCH 1/6] Drop single-use service name constants in bsblan (#164311) --- homeassistant/components/bsblan/services.py | 8 +--- tests/components/bsblan/test_services.py | 45 +++++++-------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/bsblan/services.py b/homeassistant/components/bsblan/services.py index d11ff96780cbb2..62336f715c9c63 100644 --- a/homeassistant/components/bsblan/services.py +++ b/homeassistant/components/bsblan/services.py @@ -31,10 +31,6 @@ ATTR_SATURDAY_SLOTS = "saturday_slots" ATTR_SUNDAY_SLOTS = "sunday_slots" -# Service names -SERVICE_SET_HOT_WATER_SCHEDULE = "set_hot_water_schedule" -SERVICE_SYNC_TIME = "sync_time" - # Schema for a single time slot _SLOT_SCHEMA = vol.Schema( @@ -260,14 +256,14 @@ def async_setup_services(hass: HomeAssistant) -> None: """Register the BSB-LAN services.""" hass.services.async_register( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", set_hot_water_schedule, schema=SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA, ) hass.services.async_register( DOMAIN, - SERVICE_SYNC_TIME, + "sync_time", async_sync_time, schema=SYNC_TIME_SCHEMA, ) diff --git a/tests/components/bsblan/test_services.py b/tests/components/bsblan/test_services.py index 43b518912482fd..dcdaae9f768e01 100644 --- a/tests/components/bsblan/test_services.py +++ b/tests/components/bsblan/test_services.py @@ -10,10 +10,6 @@ import voluptuous as vol from homeassistant.components.bsblan.const import DOMAIN -from homeassistant.components.bsblan.services import ( - SERVICE_SET_HOT_WATER_SCHEDULE, - async_setup_services, -) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr @@ -134,7 +130,7 @@ async def test_set_hot_water_schedule( await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", service_call_data, blocking=True, ) @@ -163,7 +159,7 @@ async def test_invalid_device_id( with pytest.raises(ServiceValidationError) as exc_info: await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", { "device_id": "invalid_device_id", "monday_slots": [ @@ -176,11 +172,12 @@ async def test_invalid_device_id( assert exc_info.value.translation_key == "invalid_device_id" +@pytest.mark.usefixtures("setup_integration") @pytest.mark.parametrize( ("service_name", "service_data"), [ ( - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", {"monday_slots": [{"start_time": time(6, 0), "end_time": time(8, 0)}]}, ), ("sync_time", {}), @@ -205,9 +202,6 @@ async def test_no_config_entry_for_device( name="Other Device", ) - # Register the bsblan service without setting up any bsblan config entry - async_setup_services(hass) - with pytest.raises(ServiceValidationError) as exc_info: await hass.services.async_call( DOMAIN, @@ -222,26 +216,15 @@ async def test_no_config_entry_for_device( async def test_config_entry_not_loaded( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - device_registry: dr.DeviceRegistry, + device_entry: dr.DeviceEntry, ) -> None: """Test error when config entry is not loaded.""" - # Add the config entry but don't set it up (so it stays in NOT_LOADED state) - mock_config_entry.add_to_hass(hass) - - # Create the device manually since setup won't run - device_entry = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - identifiers={(DOMAIN, TEST_DEVICE_MAC)}, - name="BSB-LAN Device", - ) - - # Register the service - async_setup_services(hass) + await hass.config_entries.async_unload(mock_config_entry.entry_id) with pytest.raises(ServiceValidationError) as exc_info: await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", { "device_id": device_entry.id, "monday_slots": [ @@ -266,7 +249,7 @@ async def test_api_error( with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", { "device_id": device_entry.id, "monday_slots": [ @@ -302,7 +285,7 @@ async def test_time_validation_errors( with pytest.raises(ServiceValidationError) as exc_info: await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", { "device_id": device_entry.id, "monday_slots": [ @@ -325,7 +308,7 @@ async def test_unprovided_days_are_none( # Only provide Monday and Tuesday, leave other days unprovided await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", { "device_id": device_entry.id, "monday_slots": [ @@ -369,7 +352,7 @@ async def test_string_time_formats( # Test with string time formats await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", { "device_id": device_entry.id, "monday_slots": [ @@ -406,7 +389,7 @@ async def test_non_standard_time_types( with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( DOMAIN, - SERVICE_SET_HOT_WATER_SCHEDULE, + "set_hot_water_schedule", { "device_id": device_entry.id, "monday_slots": [ @@ -424,7 +407,7 @@ async def test_async_setup_services( ) -> None: """Test service registration.""" # Verify service doesn't exist initially - assert not hass.services.has_service(DOMAIN, SERVICE_SET_HOT_WATER_SCHEDULE) + assert not hass.services.has_service(DOMAIN, "set_hot_water_schedule") # Set up the integration mock_config_entry.add_to_hass(hass) @@ -432,7 +415,7 @@ async def test_async_setup_services( await hass.async_block_till_done() # Verify service is now registered - assert hass.services.has_service(DOMAIN, SERVICE_SET_HOT_WATER_SCHEDULE) + assert hass.services.has_service(DOMAIN, "set_hot_water_schedule") async def test_sync_time_service( From 1944a8bd3ad89092535f9095235269b789b54ddb Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:20:46 +0100 Subject: [PATCH 2/6] Remove vacuum area mapping not configured issue (#164259) --- homeassistant/components/vacuum/__init__.py | 43 ------------ homeassistant/components/vacuum/strings.json | 4 -- tests/components/vacuum/test_init.py | 74 -------------------- 3 files changed, 121 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 47e18e9e9ddd74..99081783405747 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -236,12 +236,6 @@ def add_to_platform_start( if self.__vacuum_legacy_battery_icon: self._report_deprecated_battery_properties("battery_icon") - @callback - def async_write_ha_state(self) -> None: - """Write the state to the state machine.""" - super().async_write_ha_state() - self._async_check_segments_issues() - @callback def async_registry_entry_updated(self) -> None: """Run when the entity registry entry has been updated.""" @@ -514,43 +508,6 @@ def _async_check_segments_issues(self) -> None: return options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {}) - should_have_not_configured_issue = ( - VacuumEntityFeature.CLEAN_AREA in self.supported_features - and options.get("area_mapping") is None - ) - - if ( - should_have_not_configured_issue - and not self._segments_not_configured_issue_created - ): - issue_id = ( - f"{ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED}_{self.registry_entry.id}" - ) - ir.async_create_issue( - self.hass, - DOMAIN, - issue_id, - data={ - "entry_id": self.registry_entry.id, - "entity_id": self.entity_id, - }, - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key=ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED, - translation_placeholders={ - "entity_id": self.entity_id, - }, - ) - self._segments_not_configured_issue_created = True - elif ( - not should_have_not_configured_issue - and self._segments_not_configured_issue_created - ): - issue_id = ( - f"{ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED}_{self.registry_entry.id}" - ) - ir.async_delete_issue(self.hass, DOMAIN, issue_id) - self._segments_not_configured_issue_created = False if self._segments_changed_last_seen is not None and ( VacuumEntityFeature.CLEAN_AREA not in self.supported_features diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 778261713b0bb3..1695e1f2a4ca6b 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -93,10 +93,6 @@ "segments_changed": { "description": "", "title": "Vacuum segments have changed for {entity_id}" - }, - "segments_mapping_not_configured": { - "description": "", - "title": "Vacuum segment mapping not configured for {entity_id}" } }, "selector": { diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 7da53a66213689..40378206ddcca1 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -487,80 +487,6 @@ async def test_segments_changed_issue( assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is None -@pytest.mark.usefixtures("config_flow_fixture") -@pytest.mark.parametrize("area_mapping", [{"area_1": ["seg_1"]}, {}]) -async def test_segments_mapping_not_configured_issue( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - area_mapping: dict[str, list[str]], -) -> None: - """Test segments_mapping_not_configured issue.""" - mock_vacuum = MockVacuumWithCleanArea(name="Testing", entity_id="vacuum.testing") - - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - - mock_integration( - hass, - MockModule( - "test", - async_setup_entry=help_async_setup_entry_init, - async_unload_entry=help_async_unload_entry, - ), - ) - setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entity_entry = entity_registry.async_get(mock_vacuum.entity_id) - - issue_id = f"segments_mapping_not_configured_{entity_entry.id}" - issue = ir.async_get(hass).async_get_issue(DOMAIN, issue_id) - assert issue is not None - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.translation_key == "segments_mapping_not_configured" - - entity_registry.async_update_entity_options( - mock_vacuum.entity_id, - DOMAIN, - { - "area_mapping": area_mapping, - "last_seen_segments": [asdict(segment) for segment in mock_vacuum.segments], - }, - ) - await hass.async_block_till_done() - - assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is None - - -@pytest.mark.usefixtures("config_flow_fixture") -async def test_no_segments_mapping_issue_without_clean_area( - hass: HomeAssistant, -) -> None: - """Test no repair issue is created when CLEAN_AREA is not supported.""" - mock_vacuum = MockVacuum(name="Testing", entity_id="vacuum.testing") - - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - - mock_integration( - hass, - MockModule( - "test", - async_setup_entry=help_async_setup_entry_init, - async_unload_entry=help_async_unload_entry, - ), - ) - setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - issues = ir.async_get(hass).issues - assert not any( - issue_id[1].startswith("segments_mapping_not_configured") for issue_id in issues - ) - - @pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)]) async def test_vacuum_log_deprecated_battery_using_properties( hass: HomeAssistant, From 856a9e695ac4e7672918c64889b829a46e48c868 Mon Sep 17 00:00:00 2001 From: Ye Zhiling Date: Fri, 27 Feb 2026 18:40:58 +0800 Subject: [PATCH 3/6] Pass encoding to AtomicWriter in write_utf8_file_atomic (#164015) --- homeassistant/util/file.py | 5 ++++- tests/util/test_file.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py index 6d1c9b6e52217d..6aad7d11ef1343 100644 --- a/homeassistant/util/file.py +++ b/homeassistant/util/file.py @@ -32,8 +32,11 @@ def write_utf8_file_atomic( Using this function frequently will significantly negatively impact performance. """ + encoding = "utf-8" if "b" not in mode else None try: - with AtomicWriter(filename, mode=mode, overwrite=True).open() as fdesc: + with AtomicWriter( # type: ignore[call-arg] # atomicwrites-stubs is outdated, encoding is a valid kwarg + filename, mode=mode, overwrite=True, encoding=encoding + ).open() as fdesc: if not private: os.fchmod(fdesc.fileno(), 0o644) fdesc.write(utf8_data) diff --git a/tests/util/test_file.py b/tests/util/test_file.py index efa3c1ab0d9c10..d8b919777e476e 100644 --- a/tests/util/test_file.py +++ b/tests/util/test_file.py @@ -83,6 +83,19 @@ def test_write_utf8_file_fails_at_rename_and_remove( assert "File replacement cleanup failed" in caplog.text +@pytest.mark.parametrize("func", [write_utf8_file, write_utf8_file_atomic]) +def test_write_utf8_file_with_non_ascii_content(tmp_path: Path, func) -> None: + """Test files with non-ASCII content can be written even when locale is ASCII.""" + test_file = tmp_path / "test.json" + non_ascii_data = '{"name":"自动化","emoji":"🏠"}' + + with patch("locale.getpreferredencoding", return_value="ascii"): + func(test_file, non_ascii_data, False) + + file_text = test_file.read_text(encoding="utf-8") + assert file_text == non_ascii_data + + def test_write_utf8_file_atomic_fails(tmpdir: py.path.local) -> None: """Test OSError from write_utf8_file_atomic is rethrown as WriteError.""" test_dir = tmpdir.mkdir("files") From 3e050ebe59334c1093a386ffe4fbd1ec8aa689d4 Mon Sep 17 00:00:00 2001 From: 7eaves Date: Fri, 27 Feb 2026 20:11:14 +0800 Subject: [PATCH 4/6] Bump PySwitchBot to 1.1.0 (#164298) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 8c26c02bf39c5a..90454ca54adb22 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -42,5 +42,5 @@ "iot_class": "local_push", "loggers": ["switchbot"], "quality_scale": "gold", - "requirements": ["PySwitchbot==1.0.0"] + "requirements": ["PySwitchbot==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 408a2f1b01a32e..2896fcffa8dd26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -83,7 +83,7 @@ PyRMVtransport==0.3.3 PySrDaliGateway==0.19.3 # homeassistant.components.switchbot -PySwitchbot==1.0.0 +PySwitchbot==1.1.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1945f4579acf84..c8c79d3c3b9d46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -83,7 +83,7 @@ PyRMVtransport==0.3.3 PySrDaliGateway==0.19.3 # homeassistant.components.switchbot -PySwitchbot==1.0.0 +PySwitchbot==1.1.0 # homeassistant.components.syncthru PySyncThru==0.8.0 From bb7d5897d18ad6047432f2d45486cbaef0e48d1f Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 27 Feb 2026 13:54:12 +0100 Subject: [PATCH 5/6] Portainer redact CONF_HOST in diagnostics (#164301) --- homeassistant/components/portainer/diagnostics.py | 4 ++-- tests/components/portainer/snapshots/test_diagnostics.ambr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/portainer/diagnostics.py b/homeassistant/components/portainer/diagnostics.py index 8899a93f3d238a..de53dc8033fe27 100644 --- a/homeassistant/components/portainer/diagnostics.py +++ b/homeassistant/components/portainer/diagnostics.py @@ -5,13 +5,13 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.const import CONF_API_TOKEN +from homeassistant.const import CONF_API_TOKEN, CONF_URL from homeassistant.core import HomeAssistant from . import PortainerConfigEntry from .coordinator import PortainerCoordinator -TO_REDACT = [CONF_API_TOKEN] +TO_REDACT = [CONF_API_TOKEN, CONF_URL] def _serialize_coordinator(coordinator: PortainerCoordinator) -> dict[str, Any]: diff --git a/tests/components/portainer/snapshots/test_diagnostics.ambr b/tests/components/portainer/snapshots/test_diagnostics.ambr index c895b7f7bd5600..7059ae761199c1 100644 --- a/tests/components/portainer/snapshots/test_diagnostics.ambr +++ b/tests/components/portainer/snapshots/test_diagnostics.ambr @@ -4,7 +4,7 @@ 'config_entry': dict({ 'data': dict({ 'api_token': '**REDACTED**', - 'url': 'https://127.0.0.1:9000/', + 'url': '**REDACTED**', 'verify_ssl': True, }), 'disabled_by': None, From 553cecb397a108351e966eea41ec0f3fe48987cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:51:34 +0100 Subject: [PATCH 6/6] Ensure future is marked as retrieved in frontend storage (#164320) --- homeassistant/components/frontend/storage.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 2c626102ac66f6..71b6580a0a1e55 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -45,6 +45,10 @@ async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore: except BaseException as ex: del stores[user_id] future.set_exception(ex) + # Ensure the future is marked as retrieved + # since if there is no concurrent call it + # will otherwise never be retrieved. + future.exception() raise future.set_result(store)