Example #1
0
async def async_setup_entry(hass: HomeAssistant,
                            config_entry: ConfigEntry) -> bool:
    """Set up Hyperion from a config entry."""
    host = config_entry.data[CONF_HOST]
    port = config_entry.data[CONF_PORT]
    token = config_entry.data.get(CONF_TOKEN)

    hyperion_client = await async_create_connect_hyperion_client(
        host, port, token=token, raw_connection=True)

    # Client won't connect? => Not ready.
    if not hyperion_client:
        raise ConfigEntryNotReady
    version = await hyperion_client.async_sysinfo_version()
    if version is not None:
        try:
            if parse_version(version) < parse_version(
                    HYPERION_VERSION_WARN_CUTOFF):
                _LOGGER.warning(
                    "Using a Hyperion server version < %s is not recommended -- "
                    "some features may be unavailable or may not function correctly. "
                    "Please consider upgrading: %s",
                    HYPERION_VERSION_WARN_CUTOFF,
                    HYPERION_RELEASES_URL,
                )
        except ValueError:
            pass

    # Client needs authentication, but no token provided? => Reauth.
    auth_resp = await hyperion_client.async_is_auth_required()
    if (auth_resp is not None and client.ResponseOK(auth_resp)
            and auth_resp.get(hyperion_const.KEY_INFO, {}).get(
                hyperion_const.KEY_REQUIRED, False) and token is None):
        await hyperion_client.async_client_disconnect()
        await _create_reauth_flow(hass, config_entry)
        return False

    # Client login doesn't work? => Reauth.
    if not await hyperion_client.async_client_login():
        await hyperion_client.async_client_disconnect()
        await _create_reauth_flow(hass, config_entry)
        return False

    # Cannot switch instance or cannot load state? => Not ready.
    if (not await hyperion_client.async_client_switch_instance()
            or not client.ServerInfoResponseOK(
                await hyperion_client.async_get_serverinfo())):
        await hyperion_client.async_client_disconnect()
        raise ConfigEntryNotReady

    # We need 1 root client (to manage instances being removed/added) and then 1 client
    # per Hyperion server instance which is shared for all entities associated with
    # that instance.
    hass.data[DOMAIN][config_entry.entry_id] = {
        CONF_ROOT_CLIENT: hyperion_client,
        CONF_INSTANCE_CLIENTS: {},
        CONF_ON_UNLOAD: [],
    }

    async def async_instances_to_clients(response: Dict[str, Any]) -> None:
        """Convert instances to Hyperion clients."""
        if not response or hyperion_const.KEY_DATA not in response:
            return
        await async_instances_to_clients_raw(response[hyperion_const.KEY_DATA])

    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)

    hyperion_client.set_callbacks({
        f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}":
        async_instances_to_clients,
    })

    # Must only listen for option updates after the setup is complete, as otherwise
    # the YAML->ConfigEntry migration code triggers an options update, which causes a
    # reload -- which clashes with the initial load (causing entity_id / unique_id
    # clashes).
    async def setup_then_listen() -> None:
        await asyncio.gather(*[
            hass.config_entries.async_forward_entry_setup(
                config_entry, component) for component in PLATFORMS
        ])
        assert hyperion_client
        await async_instances_to_clients_raw(hyperion_client.instances)
        hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
            config_entry.add_update_listener(_async_entry_updated))

    hass.async_create_task(setup_then_listen())
    return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up Hyperion from a config entry."""
    host = entry.data[CONF_HOST]
    port = entry.data[CONF_PORT]
    token = entry.data.get(CONF_TOKEN)

    hyperion_client = await async_create_connect_hyperion_client(
        host, port, token=token, raw_connection=True
    )

    # Client won't connect? => Not ready.
    if not hyperion_client:
        raise ConfigEntryNotReady
    version = await hyperion_client.async_sysinfo_version()
    if version is not None:
        with suppress(ValueError):
            if AwesomeVersion(version) < AwesomeVersion(HYPERION_VERSION_WARN_CUTOFF):
                _LOGGER.warning(
                    "Using a Hyperion server version < %s is not recommended -- "
                    "some features may be unavailable or may not function correctly. "
                    "Please consider upgrading: %s",
                    HYPERION_VERSION_WARN_CUTOFF,
                    HYPERION_RELEASES_URL,
                )

    # Client needs authentication, but no token provided? => Reauth.
    auth_resp = await hyperion_client.async_is_auth_required()
    if (
        auth_resp is not None
        and client.ResponseOK(auth_resp)
        and auth_resp.get(hyperion_const.KEY_INFO, {}).get(
            hyperion_const.KEY_REQUIRED, False
        )
        and token is None
    ):
        await hyperion_client.async_client_disconnect()
        raise ConfigEntryAuthFailed

    # Client login doesn't work? => Reauth.
    if not await hyperion_client.async_client_login():
        await hyperion_client.async_client_disconnect()
        raise ConfigEntryAuthFailed

    # Cannot switch instance or cannot load state? => Not ready.
    if (
        not await hyperion_client.async_client_switch_instance()
        or not client.ServerInfoResponseOK(await hyperion_client.async_get_serverinfo())
    ):
        await hyperion_client.async_client_disconnect()
        raise ConfigEntryNotReady

    # We need 1 root client (to manage instances being removed/added) and then 1 client
    # per Hyperion server instance which is shared for all entities associated with
    # that instance.
    hass.data[DOMAIN][entry.entry_id] = {
        CONF_ROOT_CLIENT: hyperion_client,
        CONF_INSTANCE_CLIENTS: {},
        CONF_ON_UNLOAD: [],
    }

    async def async_instances_to_clients(response: dict[str, Any]) -> None:
        """Convert instances to Hyperion clients."""
        if not response or hyperion_const.KEY_DATA not in response:
            return
        await async_instances_to_clients_raw(response[hyperion_const.KEY_DATA])

    async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None:
        """Convert instances to Hyperion clients."""
        device_registry = dr.async_get(hass)
        running_instances: set[int] = set()
        stopped_instances: set[int] = set()
        existing_instances = hass.data[DOMAIN][entry.entry_id][CONF_INSTANCE_CLIENTS]
        server_id = cast(str, 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(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(entry.entry_id), instance_num
            )

        # Ensure every device associated with this config entry is still in the list of
        # motionEye cameras, otherwise remove the device (and thus entities).
        known_devices = {
            get_hyperion_device_id(server_id, instance_num)
            for instance_num in running_instances | stopped_instances
        }
        for device_entry in dr.async_entries_for_config_entry(
            device_registry, entry.entry_id
        ):
            for (kind, key) in device_entry.identifiers:
                if kind == DOMAIN and key in known_devices:
                    break
            else:
                device_registry.async_remove_device(device_entry.id)

    hyperion_client.set_callbacks(
        {
            f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": async_instances_to_clients,
        }
    )

    async def setup_then_listen() -> None:
        await asyncio.gather(
            *(
                hass.config_entries.async_forward_entry_setup(entry, platform)
                for platform in PLATFORMS
            )
        )
        assert hyperion_client
        if hyperion_client.instances is not None:
            await async_instances_to_clients_raw(hyperion_client.instances)
        hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append(
            entry.add_update_listener(_async_entry_updated)
        )

    hass.async_create_task(setup_then_listen())
    return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
    """Set up Hyperion from a config entry."""
    host = config_entry.data[CONF_HOST]
    port = config_entry.data[CONF_PORT]
    token = config_entry.data.get(CONF_TOKEN)

    hyperion_client = await async_create_connect_hyperion_client(
        host, port, token=token, raw_connection=True
    )

    # Client won't connect? => Not ready.
    if not hyperion_client:
        raise ConfigEntryNotReady
    version = await hyperion_client.async_sysinfo_version()
    if version is not None:
        try:
            if parse_version(version) < parse_version(HYPERION_VERSION_WARN_CUTOFF):
                _LOGGER.warning(
                    "Using a Hyperion server version < %s is not recommended -- "
                    "some features may be unavailable or may not function correctly. "
                    "Please consider upgrading: %s",
                    HYPERION_VERSION_WARN_CUTOFF,
                    HYPERION_RELEASES_URL,
                )
        except ValueError:
            pass

    # Client needs authentication, but no token provided? => Reauth.
    auth_resp = await hyperion_client.async_is_auth_required()
    if (
        auth_resp is not None
        and client.ResponseOK(auth_resp)
        and auth_resp.get(hyperion_const.KEY_INFO, {}).get(
            hyperion_const.KEY_REQUIRED, False
        )
        and token is None
    ):
        await _create_reauth_flow(hass, config_entry)
        return False

    # Client login doesn't work? => Reauth.
    if not await hyperion_client.async_client_login():
        await _create_reauth_flow(hass, config_entry)
        return False

    # Cannot switch instance or cannot load state? => Not ready.
    if (
        not await hyperion_client.async_client_switch_instance()
        or not client.ServerInfoResponseOK(await hyperion_client.async_get_serverinfo())
    ):
        raise ConfigEntryNotReady

    hyperion_client.set_callbacks(
        {
            f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": lambda json: (
                async_dispatcher_send(
                    hass,
                    SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id),
                    json,
                )
            )
        }
    )

    hass.data[DOMAIN][config_entry.entry_id] = {
        CONF_ROOT_CLIENT: hyperion_client,
        CONF_ON_UNLOAD: [],
    }

    # Must only listen for option updates after the setup is complete, as otherwise
    # the YAML->ConfigEntry migration code triggers an options update, which causes a
    # reload -- which clashes with the initial load (causing entity_id / unique_id
    # clashes).
    async def setup_then_listen() -> None:
        await asyncio.gather(
            *[
                hass.config_entries.async_forward_entry_setup(config_entry, component)
                for component in PLATFORMS
            ]
        )
        hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
            config_entry.add_update_listener(_async_entry_updated)
        )

    hass.async_create_task(setup_then_listen())
    return True