Skip to content

Commit 01e235d

Browse files
authored
fix: cleanup orphaned entities and devices when scenes are removed (#208)
1 parent 52de508 commit 01e235d

3 files changed

Lines changed: 84 additions & 2 deletions

File tree

custom_components/stateful_scenes/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from .discovery import DiscoveryManager
2323
from .StatefulScenes import Hub, Scene
24+
from .helpers import async_cleanup_orphaned_entities
2425

2526
PLATFORMS: list[Platform] = [
2627
Platform.NUMBER,
@@ -44,14 +45,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
4445

4546
scene_confs = await load_scenes_file(entry.data[CONF_SCENE_PATH])
4647

47-
hass.data[DOMAIN][entry.entry_id] = Hub(
48+
hub = Hub(
4849
hass=hass,
4950
scene_confs=scene_confs,
5051
number_tolerance=entry.data[CONF_NUMBER_TOLERANCE],
5152
)
53+
hass.data[DOMAIN][entry.entry_id] = hub
54+
55+
# Clean up orphaned entities for removed scenes
56+
valid_scene_ids = {scene.id for scene in hub.scenes}
57+
await async_cleanup_orphaned_entities(hass, DOMAIN, entry.entry_id, valid_scene_ids)
5258

5359
else:
54-
hass.data[DOMAIN][entry.entry_id] = Scene(hass, entry.data)
60+
scene = Scene(hass, entry.data)
61+
hass.data[DOMAIN][entry.entry_id] = scene
62+
63+
# Clean up orphaned entities for single scene setup
64+
valid_scene_ids = {scene.id}
65+
await async_cleanup_orphaned_entities(hass, DOMAIN, entry.entry_id, valid_scene_ids)
5566

5667
if is_hub and entry.data.get(CONF_ENABLE_DISCOVERY, False):
5768
discovery_manager = DiscoveryManager(hass, entry)

custom_components/stateful_scenes/helpers.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Helper functions for stateful_scenes."""
22

3+
import logging
34
from homeassistant.core import HomeAssistant
45
from homeassistant.helpers import entity_registry, device_registry, area_registry
56
from homeassistant.helpers.template import state_attr
67

8+
_LOGGER = logging.getLogger(__name__)
79

810
def get_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> str:
911
"""Get scene id from entity_id."""
@@ -31,3 +33,67 @@ def get_area_from_entity_id(hass: HomeAssistant, entity_id: str) -> str:
3133
dr = device_registry.async_get(hass)
3234
device = dr.async_get(entity.device_id)
3335
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)

scripts/develop

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ set -e
44

55
cd "$(dirname "$0")/.."
66

7+
# Activate virtual environment if it exists
8+
if [[ -f ".venv/bin/activate" ]]; then
9+
source .venv/bin/activate
10+
fi
11+
712
# Create config dir if not present
813
if [[ ! -d "${PWD}/config" ]]; then
914
mkdir -p "${PWD}/config"

0 commit comments

Comments
 (0)