Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/airos/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
4 changes: 2 additions & 2 deletions homeassistant/components/alexa_devices/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/alexa_devices/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/scrape/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
6 changes: 6 additions & 0 deletions homeassistant/helpers/device_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
10 changes: 9 additions & 1 deletion homeassistant/helpers/entity_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
4 changes: 2 additions & 2 deletions requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions tests/helpers/test_device_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions tests/helpers/test_entity_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading