diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 5582ab488df5..d12236177b81 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -67,6 +67,7 @@ from .utils import ( get_http_port, get_rpc_scripts_event_types, get_ws_context, + remove_empty_sub_devices, remove_stale_blu_trv_devices, ) @@ -223,6 +224,7 @@ async def _async_setup_block_entry( await hass.config_entries.async_forward_entry_setups( entry, runtime_data.platforms ) + remove_empty_sub_devices(hass, entry) elif ( sleep_period is None or device_entry is None @@ -334,6 +336,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) hass, entry, ) + remove_empty_sub_devices(hass, entry) elif ( sleep_period is None or device_entry is None diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index c814c987621b..075040cb9299 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -884,3 +884,27 @@ def remove_stale_blu_trv_devices( LOGGER.debug("Removing stale BLU TRV device %s", device.name) dev_reg.async_update_device(device.id, remove_config_entry_id=entry.entry_id) + + +@callback +def remove_empty_sub_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove sub devices without entities.""" + dev_reg = dr.async_get(hass) + entity_reg = er.async_get(hass) + + devices = dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) + + for device in devices: + if not device.via_device_id: + # Device is not a sub-device, skip + continue + + if er.async_entries_for_device(entity_reg, device.id, True): + # Device has entities, skip + continue + + if any(identifier[0] == DOMAIN for identifier in device.identifiers): + LOGGER.debug("Removing empty sub-device %s", device.name) + dev_reg.async_update_device( + device.id, remove_config_entry_id=entry.entry_id + ) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index b1c3d1487b4a..69a7e266dcab 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -26,7 +26,6 @@ from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceEntry, DeviceRegistry, - format_mac, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -152,7 +151,7 @@ def register_device( """Register Shelly device.""" return device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, + connections={(CONNECTION_NETWORK_MAC, MOCK_MAC)}, ) @@ -163,7 +162,7 @@ def register_sub_device( return device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, f"{MOCK_MAC}-{unique_id}")}, - via_device=(DOMAIN, format_mac(MOCK_MAC)), + via_device=(DOMAIN, MOCK_MAC), ) diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 703df09bb615..8457354351f6 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -41,7 +41,7 @@ from homeassistant.helpers.device_registry import DeviceRegistry, format_mac from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component -from . import MOCK_MAC, init_integration, mutate_rpc_device_status +from . import MOCK_MAC, init_integration, mutate_rpc_device_status, register_sub_device async def test_custom_coap_port( @@ -653,3 +653,30 @@ async def test_blu_trv_stale_device_removal( assert hass.states.get(trv_201_entity_id) is None assert device_registry.async_get(trv_201_entry.device_id) is None + + +async def test_empty_device_removal( + hass: HomeAssistant, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + mock_rpc_device: Mock, +) -> None: + """Test removal of empty devices due to device configuration changes.""" + config_entry = await init_integration(hass, 3) + + # create empty sub-device + sub_device_entry = register_sub_device( + device_registry, + config_entry, + "boolean:201-boolean", + ) + + # verify that the sub-device is created + assert device_registry.async_get(sub_device_entry.id) is not None + + # device config change triggers a reload + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + # verify that the empty sub-device is removed + assert device_registry.async_get(sub_device_entry.id) is None