Exemple #1
0
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,
        )
Exemple #3
0
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
Exemple #4
0
 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
Exemple #5
0
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
Exemple #6
0
    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()
Exemple #8
0
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
Exemple #9
0
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")
Exemple #10
0
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")