|
1 | 1 | """Helper functions for stateful_scenes.""" |
2 | 2 |
|
| 3 | +import logging |
3 | 4 | from homeassistant.core import HomeAssistant |
4 | 5 | from homeassistant.helpers import entity_registry, device_registry, area_registry |
5 | 6 | from homeassistant.helpers.template import state_attr |
6 | 7 |
|
| 8 | +_LOGGER = logging.getLogger(__name__) |
7 | 9 |
|
8 | 10 | def get_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> str: |
9 | 11 | """Get scene id from entity_id.""" |
@@ -31,3 +33,67 @@ def get_area_from_entity_id(hass: HomeAssistant, entity_id: str) -> str: |
31 | 33 | dr = device_registry.async_get(hass) |
32 | 34 | device = dr.async_get(entity.device_id) |
33 | 35 | return areas[device.area_id].name if device.area_id is not None else None |
| 36 | + |
| 37 | + |
| 38 | +def _extract_scene_id_from_unique_id(unique_id: str) -> str | None: |
| 39 | + """Extract scene ID from entity unique_id.""" |
| 40 | + if unique_id.startswith("stateful_"): |
| 41 | + return unique_id[9:] # Remove "stateful_" prefix |
| 42 | + |
| 43 | + # Check for suffixes and remove them |
| 44 | + suffixes = [ |
| 45 | + "_restore_on_deactivate", "_ignore_unavailable", "_ignore_attributes", |
| 46 | + "_transition_time", "_debounce_time", "_tolerance", "_off_scene" |
| 47 | + ] |
| 48 | + |
| 49 | + for suffix in suffixes: |
| 50 | + if unique_id.endswith(suffix): |
| 51 | + return unique_id[:-len(suffix)] |
| 52 | + |
| 53 | + return None |
| 54 | + |
| 55 | + |
| 56 | +def _get_device_entities(er: entity_registry.EntityRegistry, device_id: str) -> list: |
| 57 | + """Get all entities for a device.""" |
| 58 | + return [entity for entity in er.entities.values() if entity.device_id == device_id] |
| 59 | + |
| 60 | + |
| 61 | +async def async_cleanup_orphaned_entities(hass: HomeAssistant, domain: str, entry_id: str, valid_scene_ids: set[str]) -> None: |
| 62 | + """Remove orphaned stateful scene entities and devices that no longer have corresponding scenes.""" |
| 63 | + er = entity_registry.async_get(hass) |
| 64 | + dr = device_registry.async_get(hass) |
| 65 | + |
| 66 | + # Find and remove orphaned entities |
| 67 | + entities_to_remove = [] |
| 68 | + orphaned_devices = set() |
| 69 | + |
| 70 | + for entity_id, entity in er.entities.items(): |
| 71 | + if entity.platform == domain and entity.config_entry_id == entry_id and entity.unique_id: |
| 72 | + scene_id = _extract_scene_id_from_unique_id(entity.unique_id) |
| 73 | + |
| 74 | + if scene_id and scene_id not in valid_scene_ids: |
| 75 | + entities_to_remove.append(entity_id) |
| 76 | + if entity.device_id: |
| 77 | + orphaned_devices.add(entity.device_id) |
| 78 | + _LOGGER.info("Marking orphaned entity for removal: %s (scene_id: %s)", entity_id, scene_id) |
| 79 | + |
| 80 | + # Remove orphaned entities |
| 81 | + for entity_id in entities_to_remove: |
| 82 | + _LOGGER.info("Removing orphaned entity: %s", entity_id) |
| 83 | + er.async_remove(entity_id) |
| 84 | + |
| 85 | + # Remove all orphaned devices (both from entities removed above and existing empty devices) |
| 86 | + devices_to_check = orphaned_devices.copy() |
| 87 | + |
| 88 | + # Add all devices belonging to this integration that have no entities |
| 89 | + for device_id, device in dr.devices.items(): |
| 90 | + if entry_id in device.config_entries and not _get_device_entities(er, device_id): |
| 91 | + devices_to_check.add(device_id) |
| 92 | + |
| 93 | + # Remove devices with no entities |
| 94 | + for device_id in devices_to_check: |
| 95 | + if not _get_device_entities(er, device_id): |
| 96 | + device = dr.devices.get(device_id) |
| 97 | + device_name = device.name if device else "Unknown" |
| 98 | + _LOGGER.info("Removing orphaned device: %s (name: %s)", device_id, device_name) |
| 99 | + dr.async_remove_device(device_id) |
0 commit comments