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
43 changes: 43 additions & 0 deletions custom_components/remote_buttons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ def _handle_removed_remote(

async def _async_options_updated(hass: HomeAssistant, entry: RemoteButtonsConfigEntry) -> None:
"""Apply updated options: sync into entry.data, dismiss repairs, and rescan."""
old_remotes = set(entry.data.get("remote_entities", []))

new_remotes = entry.options.get("remote_entities")
if new_remotes is not None:
hass.config_entries.async_update_entry(
Expand All @@ -261,6 +263,11 @@ async def _async_options_updated(hass: HomeAssistant, entry: RemoteButtonsConfig
for entity_id in watched:
ir.async_delete_issue(hass, DOMAIN, f"new_remote_{entity_id}")

# Clean up entities for remotes that were removed from the watched list.
removed_remotes = old_remotes - watched
for entity_id in removed_remotes:
_cleanup_remote_entities(hass, entry, entity_id)

await async_scan_remote_commands(hass, entry)


Expand Down Expand Up @@ -405,6 +412,42 @@ async def _async_scan_remote_commands_locked(
)


@callback
def _cleanup_remote_entities(
hass: HomeAssistant, entry: RemoteButtonsConfigEntry, entity_id: str
) -> None:
"""Remove all button/number entities and devices for a deselected remote."""
data = entry.runtime_data
known = data.known_commands

to_remove = {(r, s, c) for r, s, c in known if r == entity_id}

entity_reg = er.async_get(hass)
subdevices: set[str] = set()
for _remote, subdevice, cmd_name in to_remove:
subdevices.add(subdevice)
uid = f"remote_buttons_{entity_id}_{subdevice}_{cmd_name}"
ent_id = entity_reg.async_get_entity_id(Platform.BUTTON, DOMAIN, uid)
if ent_id:
entity_reg.async_remove(ent_id)

ir_numbers = data.ir_numbers
ir_subdevices = data.ir_subdevices
for subdevice in subdevices:
_remove_ir_numbers(entity_reg, entity_id, subdevice, ir_numbers, ir_subdevices)

device_reg = dr.async_get(hass)
for subdevice in subdevices:
dev_identifier = (DOMAIN, f"{entity_id}_{subdevice}")
device_entry = device_reg.async_get_device(identifiers={dev_identifier})
if device_entry:
remaining = er.async_entries_for_device(entity_reg, device_entry.id)
if not remaining:
device_reg.async_remove_device(device_entry.id)

data.known_commands = known - to_remove


def _has_ir_codes(commands: dict[str, Any]) -> bool:
"""Return True if any command in the dict has an IR (non-RF) code."""
for code_val in commands.values():
Expand Down
55 changes: 55 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,61 @@ async def test_options_update_dismisses_issue(hass: HomeAssistant) -> None:
assert ir.async_get(hass).async_get_issue(DOMAIN, "new_remote_remote.bedroom") is None


async def test_options_update_cleans_up_deselected_remote(hass: HomeAssistant) -> None:
"""Deselecting a remote via options → its entities and devices are removed."""
setup_remote(hass)
entry = make_entry(hass, ["remote.living_room"])

data = entry.runtime_data
data.known_commands = {
("remote.living_room", "TV", "power"),
("remote.living_room", "TV", "mute"),
}

entity_reg = er.async_get(hass)
device_reg = dr.async_get(hass)

# Create the subdevice device entry.
subdevice_dev = device_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, "remote.living_room_TV")},
name="TV",
)

# Create button entities linked to that device.
for cmd in ("power", "mute"):
entity_reg.async_get_or_create(
Platform.BUTTON,
DOMAIN,
f"remote_buttons_remote.living_room_TV_{cmd}",
config_entry=entry,
device_id=subdevice_dev.id,
)

# Simulate options update that removes remote.living_room.
hass.config_entries.async_update_entry(
entry,
options={"remote_entities": []},
)
with patch(
"custom_components.remote_buttons.storage.Store.async_load",
new_callable=AsyncMock,
return_value={},
):
await _async_options_updated(hass, entry)

# Button entities should be removed.
for cmd in ("power", "mute"):
uid = f"remote_buttons_remote.living_room_TV_{cmd}"
assert entity_reg.async_get_entity_id(Platform.BUTTON, DOMAIN, uid) is None

# Subdevice device should be removed.
assert device_reg.async_get_device(identifiers={(DOMAIN, "remote.living_room_TV")}) is None

# Known commands should be empty.
assert data.known_commands == set()


async def test_removed_remote_cleans_up_buttons(hass: HomeAssistant) -> None:
"""Watched remote removed → button entities and devices are cleaned up."""
setup_remote(hass)
Expand Down