async def instance_start_and_switch():
    """Wait for an instance to start."""

    instance_ready = asyncio.Event()

    def instance_update(json):
        print("receive json %s", json)
        for data in json["data"]:
            if data["instance"] == 1 and data["running"]:
                instance_ready.set()

    hc = client.HyperionClient(HOST,
                               callbacks={"instance-update": instance_update})

    if not await hc.async_client_connect():
        logging.error("Could not connect to: %s", HOST)
        return

    if not client.ResponseOK(await hc.async_start_instance(instance=1)):
        logging.error("Could not start instance on: %s", HOST)
        return

    # Blocks waiting for the instance to start.
    await instance_ready.wait()

    if not client.ResponseOK(await hc.async_switch_instance(instance=1)):
        logging.error("Could not switch instance on: %s", HOST)
        return
    await hc.async_client_disconnect()
Example #2
0
 async def async_step_create_token_external(
         self, auth_resp: Optional[ConfigType] = None) -> Dict[str, Any]:
     """Handle completion of the request for a new token."""
     if auth_resp is not None and client.ResponseOK(auth_resp):
         token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN)
         if token:
             self._data[CONF_TOKEN] = token
             return self.async_external_step_done(
                 next_step_id="create_token_success")
     return self.async_external_step_done(next_step_id="create_token_fail")
    async def _advance_to_auth_step_if_necessary(
            self, hyperion_client: client.HyperionClient) -> FlowResult:
        """Determine if auth is required."""
        auth_resp = await hyperion_client.async_is_auth_required()

        # Could not determine if auth is required.
        if not auth_resp or not client.ResponseOK(auth_resp):
            return self.async_abort(reason="auth_required_error")
        auth_required = auth_resp.get(const.KEY_INFO,
                                      {}).get(const.KEY_REQUIRED, False)
        if auth_required:
            return await self.async_step_auth()
        return await self.async_step_confirm()
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
Example #5
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, 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