From 2bd45e4625e5221498289d73989895454cbba88e Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 24 Aug 2025 13:04:41 +0200 Subject: [PATCH 1/4] Bump airos to 0.4.3 (#151042) --- homeassistant/components/airos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 5699d082956f8..2a2a241aef027 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.3.0"] + "requirements": ["airos==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f0c1c2bd55a0..7eeb6303415f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.3.0 +airos==0.4.3 # homeassistant.components.airthings_ble airthings-ble==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 358dc51aa7d6b..b4bdb34662b97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.3.0 +airos==0.4.3 # homeassistant.components.airthings_ble airthings-ble==0.9.2 From ef0712a785e8ea02c346df353fe9b61757fd008f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 24 Aug 2025 15:08:51 +0200 Subject: [PATCH 2/4] Handle TypeError in Alexa Devices (#151088) --- homeassistant/components/alexa_devices/config_flow.py | 4 ++-- homeassistant/components/alexa_devices/coordinator.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index 052873f551db2..d75ba39323dab 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -64,7 +64,7 @@ async def async_step_user( data = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" - except CannotAuthenticate: + except (CannotAuthenticate, TypeError): errors["base"] = "invalid_auth" except CannotRetrieveData: errors["base"] = "cannot_retrieve_data" @@ -110,7 +110,7 @@ async def async_step_reauth_confirm( await validate_input(self.hass, {**reauth_entry.data, **user_input}) except CannotConnect: errors["base"] = "cannot_connect" - except CannotAuthenticate: + except (CannotAuthenticate, TypeError): errors["base"] = "invalid_auth" except CannotRetrieveData: errors["base"] = "cannot_retrieve_data" diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index ac033a487ee3f..7807c6f0efdbb 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -66,7 +66,7 @@ async def _async_update_data(self) -> dict[str, AmazonDevice]: translation_key="cannot_retrieve_data_with_error", translation_placeholders={"error": repr(err)}, ) from err - except CannotAuthenticate as err: + except (CannotAuthenticate, TypeError) as err: raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth", From 6c2ba15a7303088727b01652ad03e45fe6a207a3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 24 Aug 2025 15:16:41 +0200 Subject: [PATCH 3/4] Update lxml to 6.0.1 (#151093) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 8b9d7ddf37e50..1fddfe6c8f1e0 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", - "requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.0"] + "requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7eeb6303415f7..f0498c3c73baf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1388,7 +1388,7 @@ lupupy==0.3.2 lw12==0.9.2 # homeassistant.components.scrape -lxml==6.0.0 +lxml==6.0.1 # homeassistant.components.matrix matrix-nio==0.25.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4bdb34662b97..27fcb4d049e9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1186,7 +1186,7 @@ luftdaten==0.7.4 lupupy==0.3.2 # homeassistant.components.scrape -lxml==6.0.0 +lxml==6.0.1 # homeassistant.components.matrix matrix-nio==0.25.2 From bc6f26110554ddc75af3e27a212cd923a3ea36ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 15:55:58 +0200 Subject: [PATCH 4/4] Fix entities/devices stuck in disabled state after config entry re-add (#151075) --- homeassistant/helpers/device_registry.py | 6 ++ homeassistant/helpers/entity_registry.py | 10 ++- tests/helpers/test_device_registry.py | 92 ++++++++++++++++++++++++ tests/helpers/test_entity_registry.py | 91 +++++++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c7f7d4c369d2f..463b5c4dddc9a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1460,12 +1460,18 @@ def async_clear_config_entry(self, config_entry_id: str) -> None: if config_entry_id not in config_entries: continue if config_entries == {config_entry_id}: + # Clear disabled_by if it was disabled by the config entry + if deleted_device.disabled_by is DeviceEntryDisabler.CONFIG_ENTRY: + disabled_by = None + else: + disabled_by = deleted_device.disabled_by # Add a time stamp when the deleted device became orphaned self.deleted_devices[deleted_device.id] = attr.evolve( deleted_device, orphaned_timestamp=now_time, config_entries=set(), config_entries_subentries={}, + disabled_by=disabled_by, ) else: config_entries = config_entries - {config_entry_id} diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index d972b421fc43b..2125c0f451231 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1613,9 +1613,17 @@ def async_clear_config_entry(self, config_entry_id: str) -> None: for key, deleted_entity in list(self.deleted_entities.items()): if config_entry_id != deleted_entity.config_entry_id: continue + # Clear disabled_by if it was disabled by the config entry + if deleted_entity.disabled_by is RegistryEntryDisabler.CONFIG_ENTRY: + disabled_by = None + else: + disabled_by = deleted_entity.disabled_by # Add a time stamp when the deleted entity became orphaned self.deleted_entities[key] = attr.evolve( - deleted_entity, orphaned_timestamp=now_time, config_entry_id=None + deleted_entity, + orphaned_timestamp=now_time, + config_entry_id=None, + disabled_by=disabled_by, ) self.async_schedule_save() diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index d056c25fc3be4..d45c4f6cf91c4 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3368,6 +3368,98 @@ async def test_cleanup_startup(hass: HomeAssistant) -> None: assert len(mock_call.mock_calls) == 1 +async def test_deleted_device_clears_disabled_by_on_config_entry_removal( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that disabled_by is cleared when config entry is removed.""" + config_entry = MockConfigEntry(domain="test", entry_id="mock-id-1") + config_entry.add_to_hass(hass) + + # Create a device disabled by the config entry + device = device_registry.async_get_or_create( + config_entry_id="mock-id-1", + identifiers={("test", "device_1")}, + name="Test Device", + disabled_by=dr.DeviceEntryDisabler.CONFIG_ENTRY, + ) + assert device.config_entries == {"mock-id-1"} + assert device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY + + # Remove the device (it moves to deleted_devices) + device_registry.async_remove_device(device.id) + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 1 + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == {"mock-id-1"} + assert deleted_device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY + assert deleted_device.orphaned_timestamp is None + + # Clear the config entry + device_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is cleared + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == set() + assert deleted_device.disabled_by is None # Should be cleared + assert deleted_device.orphaned_timestamp is not None + + # Now re-add the config entry and device to verify it can be enabled + config_entry2 = MockConfigEntry(domain="test", entry_id="mock-id-2") + config_entry2.add_to_hass(hass) + + # Re-create the device with same identifiers + device2 = device_registry.async_get_or_create( + config_entry_id="mock-id-2", + identifiers={("test", "device_1")}, + name="Test Device", + ) + assert device2.config_entries == {"mock-id-2"} + assert device2.disabled_by is None # Should not be disabled anymore + assert device2.id == device.id # Should keep the same device id + + +async def test_deleted_device_disabled_by_user_not_cleared( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that disabled_by=USER is not cleared when config entry is removed.""" + config_entry = MockConfigEntry(domain="test", entry_id="mock-id-1") + config_entry.add_to_hass(hass) + + # Create a device disabled by the user + device = device_registry.async_get_or_create( + config_entry_id="mock-id-1", + identifiers={("test", "device_1")}, + name="Test Device", + disabled_by=dr.DeviceEntryDisabler.USER, + ) + assert device.config_entries == {"mock-id-1"} + assert device.disabled_by is dr.DeviceEntryDisabler.USER + + # Remove the device (it moves to deleted_devices) + device_registry.async_remove_device(device.id) + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 1 + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == {"mock-id-1"} + assert deleted_device.disabled_by is dr.DeviceEntryDisabler.USER + assert deleted_device.orphaned_timestamp is None + + # Clear the config entry + device_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is NOT cleared for USER disabled devices + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == set() + assert ( + deleted_device.disabled_by is dr.DeviceEntryDisabler.USER + ) # Should remain USER + assert deleted_device.orphaned_timestamp is not None + + @pytest.mark.parametrize("load_registries", [False]) async def test_cleanup_entity_registry_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index e403333d8dfe9..89822b8003929 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -782,6 +782,97 @@ async def test_deleted_entity_removing_config_entry_id( assert entity_registry.deleted_entities[("light", "hue", "1234")] == deleted_entry2 +async def test_deleted_entity_clears_disabled_by_on_config_entry_removal( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test that disabled_by is cleared when config entry is removed.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) + + # Create an entity disabled by the config entry + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=mock_config, + disabled_by=er.RegistryEntryDisabler.CONFIG_ENTRY, + ) + assert entry.config_entry_id == "mock-id-1" + assert entry.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY + + # Remove the entity (it moves to deleted_entities) + entity_registry.async_remove(entry.entity_id) + + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id == "mock-id-1" + assert deleted_entry.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY + assert deleted_entry.orphaned_timestamp is None + + # Clear the config entry + entity_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is cleared + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id is None + assert deleted_entry.disabled_by is None # Should be cleared + assert deleted_entry.orphaned_timestamp is not None + + # Now re-add the config entry and entity to verify it can be enabled + mock_config2 = MockConfigEntry(domain="light", entry_id="mock-id-2") + mock_config2.add_to_hass(hass) + + # Re-create the entity with same unique ID + entry2 = entity_registry.async_get_or_create( + "light", "hue", "5678", config_entry=mock_config2 + ) + assert entry2.config_entry_id == "mock-id-2" + assert entry2.disabled_by is None # Should not be disabled anymore + + +async def test_deleted_entity_disabled_by_user_not_cleared( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test that disabled_by=USER is not cleared when config entry is removed.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) + + # Create an entity disabled by the user + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=mock_config, + disabled_by=er.RegistryEntryDisabler.USER, + ) + assert entry.config_entry_id == "mock-id-1" + assert entry.disabled_by is er.RegistryEntryDisabler.USER + + # Remove the entity (it moves to deleted_entities) + entity_registry.async_remove(entry.entity_id) + + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id == "mock-id-1" + assert deleted_entry.disabled_by is er.RegistryEntryDisabler.USER + assert deleted_entry.orphaned_timestamp is None + + # Clear the config entry + entity_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is NOT cleared for USER disabled entities + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id is None + assert ( + deleted_entry.disabled_by is er.RegistryEntryDisabler.USER + ) # Should remain USER + assert deleted_entry.orphaned_timestamp is not None + + async def test_removing_config_subentry_id( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: