async def async_register_device(hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice) -> DeviceCoordinator: """Register a device with home assistant and enable pywemo event callbacks.""" # Ensure proper communication with the device and get the initial state. await hass.async_add_executor_job(wemo.get_state, True) device_registry = async_get_device_registry(hass) entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, **_device_info(wemo)) device = DeviceCoordinator(hass, wemo, entry.id) hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device registry = hass.data[DOMAIN]["registry"] registry.on(wemo, None, device.subscription_callback) await hass.async_add_executor_job(registry.register, wemo) if isinstance(wemo, LongPressMixin): try: await hass.async_add_executor_job( wemo.ensure_long_press_virtual_device) # Temporarily handling all exceptions for #52996 & pywemo/pywemo/issues/276 # Replace this with `except: PyWeMoException` after upstream has been fixed. except Exception: # pylint: disable=broad-except _LOGGER.exception( "Failed to enable long press support for device: %s", wemo.name) device.supports_long_press = False return device
async def async_migrate_legacy_zwave( hass: HomeAssistant, zwave_config_entry: ConfigEntry, zwave_js_config_entry: ConfigEntry, migration_map: LegacyZWaveMappedData, ) -> None: """Perform Z-Wave to Z-Wave JS migration.""" dev_reg = async_get_device_registry(hass) for zwave_js_device_id, zwave_device_id in migration_map.device_entries.items( ): zwave_device_entry = dev_reg.async_get(zwave_device_id) if not zwave_device_entry: continue dev_reg.async_update_device( zwave_js_device_id, area_id=zwave_device_entry.area_id, name_by_user=zwave_device_entry.name_by_user, ) ent_reg = async_get_entity_registry(hass) for zwave_js_entity_id, zwave_entry in migration_map.entity_entries.items( ): zwave_entity_id = zwave_entry["entity_id"] if not (entity_entry := ent_reg.async_get(zwave_entity_id)): continue ent_reg.async_remove(zwave_entity_id) ent_reg.async_update_entity( zwave_js_entity_id, new_entity_id=entity_entry.entity_id, name=entity_entry.name, icon=entity_entry.icon, )
async def async_register_device(hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice) -> DeviceWrapper: """Register a device with home assistant and enable pywemo event callbacks.""" device_registry = async_get_device_registry(hass) entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, **_device_info(wemo)) registry = hass.data[DOMAIN]["registry"] await hass.async_add_executor_job(registry.register, wemo) device = DeviceWrapper(hass, wemo, entry.id) hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device registry.on(wemo, None, device.subscription_callback) if device.supports_long_press: try: await hass.async_add_executor_job( wemo.ensure_long_press_virtual_device) except PyWeMoException: _LOGGER.warning( "Failed to enable long press support for device: %s", wemo.name) device.supports_long_press = False return device
def _handle_event(self, event_type: EventType, resource: HueResource) -> None: """Handle status event for this resource (or it's parent).""" if event_type == EventType.RESOURCE_DELETED: # remove any services created for zones/rooms # regular devices are removed automatically by the logic in device.py. if resource.type in [ResourceTypes.ROOM, ResourceTypes.ZONE]: dev_reg = async_get_device_registry(self.hass) if device := dev_reg.async_get_device({(DOMAIN, resource.id)}): dev_reg.async_remove_device(device.id) if resource.type in [ ResourceTypes.GROUPED_LIGHT, ResourceTypes.SCENE ]: ent_reg = async_get_entity_registry(self.hass) ent_reg.async_remove(self.entity_id) return
async def async_update_zha_devices( hass: HomeAssistant, entry_id: str, primary_lock: KeymasterLock, child_locks: List[KeymasterLock], ) -> None: """Update ZHA devices.""" ent_reg = async_get_entity_registry(hass) dev_reg = async_get_device_registry(hass) for lock in [primary_lock, *child_locks]: lock_ent_reg_entry = ent_reg.async_get(lock.lock_entity_id) if not lock_ent_reg_entry: continue lock_dev_reg_entry = dev_reg.async_get(lock_ent_reg_entry.device_id) if not lock_dev_reg_entry: continue lock.zha_lock_entity = lock_ent_reg_entry lock.zha_lock_device = lock_dev_reg_entry
def add_entity_value( self, entity_id: str, entity_values: ZWaveDeviceEntityValues, ) -> None: """Add info for one entity and Z-Wave value.""" ent_reg = async_get_entity_registry(self._hass) dev_reg = async_get_device_registry(self._hass) node = entity_values.primary.node entity_entry = ent_reg.async_get(entity_id) assert entity_entry device_identifier, _ = node_device_id_and_name( node, entity_values.primary.instance) device_entry = dev_reg.async_get_device({device_identifier}, set()) assert device_entry # Normalize unit of measurement. if unit := entity_entry.unit_of_measurement: unit = unit.lower()
def add_entity_value( self, config_entry: ConfigEntry, entity_id: str, discovery_info: ZwaveDiscoveryInfo, ) -> None: """Add info for one entity and Z-Wave JS value.""" ent_reg = async_get_entity_registry(self._hass) dev_reg = async_get_device_registry(self._hass) node = discovery_info.node primary_value = discovery_info.primary_value entity_entry = ent_reg.async_get(entity_id) assert entity_entry device_identifier = get_device_id(node.client, node) device_entry = dev_reg.async_get_device({device_identifier}, set()) assert device_entry # Normalize unit of measurement. if unit := entity_entry.unit_of_measurement: unit = unit.lower()
async def async_update_zwave_js_nodes_and_devices( hass: HomeAssistant, entry_id: str, primary_lock: KeymasterLock, child_locks: List[KeymasterLock], ) -> None: """Update Z-Wave JS nodes and devices.""" client = hass.data[ZWAVE_JS_DOMAIN][entry_id][ZWAVE_JS_DATA_CLIENT] ent_reg = async_get_entity_registry(hass) dev_reg = async_get_device_registry(hass) for lock in [primary_lock, *child_locks]: lock_ent_reg_entry = ent_reg.async_get(lock.lock_entity_id) if not lock_ent_reg_entry: continue lock_dev_reg_entry = dev_reg.async_get(lock_ent_reg_entry.device_id) if not lock_dev_reg_entry: continue node_id: int = 0 for identifier in lock_dev_reg_entry.identifiers: if identifier[0] == ZWAVE_JS_DOMAIN: node_id = int(identifier[1].split("-")[1]) lock.zwave_js_lock_node = client.driver.controller.nodes[node_id] lock.zwave_js_lock_device = lock_dev_reg_entry
async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: """Perform migration of devices and entities to V2 Id's.""" host = entry.data[CONF_HOST] api_key = entry.data[CONF_API_KEY] websession = aiohttp_client.async_get_clientsession(hass) dev_reg = async_get_device_registry(hass) ent_reg = async_get_entity_registry(hass) LOGGER.info( "Start of migration of devices and entities to support API schema 2") # initialize bridge connection just for the migration async with HueBridgeV2(host, api_key, websession) as api: sensor_class_mapping = { DEVICE_CLASS_BATTERY: ResourceTypes.DEVICE_POWER, DEVICE_CLASS_MOTION: ResourceTypes.MOTION, DEVICE_CLASS_ILLUMINANCE: ResourceTypes.LIGHT_LEVEL, DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE, } # handle entities attached to device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) if not zigbee or not zigbee.mac_address: # not a zigbee device or invalid mac continue # get/update existing device by V1 identifier (mac address) # the device will now have both the old and the new identifier identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)} hass_dev = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, identifiers=identifiers) LOGGER.info("Migrated device %s (%s)", hass_dev.name, hass_dev.id) # loop through al entities for device and find match for ent in async_entries_for_device(ent_reg, hass_dev.id, True): # migrate light if ent.entity_id.startswith("light"): # should always return one lightid here new_unique_id = next(iter(hue_dev.lights)) if ent.unique_id == new_unique_id: continue # just in case LOGGER.info( "Migrating %s from unique id %s to %s", ent.entity_id, ent.unique_id, new_unique_id, ) ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id) continue # migrate sensors matched_dev_class = sensor_class_mapping.get( ent.original_device_class or "unknown") if matched_dev_class is None: # this may happen if we're looking at orphaned or unsupported entity LOGGER.warning( "Skip migration of %s because it no longer exists on the bridge", ent.entity_id, ) continue for sensor in api.devices.get_sensors(hue_dev.id): if sensor.type != matched_dev_class: continue new_unique_id = sensor.id if ent.unique_id == new_unique_id: break # just in case LOGGER.info( "Migrating %s from unique id %s to %s", ent.entity_id, ent.unique_id, new_unique_id, ) try: ent_reg.async_update_entity(ent.entity_id, new_unique_id=sensor.id) except ValueError: # assume edge case where the entity was already migrated in a previous run # which got aborted somehow and we do not want # to crash the entire integration init LOGGER.warning( "Skip migration of %s because it already exists", ent.entity_id, ) break # migrate entities that are not connected to a device (groups) for ent in entities_for_config_entry(ent_reg, entry.entry_id): if ent.device_id is not None: continue v1_id = f"/groups/{ent.unique_id}" hue_group = api.groups.room.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # try again with zone hue_group = api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( "Skip migration of %s because it no longer exist on the bridge", ent.entity_id, ) continue new_unique_id = hue_group.grouped_light LOGGER.info( "Migrating %s from unique id %s to %s ", ent.entity_id, ent.unique_id, new_unique_id, ) try: ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id) except ValueError: # assume edge case where the entity was already migrated in a previous run # which got aborted somehow and we do not want # to crash the entire integration init LOGGER.warning( "Skip migration of %s because it already exists", ent.entity_id, ) LOGGER.info( "Migration of devices and entities to support API schema 2 finished")
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Legrand Home+ Control from a config entry.""" hass_entry_data = hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) # Retrieve the registered implementation implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, config_entry)) # Using an aiohttp-based API lib, so rely on async framework # Add the API object to the domain's data in HA api = hass_entry_data[API] = HomePlusControlAsyncApi( hass, config_entry, implementation) # Set of entity unique identifiers of this integration uids = hass_entry_data[ENTITY_UIDS] = set() # Integration dispatchers hass_entry_data[DISPATCHER_REMOVERS] = [] device_registry = async_get_device_registry(hass) # Register the Data Coordinator with the integration async def async_update_data(): """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables so entities can quickly look up their data. """ try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(10): module_data = await api.async_get_modules() except HomePlusControlApiError as err: raise UpdateFailed( f"Error communicating with API: {err} [{type(err)}]") from err # Remove obsolete entities from Home Assistant entity_uids_to_remove = uids - set(module_data) for uid in entity_uids_to_remove: uids.remove(uid) device = device_registry.async_get_device({(DOMAIN, uid)}) device_registry.async_remove_device(device.id) # Send out signal for new entity addition to Home Assistant new_entity_uids = set(module_data) - uids if new_entity_uids: uids.update(new_entity_uids) dispatcher.async_dispatcher_send( hass, SIGNAL_ADD_ENTITIES, new_entity_uids, coordinator, ) return module_data coordinator = DataUpdateCoordinator( hass, _LOGGER, # Name of the data. For logging purposes. name="home_plus_control_module", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta(seconds=60), ) hass_entry_data[DATA_COORDINATOR] = coordinator async def start_platforms(): """Continue setting up the platforms.""" await asyncio.gather(*[ hass.config_entries.async_forward_entry_setup( config_entry, platform) for platform in PLATFORMS ]) # Only refresh the coordinator after all platforms are loaded. await coordinator.async_refresh() hass.async_create_task(start_platforms()) return True
async def device_registry_fixture(hass): """Return the device registry.""" return async_get_device_registry(hass)
async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: """Perform migration of devices and entities to V2 Id's.""" host = entry.data[CONF_HOST] api_key = entry.data[CONF_API_KEY] websession = aiohttp_client.async_get_clientsession(hass) dev_reg = async_get_device_registry(hass) ent_reg = async_get_entity_registry(hass) LOGGER.info( "Start of migration of devices and entities to support API schema 2") # Create mapping of mac address to HA device id's. # Identifier in dev reg should be mac-address, # but in some cases it has a postfix like `-0b` or `-01`. dev_ids = {} for hass_dev in devices_for_config_entries(dev_reg, entry.entry_id): for domain, mac in hass_dev.identifiers: if domain != DOMAIN: continue normalized_mac = mac.split("-")[0] dev_ids[normalized_mac] = hass_dev.id # initialize bridge connection just for the migration async with HueBridgeV2(host, api_key, websession) as api: sensor_class_mapping = { SensorDeviceClass.BATTERY.value: ResourceTypes.DEVICE_POWER, BinarySensorDeviceClass.MOTION.value: ResourceTypes.MOTION, SensorDeviceClass.ILLUMINANCE.value: ResourceTypes.LIGHT_LEVEL, SensorDeviceClass.TEMPERATURE.value: ResourceTypes.TEMPERATURE, } # migrate entities attached to a device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) if not zigbee or not zigbee.mac_address: # not a zigbee device or invalid mac continue # get existing device by V1 identifier (mac address) if hue_dev.product_data.product_archetype == DeviceArchetypes.BRIDGE_V2: hass_dev_id = dev_ids.get(api.config.bridge_id.upper()) else: hass_dev_id = dev_ids.get(zigbee.mac_address) if hass_dev_id is None: # can be safely ignored, this device does not exist in current config LOGGER.debug( "Ignoring device %s (%s) as it does not (yet) exist in the device registry", hue_dev.metadata.name, hue_dev.id, ) continue dev_reg.async_update_device(hass_dev_id, new_identifiers={(DOMAIN, hue_dev.id)}) LOGGER.info("Migrated device %s (%s)", hue_dev.metadata.name, hass_dev_id) # loop through all entities for device and find match for ent in async_entries_for_device(ent_reg, hass_dev_id, True): if ent.entity_id.startswith("light"): # migrate light # should always return one lightid here new_unique_id = next(iter(hue_dev.lights), None) else: # migrate sensors matched_dev_class = sensor_class_mapping.get( ent.original_device_class or "unknown") new_unique_id = next( (sensor.id for sensor in api.devices.get_sensors(hue_dev.id) if sensor.type == matched_dev_class), None, ) if new_unique_id is None: # this may happen if we're looking at orphaned or unsupported entity LOGGER.warning( "Skip migration of %s because it no longer exists on the bridge", ent.entity_id, ) continue try: ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id) except ValueError: # assume edge case where the entity was already migrated in a previous run # which got aborted somehow and we do not want # to crash the entire integration init LOGGER.warning( "Skip migration of %s because it already exists", ent.entity_id, ) else: LOGGER.info( "Migrated entity %s from unique id %s to %s", ent.entity_id, ent.unique_id, new_unique_id, ) # migrate entities that are not connected to a device (groups) for ent in entities_for_config_entry(ent_reg, entry.entry_id): if ent.device_id is not None: continue if "-" in ent.unique_id: # handle case where unique id is v2-id of group/zone hue_group = api.groups.get(ent.unique_id) else: # handle case where the unique id is just the v1 id v1_id = f"/groups/{ent.unique_id}" hue_group = api.groups.room.get_by_v1_id( v1_id) or api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( "Skip migration of %s because it no longer exist on the bridge", ent.entity_id, ) continue new_unique_id = hue_group.grouped_light LOGGER.info( "Migrating %s from unique id %s to %s ", ent.entity_id, ent.unique_id, new_unique_id, ) try: ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id) except ValueError: # assume edge case where the entity was already migrated in a previous run # which got aborted somehow and we do not want # to crash the entire integration init LOGGER.warning( "Skip migration of %s because it already exists", ent.entity_id, ) LOGGER.info( "Migration of devices and entities to support API schema 2 finished")