async def test_registry_cleanup(owproxy, hass): """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. """ await async_setup_component(hass, "persistent_notification", {}) entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) # Initialise with two components setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"]) with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): await setup_onewire_patched_owserver_integration(hass) await hass.async_block_till_done() assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2 # Second item has disappeared from bus, and was removed manually from the front-end setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"]) entity_registry.async_remove("sensor.28_111111111111_temperature") await hass.async_block_till_done() assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 # Second item has disappeared from bus, and was removed manually from the front-end with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): await hass.config_entries.async_reload("2") await hass.async_block_till_done() assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1
async def test_registry_cleanup(hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock): """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. """ entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) # Initialise with two components setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2 # Second item has disappeared from bus, and was removed manually from the front-end setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"]) entity_registry.async_remove("sensor.28_111111111111_temperature") await hass.async_block_till_done() assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 # Second item has disappeared from bus, and was removed manually from the front-end await hass.config_entries.async_reload("2") await hass.async_block_till_done() assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1
async def test_unique_id_migration( hass, mock_events_list_items, component_setup, config_entry, old_unique_id, ): """Test that old unique id format is migrated to the new format that supports multiple accounts.""" entity_registry = er.async_get(hass) # Create an entity using the old unique id format entity_registry.async_get_or_create( DOMAIN, Platform.CALENDAR, unique_id=old_unique_id, config_entry=config_entry, ) registry_entries = er.async_entries_for_config_entry( entity_registry, config_entry.entry_id) assert {entry.unique_id for entry in registry_entries} == {old_unique_id} mock_events_list_items([]) assert await component_setup() registry_entries = er.async_entries_for_config_entry( entity_registry, config_entry.entry_id) assert {entry.unique_id for entry in registry_entries } == {f"{config_entry.unique_id}-{CALENDAR_ID}"}
async def test_removed_device(hass, client, climate_radio_thermostat_ct100_plus, lock_schlage_be469, integration): """Test that the device registry gets updated when a device gets removed.""" driver = client.driver assert driver # Verify how many nodes are available assert len(driver.controller.nodes) == 2 # Make sure there are the same number of devices dev_reg = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 2 # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) assert len(entity_entries) == 29 # Remove a node and reload the entry old_node = driver.controller.nodes.pop(13) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() # Assert that the node and all of it's entities were removed from the device and # entity registry device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 1 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) assert len(entity_entries) == 17 assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None
async def test_removed_device(hass, client, multiple_devices, integration): """Test that the device registry gets updated when a device gets removed.""" nodes = multiple_devices # Verify how many nodes are available assert len(client.driver.controller.nodes) == 2 # Make sure there are the same number of devices dev_reg = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 2 # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) assert len(entity_entries) == 24 # Remove a node and reload the entry old_node = nodes.pop(13) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() # Assert that the node and all of it's entities were removed from the device and # entity registry device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 1 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) assert len(entity_entries) == 15 assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None
async def test_remove_orphaned_entries_service(hass): """Test service works and also don't remove more than expected.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = deepcopy(LIGHT) data["sensors"] = deepcopy(SWITCH) gateway = await setup_deconz_integration(hass, get_state_response=data) data = {CONF_BRIDGE_ID: BRIDGEID} device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get_or_create( config_entry_id=gateway.config_entry.entry_id, identifiers={("mac", "123")}) assert (len([ entry for entry in device_registry.devices.values() if gateway.config_entry.entry_id in entry.config_entries ]) == 4 # Gateway, light, switch and orphan ) entity_registry = await hass.helpers.entity_registry.async_get_registry() entity_registry.async_get_or_create( SENSOR_DOMAIN, deconz.DOMAIN, "12345", suggested_object_id="Orphaned sensor", config_entry=gateway.config_entry, device_id=device.id, ) assert (len( async_entries_for_config_entry(entity_registry, gateway.config_entry.entry_id)) == 3 # Light, switch battery and orphan ) await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_REMOVE_ORPHANED_ENTRIES, service_data=data, ) await hass.async_block_till_done() assert (len([ entry for entry in device_registry.devices.values() if gateway.config_entry.entry_id in entry.config_entries ]) == 3 # Gateway, light and switch ) assert (len( async_entries_for_config_entry(entity_registry, gateway.config_entry.entry_id)) == 2 # Light and switch battery )
async def async_remove_config_entry_device(hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry) -> bool: """Remove a config entry from a device.""" dev_id = list(device_entry.identifiers)[0][1].split("_")[-1] ent_reg = er.async_get(hass) entities = { ent.unique_id: ent.entity_id for ent in er.async_entries_for_config_entry( ent_reg, config_entry.entry_id) if dev_id in ent.unique_id } for entity_id in entities.values(): ent_reg.async_remove(entity_id) if dev_id not in config_entry.data[CONF_DEVICES]: _LOGGER.info( "Device %s not found in config entry: finalizing device removal", dev_id) return True await hass.data[DOMAIN][TUYA_DEVICES][dev_id].close() new_data = config_entry.data.copy() new_data[CONF_DEVICES].pop(dev_id) new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) hass.config_entries.async_update_entry( config_entry, data=new_data, ) _LOGGER.info("Device %s removed.", dev_id) return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( entry, PLATFORMS) router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) if router.mode != MODE_ROUTER: router_id = None # Remove devices that are no longer tracked device_registry = dr.async_get(hass) devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) for device_entry in devices: if device_entry.via_device_id is None: router_id = device_entry.id continue # do not remove the router itself device_registry.async_update_device( device_entry.id, remove_config_entry_id=entry.entry_id) # Remove entities that are no longer tracked entity_registry = er.async_get(hass) entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) for entity_entry in entries: if entity_entry.device_id is not router_id: entity_registry.async_remove(entity_entry.entity_id) return unload_ok
async def async_migrate_entities_devices( hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry ) -> None: """Move entities and devices to the new config entry.""" migrated_devices = [] device_registry = dr.async_get(hass) for dev_entry in dr.async_entries_for_config_entry( device_registry, legacy_entry_id ): for connection_type, value in dev_entry.connections: if ( connection_type == dr.CONNECTION_NETWORK_MAC and value == new_entry.unique_id ): migrated_devices.append(dev_entry.id) device_registry.async_update_device( dev_entry.id, add_config_entry_id=new_entry.entry_id ) entity_registry = er.async_get(hass) for reg_entity in er.async_entries_for_config_entry( entity_registry, legacy_entry_id ): if reg_entity.device_id in migrated_devices: entity_registry.async_update_entity( reg_entity.entity_id, config_entry_id=new_entry.entry_id )
async def test_migrate_reboot_button_no_device(hass: HomeAssistant, ufp: MockUFPFixture, light: Light): """Test migrating unique ID of reboot button if UniFi Protect device ID changed.""" light2_id, _ = generate_random_ids() registry = er.async_get(hass) registry.async_get_or_create(Platform.BUTTON, DOMAIN, light2_id, config_entry=ufp.entry) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) await init_entry(hass, ufp, [light], regenerate_ids=False) assert ufp.entry.state == ConfigEntryState.LOADED assert ufp.api.update.called assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac buttons = [] for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id): if entity.domain == Platform.BUTTON.value: buttons.append(entity) assert len(buttons) == 3 entity = registry.async_get( f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}") assert entity is not None assert entity.unique_id == light2_id
def async_restore_block_attribute_entities( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, wrapper: BlockDeviceWrapper, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, description_class: Callable[[entity_registry.RegistryEntry], BlockEntityDescription], ) -> None: """Restore block attributes entities.""" entities = [] ent_reg = entity_registry.async_get(hass) entries = entity_registry.async_entries_for_config_entry( ent_reg, config_entry.entry_id) domain = sensor_class.__module__.split(".")[-1] for entry in entries: if entry.domain != domain: continue attribute = entry.unique_id.split("-")[-1] description = description_class(entry) entities.append( sensor_class(wrapper, None, attribute, description, entry, sensors)) if not entities: return async_add_entities(entities)
async def async_remove_orphaned_entries_service(gateway): """Remove orphaned deCONZ entries from device and entity registries.""" device_registry, entity_registry = await asyncio.gather( gateway.hass.helpers.device_registry.async_get_registry(), gateway.hass.helpers.entity_registry.async_get_registry(), ) entity_entries = async_entries_for_config_entry( entity_registry, gateway.config_entry.entry_id) entities_to_be_removed = [] devices_to_be_removed = [ entry.id for entry in device_registry.devices.values() if gateway.config_entry.entry_id in entry.config_entries ] # Don't remove the Gateway host entry gateway_host = device_registry.async_get_device( connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)}, identifiers=set(), ) if gateway_host.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_host.id) # Don't remove the Gateway service entry gateway_service = device_registry.async_get_device(identifiers={ (DOMAIN, gateway.api.config.bridgeid) }, connections=set()) if gateway_service.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_service.id) # Don't remove devices belonging to available events for event in gateway.events: if event.device_id in devices_to_be_removed: devices_to_be_removed.remove(event.device_id) for entry in entity_entries: # Don't remove available entities if entry.unique_id in gateway.entities[entry.domain]: # Don't remove devices with available entities if entry.device_id in devices_to_be_removed: devices_to_be_removed.remove(entry.device_id) continue # Remove entities that are not available entities_to_be_removed.append(entry.entity_id) # Remove unavailable entities for entity_id in entities_to_be_removed: entity_registry.async_remove(entity_id) # Remove devices that don't belong to any entity for device_id in devices_to_be_removed: if (len( async_entries_for_device( entity_registry, device_id, include_disabled_entities=True)) == 0): device_registry.async_remove_device(device_id)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huawei LTE component from config entry.""" url = entry.data[CONF_URL] def get_connection() -> Connection: """Set up a connection.""" if entry.options.get(CONF_UNAUTHENTICATED_MODE): _LOGGER.debug( "Connecting in unauthenticated mode, reduced feature set") connection = Connection(url, timeout=CONNECTION_TIMEOUT) else: _LOGGER.debug("Connecting in authenticated mode, full feature set") username = entry.data.get(CONF_USERNAME) or "" password = entry.data.get(CONF_PASSWORD) or "" connection = Connection(url, username=username, password=password, timeout=CONNECTION_TIMEOUT) return connection try: connection = await hass.async_add_executor_job(get_connection) except Timeout as ex: raise ConfigEntryNotReady from ex # Set up router router = Router(hass, entry, connection, url) # Do initial data update await hass.async_add_executor_job(router.update) # Check that we found required information router_info = router.data.get(KEY_DEVICE_INFORMATION) if not entry.unique_id: # Transitional from < 2021.8: update None config entry and entity unique ids if router_info and (serial_number := router_info.get("SerialNumber")): hass.config_entries.async_update_entry(entry, unique_id=serial_number) ent_reg = entity_registry.async_get(hass) for entity_entry in entity_registry.async_entries_for_config_entry( ent_reg, entry.entry_id): if not entity_entry.unique_id.startswith("None-"): continue new_unique_id = ( f"{serial_number}-{entity_entry.unique_id.split('-', 1)[1]}" ) ent_reg.async_update_entity(entity_entry.entity_id, new_unique_id=new_unique_id) else: await hass.async_add_executor_job(router.cleanup) msg = ( "Could not resolve serial number to use as unique id for router at %s" ", setup failed") if not entry.data.get(CONF_PASSWORD): msg += ( ". Try setting up credentials for the router for one startup, " "unauthenticated mode can be enabled after that in integration " "settings") _LOGGER.error(msg, url) return False
async def async_get_migration_data(hass): """Return dict with ozw side migration info.""" data = {} nodes_values = hass.data[DOMAIN][NODES_VALUES] ozw_config_entries = hass.config_entries.async_entries(DOMAIN) config_entry = ozw_config_entries[0] # ozw only has a single config entry ent_reg = await async_get_entity_registry(hass) entity_entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) unique_entries = {entry.unique_id: entry for entry in entity_entries} dev_reg = await async_get_device_registry(hass) for node_id, node_values in nodes_values.items(): for entity_values in node_values: unique_id = create_value_id(entity_values.primary) if unique_id not in unique_entries: continue node = entity_values.primary.node device_identifier = ( DOMAIN, create_device_id(node, entity_values.primary.instance), ) device_entry = dev_reg.async_get_device({device_identifier}, set()) data[unique_id] = { "node_id": node_id, "node_instance": entity_values.primary.instance, "device_id": device_entry.id, "command_class": entity_values.primary.command_class.value, "command_class_label": entity_values.primary.label, "value_index": entity_values.primary.index, "unique_id": unique_id, "entity_entry": unique_entries[unique_id], } return data
def async_cleanup_registry_entries(service: ServiceCall) -> None: """Remove extra entities that are no longer part of the integration.""" entity_registry = er.async_get(hass) config_ids = [] current_unique_ids: set[str] = set() for config_entry_id in hass.data[DOMAIN]: entries_for_this_config = er.async_entries_for_config_entry( entity_registry, config_entry_id ) config_ids.extend( [ (entity.unique_id, entity.entity_id) for entity in entries_for_this_config ] ) current_unique_ids |= unique_ids_for_config_entry_id(hass, config_entry_id) extra_entities = [ entity_id for unique_id, entity_id in config_ids if unique_id not in current_unique_ids ] for entity_id in extra_entities: if entity_registry.async_is_registered(entity_id): entity_registry.async_remove(entity_id) _LOGGER.debug( "Cleaning up ISY994 Entities and devices: Config Entries: %s, Current Entries: %s, " "Extra Entries Removed: %s", len(config_ids), len(current_unique_ids), len(extra_entities), )
async def async_setup_entry(hass, config_entry, async_add_entities): controller: OmadaController = hass.data[OMADA_DOMAIN][ config_entry.entry_id][DATA_OMADA] controller.entities[DOMAIN] = set() def get_clients_filtered(): clients = set() for mac in controller.api.clients: client = controller.api.clients[mac] # Skip adding client if not connected to ssid in filter list if controller.option_ssid_filter and client.ssid not in controller.option_ssid_filter: continue clients.add(client.mac) return clients def get_devices(): devices = set() for mac in controller.api.devices: device = controller.api.devices[mac] devices.add(device.mac) return devices @callback def items_added(macs: set = get_clients_filtered()): add_client_entities(controller, async_add_entities, macs) config_entry.async_on_unload( async_dispatcher_connect(hass, controller.signal_update, items_added)) entity_registry = await hass.helpers.entity_registry.async_get_registry() initial_set = set() # Add connected entries for mac in get_devices(): initial_set.add(mac) for mac in get_clients_filtered(): initial_set.add(mac) # Add entries that used to exist in HA but are now disconnected. for entry in async_entries_for_config_entry(entity_registry, config_entry.entry_id): mac = entry.unique_id if mac in controller.api.devices: continue elif mac not in controller.api.clients: if mac in controller.api.known_clients: initial_set.add(mac) elif controller.option_ssid_filter and controller.api.clients[ mac].ssid not in controller.option_ssid_filter: entity_registry.async_remove(entry.entity_id) items_added(initial_set)
async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" unload_ok = await hass.config_entries.async_forward_entry_unload( config_entry, "media_player" ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() # Remove zone2 and zone3 entities if needed entity_registry = await er.async_get_registry(hass) entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) zone2_id = f"{config_entry.unique_id}-Zone2" zone3_id = f"{config_entry.unique_id}-Zone3" for entry in entries: if entry.unique_id == zone2_id and not config_entry.options.get(CONF_ZONE2): entity_registry.async_remove(entry.entity_id) _LOGGER.debug("Removing zone2 from DenonAvr") if entry.unique_id == zone3_id and not config_entry.options.get(CONF_ZONE3): entity_registry.async_remove(entry.entity_id) _LOGGER.debug("Removing zone3 from DenonAvr") if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok
async def async_step_init(self, user_input: Dict[str, str] = None) -> Dict[str, str]: """Manage the options for the custom component.""" errors: Dict[str, str] = {} # Grab all devices from the entity registry so we can populate the # dropdown list that will allow a user to configure a device. entity_registry = await async_get_registry(self.hass) devices = async_entries_for_config_entry(entity_registry, self.config_entry.entry_id) stats = { e.unique_id: e.capabilities for e in devices if e.entity_id.startswith('climate.') } if user_input is not None: _LOGGER.debug(f"user_input: {user_input}") _LOGGER.debug(f"original config: {self.config}") # Remove any devices where hvac_modes have been unset. remove_devices = [ unique_id for unique_id in stats.keys() if unique_id == user_input["device"] if len(user_input["hvac_modes"]) == 0 ] for unique_id in remove_devices: if unique_id in self.config: self.config.pop(unique_id) if len(user_input["hvac_modes"]) != 0: if not errors: # Add the new device config. self.config[ user_input["device"]] = user_input["hvac_modes"] _LOGGER.debug(f"updated config: {self.config}") if not errors: # If user selected the 'more' tickbox, show this form again # so they can configure additional devices. if user_input.get('more', False): return await self.async_step_init() # Value of data will be set on the options property of the config_entry instance. return self.async_create_entry( title="", data={CONF_HVAC_MODES: self.config}) options_schema = vol.Schema({ vol.Optional("device", default=list(stats.keys())): vol.In(stats.keys()), vol.Optional("hvac_modes", default=list(default_modes)): cv.multi_select(modes), vol.Optional("more"): cv.boolean }) return self.async_show_form(step_id="init", data_schema=options_schema, errors=errors)
async def test_add_new_binary_sensor_ignored(hass): """Test that adding a new binary sensor is not allowed.""" config_entry = await setup_deconz_integration( hass, options={CONF_ALLOW_NEW_DEVICES: False}, ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 state_added_event = { "t": "event", "e": "added", "r": "sensors", "id": "1", "sensor": deepcopy(SENSORS["1"]), } gateway.api.event_handler(state_added_event) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 entity_registry = await hass.helpers.entity_registry.async_get_registry() assert ( len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 )
def async_migrate_entities_devices( hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry ) -> None: """Move entities and devices to the new config entry.""" migrated_devices = [] device_registry = dr.async_get(hass) for dev_entry in dr.async_entries_for_config_entry( device_registry, legacy_entry_id ): for domain, value in dev_entry.identifiers: if domain == DOMAIN and value == new_entry.unique_id: _LOGGER.debug( "Migrating device with %s to %s", dev_entry.identifiers, new_entry.unique_id, ) migrated_devices.append(dev_entry.id) device_registry.async_update_device( dev_entry.id, add_config_entry_id=new_entry.entry_id, remove_config_entry_id=legacy_entry_id, ) entity_registry = er.async_get(hass) for reg_entity in er.async_entries_for_config_entry( entity_registry, legacy_entry_id ): if reg_entity.device_id in migrated_devices: entity_registry.async_update_entity( reg_entity.entity_id, config_entry_id=new_entry.entry_id )
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() # Remove zone2 and zone3 entities if needed entity_registry = er.async_get(hass) entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) unique_id = config_entry.unique_id or config_entry.entry_id zone2_id = f"{unique_id}-Zone2" zone3_id = f"{unique_id}-Zone3" for entry in entries: if entry.unique_id == zone2_id and not config_entry.options.get( CONF_ZONE2): entity_registry.async_remove(entry.entity_id) _LOGGER.debug("Removing zone2 from DenonAvr") if entry.unique_id == zone3_id and not config_entry.options.get( CONF_ZONE3): entity_registry.async_remove(entry.entity_id) _LOGGER.debug("Removing zone3 from DenonAvr") if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok
async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up a ONVIF binary sensor.""" device = hass.data[DOMAIN][config_entry.unique_id] entities = { event.uid: ONVIFSensor(event.uid, device) for event in device.events.get_platform("sensor") } ent_reg = er.async_get(hass) for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id): if entry.domain == "sensor" and entry.unique_id not in entities: entities[entry.unique_id] = ONVIFSensor(entry.unique_id, device, entry) async_add_entities(entities.values()) @callback def async_check_entities(): """Check if we have added an entity for the event.""" new_entities = [] for event in device.events.get_platform("sensor"): if event.uid not in entities: entities[event.uid] = ONVIFSensor(event.uid, device) new_entities.append(entities[event.uid]) async_add_entities(new_entities) device.events.async_add_listener(async_check_entities) return True
def purge_entity_registry(hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType) -> None: """Remove orphans from entity registry which are not in entry data.""" entity_registry = er.async_get(hass) # Find all entities that are referenced in the config entry. references_config_entry = { entity_entry.entity_id for entity_entry in er.async_entries_for_config_entry( entity_registry, entry_id) } # Find all entities that are referenced by the entry_data. references_entry_data = set() for entity_data in imported_entry_data[CONF_ENTITIES]: entity_unique_id = generate_unique_id(entry_id, entity_data[CONF_ADDRESS], entity_data[CONF_RESOURCE]) entity_id = entity_registry.async_get_entity_id( entity_data[CONF_DOMAIN], DOMAIN, entity_unique_id) if entity_id is not None: references_entry_data.add(entity_id) orphaned_ids = references_config_entry - references_entry_data for orphaned_id in orphaned_ids: entity_registry.async_remove(orphaned_id)
async def async_restore_block_attribute_entities(hass, config_entry, async_add_entities, wrapper, sensors, sensor_class): """Restore block attributes entities.""" entities = [] ent_reg = await entity_registry.async_get_registry(hass) entries = entity_registry.async_entries_for_config_entry( ent_reg, config_entry.entry_id) domain = sensor_class.__module__.split(".")[-1] for entry in entries: if entry.domain != domain: continue attribute = entry.unique_id.split("-")[-1] description = BlockAttributeDescription( name="", icon=entry.original_icon, unit=entry.unit_of_measurement, device_class=entry.device_class, ) entities.append( sensor_class(wrapper, None, attribute, description, entry, sensors)) if not entities: return async_add_entities(entities)
def async_migrate_alarm_unique_ids( hass: HomeAssistant, config_entry: ConfigEntry, household_id: str, alarm_ids: list[str], ) -> None: """Migrate alarm switch unique_ids in the entity registry to the new format.""" entity_registry = er.async_get(hass) registry_entries = er.async_entries_for_config_entry( entity_registry, config_entry.entry_id ) alarm_entries = [ (entry.unique_id, entry) for entry in registry_entries if entry.domain == Platform.SWITCH and entry.original_icon == "mdi:alarm" ] for old_unique_id, alarm_entry in alarm_entries: if ":" in old_unique_id: continue entry_alarm_id = old_unique_id.split("-")[-1] if entry_alarm_id in alarm_ids: new_unique_id = f"alarm-{household_id}:{entry_alarm_id}" _LOGGER.debug( "Migrating unique_id for %s from %s to %s", alarm_entry.entity_id, old_unique_id, new_unique_id, ) entity_registry.async_update_entity( alarm_entry.entity_id, new_unique_id=new_unique_id )
async def async_remove_orphaned_entries_service(hass, data): """Remove orphaned deCONZ entries from device and entity registries.""" gateway = get_master_gateway(hass) if CONF_BRIDGE_ID in data: gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() entity_entries = async_entries_for_config_entry( entity_registry, gateway.config_entry.entry_id) entities_to_be_removed = [] devices_to_be_removed = [ entry.id for entry in device_registry.devices.values() if gateway.config_entry.entry_id in entry.config_entries ] # Don't remove the Gateway host entry gateway_host = device_registry.async_get_device( connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)}, identifiers=set(), ) if gateway_host.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_host.id) # Don't remove the Gateway service entry gateway_service = device_registry.async_get_device(identifiers={ (DOMAIN, gateway.api.config.bridgeid) }, connections=set()) if gateway_service.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_service.id) # Don't remove devices belonging to available events for event in gateway.events: if event.device_id in devices_to_be_removed: devices_to_be_removed.remove(event.device_id) for entry in entity_entries: # Don't remove available entities if entry.unique_id in gateway.entities[entry.domain]: # Don't remove devices with available entities if entry.device_id in devices_to_be_removed: devices_to_be_removed.remove(entry.device_id) continue # Remove entities that are not available entities_to_be_removed.append(entry.entity_id) # Remove unavailable entities for entity_id in entities_to_be_removed: entity_registry.async_remove(entity_id) # Remove devices that don't belong to any entity for device_id in devices_to_be_removed: if len(async_entries_for_device(entity_registry, device_id)) == 0: device_registry.async_remove_device(device_id)
async def async_cleanup_legacy_entry( hass: HomeAssistant, legacy_entry_id: str, ) -> None: """Cleanup the legacy entry if the migration is successful.""" entity_registry = er.async_get(hass) if not er.async_entries_for_config_entry(entity_registry, legacy_entry_id): await hass.config_entries.async_remove(legacy_entry_id)
async def test_migrate_reboot_button( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light ): """Test migrating unique ID of reboot button.""" light1 = mock_light.copy() light1._api = mock_entry.api light1.name = "Test Light 1" light1.id = "lightid1" light2 = mock_light.copy() light2._api = mock_entry.api light2.name = "Test Light 2" light2.id = "lightid2" mock_entry.api.bootstrap.lights = { light1.id: light1, light2.id: light2, } mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) registry = er.async_get(hass) registry.async_get_or_create( Platform.BUTTON, DOMAIN, light1.id, config_entry=mock_entry.entry ) registry.async_get_or_create( Platform.BUTTON, DOMAIN, f"{light2.id}_reboot", config_entry=mock_entry.entry, ) await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() assert mock_entry.entry.state == ConfigEntryState.LOADED assert mock_entry.api.update.called assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac buttons = [] for entity in er.async_entries_for_config_entry( registry, mock_entry.entry.entry_id ): if entity.domain == Platform.BUTTON.value: buttons.append(entity) print(entity.entity_id) assert len(buttons) == 2 assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid1") assert light is not None assert light.unique_id == f"{light1.id}_reboot" assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2_reboot") assert light is not None assert light.unique_id == f"{light2.id}_reboot"
async def async_instances_to_clients_raw( instances: list[dict[str, Any]]) -> None: """Convert instances to Hyperion clients.""" registry = await async_get_registry(hass) running_instances: set[int] = set() stopped_instances: set[int] = set() existing_instances = hass.data[DOMAIN][ config_entry.entry_id][CONF_INSTANCE_CLIENTS] server_id = cast(str, config_entry.unique_id) # In practice, an instance can be in 3 states as seen by this function: # # * Exists, and is running: Should be present in HASS/registry. # * Exists, but is not running: Cannot add it yet, but entity may have be # registered from a previous time it was running. # * No longer exists at all: Should not be present in HASS/registry. # Add instances that are missing. for instance in instances: instance_num = instance.get(hyperion_const.KEY_INSTANCE) if instance_num is None: continue if not instance.get(hyperion_const.KEY_RUNNING, False): stopped_instances.add(instance_num) continue running_instances.add(instance_num) if instance_num in existing_instances: continue hyperion_client = await async_create_connect_hyperion_client( host, port, instance=instance_num, token=token) if not hyperion_client: continue existing_instances[instance_num] = hyperion_client instance_name = instance.get(hyperion_const.KEY_FRIENDLY_NAME, DEFAULT_NAME) async_dispatcher_send( hass, SIGNAL_INSTANCE_ADD.format(config_entry.entry_id), instance_num, instance_name, ) # Remove entities that are are not running instances on Hyperion. for instance_num in set(existing_instances) - running_instances: del existing_instances[instance_num] async_dispatcher_send( hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num) # Deregister entities that belong to removed instances. for entry in async_entries_for_config_entry(registry, config_entry.entry_id): data = split_hyperion_unique_id(entry.unique_id) if not data: continue if data[0] == server_id and (data[1] not in running_instances and data[1] not in stopped_instances): registry.async_remove(entry.entity_id)
async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for UniFi component. Switches are controlling network access and switch ports with POE. """ controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = { BLOCK_SWITCH: set(), POE_SWITCH: set(), DPI_SWITCH: set(), } if controller.site_role != "admin": return # Store previously known POE control entities in case their POE are turned off. known_poe_clients = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() for entry in async_entries_for_config_entry(entity_registry, config_entry.entry_id): if not entry.unique_id.startswith(POE_SWITCH): continue mac = entry.unique_id.replace(f"{POE_SWITCH}-", "") if mac not in controller.api.clients: continue known_poe_clients.append(mac) for mac in controller.option_block_clients: if mac not in controller.api.clients and mac in controller.api.clients_all: client = controller.api.clients_all[mac] controller.api.clients.process_raw([client.raw]) @callback def items_added( clients: set = controller.api.clients, devices: set = controller.api.devices, dpi_groups: set = controller.api.dpi_groups, ) -> None: """Update the values of the controller.""" if controller.option_block_clients: add_block_entities(controller, async_add_entities, clients) if controller.option_poe_clients: add_poe_entities(controller, async_add_entities, clients, known_poe_clients) if controller.option_dpi_restrictions: add_dpi_entities(controller, async_add_entities, dpi_groups) for signal in (controller.signal_update, controller.signal_options_update): config_entry.async_on_unload( async_dispatcher_connect(hass, signal, items_added)) items_added() known_poe_clients.clear()