class GenericIoTHubClient(AbstractIoTHubClient):
    """A super class representing a generic asynchronous client.
    This class needs to be extended for specific clients.
    """
    def __init__(self, **kwargs):
        """Initializer for a generic asynchronous client.

        This initializer should not be called directly.
        Instead, use one of the 'create_from_' classmethods to instantiate

        :param iothub_pipeline: The IoTHubPipeline used for the client
        :type iothub_pipeline: :class:`azure.iot.device.iothub.pipeline.IoTHubPipeline`
        :param http_pipeline: The HTTPPipeline used for the client
        :type http_pipeline: :class:`azure.iot.device.iothub.pipeline.HTTPPipeline`
        """
        # Depending on the subclass calling this __init__, there could be different arguments,
        # and the super() call could call a different class, due to the different MROs
        # in the class hierarchies of different clients. Thus, args here must be passed along as
        # **kwargs.
        super().__init__(**kwargs)
        self._inbox_manager = InboxManager(inbox_type=AsyncClientInbox)
        self._iothub_pipeline.on_connected = self._on_connected
        self._iothub_pipeline.on_disconnected = self._on_disconnected
        self._iothub_pipeline.on_method_request_received = self._inbox_manager.route_method_request
        self._iothub_pipeline.on_twin_patch_received = self._inbox_manager.route_twin_patch

    def _on_connected(self):
        """Helper handler that is called upon an iothub pipeline connect"""
        logger.info("Connection State - Connected")

    def _on_disconnected(self):
        """Helper handler that is called upon an iothub pipeline disconnect"""
        logger.info("Connection State - Disconnected")
        self._inbox_manager.clear_all_method_requests()
        logger.info("Cleared all pending method requests due to disconnect")

    async def connect(self):
        """Connects the client to an Azure IoT Hub or Azure IoT Edge Hub instance.

        The destination is chosen based on the credentials passed via the auth_provider parameter
        that was provided when this object was initialized.

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Connecting to Hub...")
        connect_async = async_adapter.emulate_async(
            self._iothub_pipeline.connect)

        callback = async_adapter.AwaitableCallback()
        await connect_async(callback=callback)
        await handle_result(callback)

        logger.info("Successfully connected to Hub")

    async def disconnect(self):
        """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance.

        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Disconnecting from Hub...")
        disconnect_async = async_adapter.emulate_async(
            self._iothub_pipeline.disconnect)

        callback = async_adapter.AwaitableCallback()
        await disconnect_async(callback=callback)
        await handle_result(callback)

        logger.info("Successfully disconnected from Hub")

    async def send_message(self, message):
        """Sends a message to the default events endpoint on the Azure IoT Hub or Azure IoT Edge Hub instance.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param message: The actual message to send. Anything passed that is not an instance of the
            Message class will be converted to Message object.
        :type message: :class:`azure.iot.device.Message` or str

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        :raises: ValueError if the message fails size validation.
        """
        if not isinstance(message, Message):
            message = Message(message)

        if message.get_size() > device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT:
            raise ValueError(
                "Size of telemetry message can not exceed 256 KB.")

        logger.info("Sending message to Hub...")
        send_message_async = async_adapter.emulate_async(
            self._iothub_pipeline.send_message)

        callback = async_adapter.AwaitableCallback()
        await send_message_async(message, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent message to Hub")

    async def receive_method_request(self, method_name=None):
        """Receive a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :param str method_name: Optionally provide the name of the method to receive requests for.
            If this parameter is not given, all methods not already being specifically targeted by
            a different call to receive_method will be received.

        :returns: MethodRequest object representing the received method request.
        :rtype: `azure.iot.device.MethodRequest`
        """
        if not self._iothub_pipeline.feature_enabled[constant.METHODS]:
            await self._enable_feature(constant.METHODS)

        method_inbox = self._inbox_manager.get_method_request_inbox(
            method_name)

        logger.info("Waiting for method request...")
        method_request = await method_inbox.get()
        logger.info("Received method request")
        return method_request

    async def send_method_response(self, method_response):
        """Send a response to a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param method_response: The MethodResponse to send
        :type method_response: :class:`azure.iot.device.MethodResponse`

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Sending method response to Hub...")
        send_method_response_async = async_adapter.emulate_async(
            self._iothub_pipeline.send_method_response)

        callback = async_adapter.AwaitableCallback()

        # TODO: maybe consolidate method_request, result and status into a new object
        await send_method_response_async(method_response, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent method response to Hub")

    async def _enable_feature(self, feature_name):
        """Enable an Azure IoT Hub feature

        :param feature_name: The name of the feature to enable.
            See azure.iot.device.common.pipeline.constant for possible values.
        """
        logger.info("Enabling feature:" + feature_name + "...")
        enable_feature_async = async_adapter.emulate_async(
            self._iothub_pipeline.enable_feature)

        callback = async_adapter.AwaitableCallback()
        await enable_feature_async(feature_name, callback=callback)
        await handle_result(callback)

        logger.info("Successfully enabled feature:" + feature_name)

    async def get_twin(self):
        """
        Gets the device or module twin from the Azure IoT Hub or Azure IoT Edge Hub service.

        :returns: Complete Twin as a JSON dict
        :rtype: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Getting twin")

        if not self._iothub_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        get_twin_async = async_adapter.emulate_async(
            self._iothub_pipeline.get_twin)

        callback = async_adapter.AwaitableCallback(return_arg_name="twin")
        await get_twin_async(callback=callback)
        twin = await handle_result(callback)
        logger.info("Successfully retrieved twin")
        return twin

    async def patch_twin_reported_properties(self, reported_properties_patch):
        """
        Update reported properties with the Azure IoT Hub or Azure IoT Edge Hub service.

        If the service returns an error on the patch operation, this function will raise the
        appropriate error.

        :param reported_properties_patch: Twin Reported Properties patch as a JSON dict
        :type reported_properties_patch: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Patching twin reported properties")

        if not self._iothub_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        patch_twin_async = async_adapter.emulate_async(
            self._iothub_pipeline.patch_twin_reported_properties)

        callback = async_adapter.AwaitableCallback()
        await patch_twin_async(patch=reported_properties_patch,
                               callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent twin patch")

    async def receive_twin_desired_properties_patch(self):
        """
        Receive a desired property patch via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :returns: Twin Desired Properties patch as a JSON dict
        :rtype: dict
        """
        if not self._iothub_pipeline.feature_enabled[constant.TWIN_PATCHES]:
            await self._enable_feature(constant.TWIN_PATCHES)
        twin_patch_inbox = self._inbox_manager.get_twin_patch_inbox()

        logger.info("Waiting for twin patches...")
        patch = await twin_patch_inbox.get()
        logger.info("twin patch received")
        return patch

    async def get_storage_info_for_blob(self, blob_name):
        """Sends a POST request over HTTP to an IoTHub endpoint that will return information for uploading via the Azure Storage Account linked to the IoTHub your device is connected to.

        :param str blob_name: The name in string format of the blob that will be uploaded using the storage API. This name will be used to generate the proper credentials for Storage, and needs to match what will be used with the Azure Storage SDK to perform the blob upload.

        :returns: A JSON-like (dictionary) object from IoT Hub that will contain relevant information including: correlationId, hostName, containerName, blobName, sasToken.
        """
        get_storage_info_for_blob_async = async_adapter.emulate_async(
            self._http_pipeline.get_storage_info_for_blob)

        callback = async_adapter.AwaitableCallback(
            return_arg_name="storage_info")
        await get_storage_info_for_blob_async(blob_name=blob_name,
                                              callback=callback)
        storage_info = await handle_result(callback)
        logger.info("Successfully retrieved storage_info")
        return storage_info

    async def notify_blob_upload_status(self, correlation_id, is_success,
                                        status_code, status_description):
        """When the upload is complete, the device sends a POST request to the IoT Hub endpoint with information on the status of an upload to blob attempt. This is used by IoT Hub to notify listening clients.

        :param str correlation_id: Provided by IoT Hub on get_storage_info_for_blob request.
        :param bool is_success: A boolean that indicates whether the file was uploaded successfully.
        :param int status_code: A numeric status code that is the status for the upload of the fiel to storage.
        :param str status_description: A description that corresponds to the status_code.
        """
        notify_blob_upload_status_async = async_adapter.emulate_async(
            self._http_pipeline.notify_blob_upload_status)

        callback = async_adapter.AwaitableCallback()
        await notify_blob_upload_status_async(
            correlation_id=correlation_id,
            is_success=is_success,
            status_code=status_code,
            status_description=status_description,
            callback=callback,
        )
        await handle_result(callback)
        logger.info("Successfully notified blob upload status")
class GenericIoTHubClient(AbstractIoTHubClient):
    """A super class representing a generic asynchronous client.
    This class needs to be extended for specific clients.
    """
    def __init__(self, pipeline):
        """Initializer for a generic asynchronous client.

        This initializer should not be called directly.
        Instead, use one of the 'create_from_' classmethods to instantiate

        :param pipeline: The pipeline that the client will use.
        """
        super().__init__(pipeline)
        self._inbox_manager = InboxManager(inbox_type=AsyncClientInbox)
        self._pipeline.on_connected = self._on_connected
        self._pipeline.on_disconnected = self._on_disconnected
        self._pipeline.on_method_request_received = self._inbox_manager.route_method_request
        self._pipeline.on_twin_patch_received = self._inbox_manager.route_twin_patch

    def _on_connected(self):
        """Helper handler that is called upon a pipeline connect"""
        logger.info("Connection State - Connected")

    def _on_disconnected(self):
        """Helper handler that is called upon a pipeline disconnect"""
        logger.info("Connection State - Disconnected")
        self._inbox_manager.clear_all_method_requests()
        logger.info("Cleared all pending method requests due to disconnect")

    async def connect(self):
        """Connects the client to an Azure IoT Hub or Azure IoT Edge Hub instance.

        The destination is chosen based on the credentials passed via the auth_provider parameter
        that was provided when this object was initialized.
        """
        logger.info("Connecting to Hub...")
        connect_async = async_adapter.emulate_async(self._pipeline.connect)

        def sync_callback():
            logger.info("Successfully connected to Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        await connect_async(callback=callback)
        await callback.completion()

    async def disconnect(self):
        """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance.
        """
        logger.info("Disconnecting from Hub...")
        disconnect_async = async_adapter.emulate_async(
            self._pipeline.disconnect)

        def sync_callback():
            logger.info("Successfully disconnected from Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        await disconnect_async(callback=callback)
        await callback.completion()

    async def send_d2c_message(self, message):
        """Sends a message to the default events endpoint on the Azure IoT Hub or Azure IoT Edge Hub instance.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param message: The actual message to send. Anything passed that is not an instance of the
        Message class will be converted to Message object.
        """
        if not isinstance(message, Message):
            message = Message(message)

        logger.info("Sending message to Hub...")
        send_d2c_message_async = async_adapter.emulate_async(
            self._pipeline.send_d2c_message)

        def sync_callback():
            logger.info("Successfully sent message to Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        await send_d2c_message_async(message, callback=callback)
        await callback.completion()

    async def receive_method_request(self, method_name=None):
        """Receive a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :param str method_name: Optionally provide the name of the method to receive requests for.
        If this parameter is not given, all methods not already being specifically targeted by
        a different call to receive_method will be received.

        :returns: MethodRequest object representing the received method request.
        """
        if not self._pipeline.feature_enabled[constant.METHODS]:
            await self._enable_feature(constant.METHODS)

        method_inbox = self._inbox_manager.get_method_request_inbox(
            method_name)

        logger.info("Waiting for method request...")
        method_request = await method_inbox.get()
        logger.info("Received method request")
        return method_request

    async def send_method_response(self, method_response):
        """Send a response to a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param method_response: The MethodResponse to send
        """
        logger.info("Sending method response to Hub...")
        send_method_response_async = async_adapter.emulate_async(
            self._pipeline.send_method_response)

        def sync_callback():
            logger.info("Successfully sent method response to Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        # TODO: maybe consolidate method_request, result and status into a new object
        await send_method_response_async(method_response, callback=callback)
        await callback.completion()

    async def _enable_feature(self, feature_name):
        """Enable an Azure IoT Hub feature

        :param feature_name: The name of the feature to enable.
        See azure.iot.device.common.pipeline.constant for possible values.
        """
        logger.info("Enabling feature:" + feature_name + "...")
        enable_feature_async = async_adapter.emulate_async(
            self._pipeline.enable_feature)

        def sync_callback():
            logger.info("Successfully enabled feature:" + feature_name)

        callback = async_adapter.AwaitableCallback(sync_callback)

        await enable_feature_async(feature_name, callback=callback)
        await callback.completion()

    async def get_twin(self):
        """
        Gets the device or module twin from the Azure IoT Hub or Azure IoT Edge Hub service.

        :returns: Twin object which was retrieved from the hub
        """
        logger.info("Getting twin")

        if not self._pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        get_twin_async = async_adapter.emulate_async(self._pipeline.get_twin)

        twin = None

        def sync_callback(received_twin):
            nonlocal twin
            logger.info("Successfully retrieved twin")
            twin = received_twin

        callback = async_adapter.AwaitableCallback(sync_callback)

        await get_twin_async(callback=callback)
        await callback.completion()

        return twin

    async def patch_twin_reported_properties(self, reported_properties_patch):
        """
        Update reported properties with the Azure IoT Hub or Azure IoT Edge Hub service.

        If the service returns an error on the patch operation, this function will raise the
        appropriate error.

        :param reported_properties_patch:
        :type reported_properties_patch: dict, str, int, float, bool, or None (JSON compatible values)
        """
        logger.info("Patching twin reported properties")

        if not self._pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        patch_twin_async = async_adapter.emulate_async(
            self._pipeline.patch_twin_reported_properties)

        def sync_callback():
            logger.info("Successfully sent twin patch")

        callback = async_adapter.AwaitableCallback(sync_callback)

        await patch_twin_async(patch=reported_properties_patch,
                               callback=callback)
        await callback.completion()

    async def receive_twin_desired_properties_patch(self):
        """
        Receive a desired property patch via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :returns: desired property patch.  This can be dict, str, int, float, bool, or None (JSON compatible values)
        """
        if not self._pipeline.feature_enabled[constant.TWIN_PATCHES]:
            await self._enable_feature(constant.TWIN_PATCHES)
        twin_patch_inbox = self._inbox_manager.get_twin_patch_inbox()

        logger.info("Waiting for twin patches...")
        patch = await twin_patch_inbox.get()
        logger.info("twin patch received")
        return patch
Beispiel #3
0
class GenericIoTHubClient(AbstractIoTHubClient):
    """A super class representing a generic asynchronous client.
    This class needs to be extended for specific clients.
    """

    def __init__(self, **kwargs):
        """Initializer for a generic asynchronous client.

        This initializer should not be called directly.
        Instead, use one of the 'create_from_' classmethods to instantiate

        :param mqtt_pipeline: The MQTTPipeline used for the client
        :type mqtt_pipeline: :class:`azure.iot.device.iothub.pipeline.MQTTPipeline`
        :param http_pipeline: The HTTPPipeline used for the client
        :type http_pipeline: :class:`azure.iot.device.iothub.pipeline.HTTPPipeline`
        """
        # Depending on the subclass calling this __init__, there could be different arguments,
        # and the super() call could call a different class, due to the different MROs
        # in the class hierarchies of different clients. Thus, args here must be passed along as
        # **kwargs.
        super().__init__(**kwargs)
        self._inbox_manager = InboxManager(inbox_type=AsyncClientInbox)
        self._handler_manager = async_handler_manager.AsyncHandlerManager(self._inbox_manager)

        # Set pipeline handlers for client events
        self._mqtt_pipeline.on_connected = self._on_connected
        self._mqtt_pipeline.on_disconnected = self._on_disconnected
        self._mqtt_pipeline.on_new_sastoken_required = self._on_new_sastoken_required

        # Set pipeline handlers for data receives
        self._mqtt_pipeline.on_method_request_received = self._inbox_manager.route_method_request
        self._mqtt_pipeline.on_twin_patch_received = self._inbox_manager.route_twin_patch

    async def _enable_feature(self, feature_name):
        """Enable an Azure IoT Hub feature

        :param feature_name: The name of the feature to enable.
            See azure.iot.device.common.pipeline.constant for possible values.
        """
        logger.info("Enabling feature:" + feature_name + "...")
        if not self._mqtt_pipeline.feature_enabled[feature_name]:
            # Enable the feature if not already enabled
            enable_feature_async = async_adapter.emulate_async(self._mqtt_pipeline.enable_feature)

            callback = async_adapter.AwaitableCallback()
            await enable_feature_async(feature_name, callback=callback)
            await handle_result(callback)

            logger.info("Successfully enabled feature:" + feature_name)
        else:
            # This branch shouldn't be reached, but in case it is, log it
            logger.info("Feature ({}) already enabled - skipping".format(feature_name))

    async def _disable_feature(self, feature_name):
        """Disable an Azure IoT Hub feature

        :param feature_name: The name of the feature to enable.
            See azure.iot.device.common.pipeline.constant for possible values.
        """
        logger.info("Disabling feature: {}...".format(feature_name))
        if self._mqtt_pipeline.feature_enabled[feature_name]:
            # Disable the feature if not already disabled
            disable_feature_async = async_adapter.emulate_async(self._mqtt_pipeline.disable_feature)

            callback = async_adapter.AwaitableCallback()
            await disable_feature_async(feature_name, callback=callback)
            await handle_result(callback)

            logger.info("Successfully disabled feature: {}".format(feature_name))
        else:
            # This branch shouldn't be reached, but in case it is, log it
            logger.info("Feature ({}) already disabled - skipping".format(feature_name))

    def _generic_receive_handler_setter(self, handler_name, feature_name, new_handler):
        """Set a receive handler on the handler manager and enable the corresponding feature.

        This is a synchronous call (yes, even though this is the async client), meaning that this
        function will not return until the feature has been enabled (if necessary).

        :param str handler_name: The name of the handler on the handler manager to set
        :param str feature_name: The name of the pipeline feature that corresponds to the handler
        :param new_handler: The function to be set as the handler
        """
        self._check_receive_mode_is_handler()
        # Set the handler on the handler manager
        setattr(self._handler_manager, handler_name, new_handler)

        # Enable the feature if necessary
        if new_handler is not None and not self._mqtt_pipeline.feature_enabled[feature_name]:
            # We have to call this on a loop running on a different thread in order to ensure
            # the setter can be called both within a coroutine (with a running event loop) and
            # outside of a coroutine (where no event loop is currently running)
            loop = loop_management.get_client_internal_loop()
            fut = asyncio.run_coroutine_threadsafe(self._enable_feature(feature_name), loop=loop)
            fut.result()

        # Disable the feature if necessary
        elif new_handler is None and self._mqtt_pipeline.feature_enabled[feature_name]:
            # We have to call this on a loop running on a different thread in order to ensure
            # the setter can be called both within a coroutine (with a running event loop) and
            # outside of a coroutine (where no event loop is currently running)
            loop = loop_management.get_client_internal_loop()
            fut = asyncio.run_coroutine_threadsafe(self._disable_feature(feature_name), loop=loop)
            fut.result()

    async def shutdown(self):
        """Shut down the client for graceful exit.

        Once this method is called, any attempts at further client calls will result in a
        ClientError being raised

        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Initiating client shutdown")
        # Note that client disconnect does the following:
        #   - Disconnects the pipeline
        #   - Resolves all pending receiver handler calls
        #   - Stops receiver handler threads
        await self.disconnect()

        # Note that shutting down does the following:
        #   - Disconnects the MQTT pipeline
        #   - Stops MQTT pipeline threads
        logger.debug("Beginning pipeline shutdown operation")
        shutdown_async = async_adapter.emulate_async(self._mqtt_pipeline.shutdown)
        callback = async_adapter.AwaitableCallback()
        await shutdown_async(callback=callback)
        await handle_result(callback)
        logger.debug("Completed pipeline shutdown operation")

        # Stop the Client Event handlers now that everything else is completed
        self._handler_manager.stop(receiver_handlers_only=False)

        # Yes, that means the pipeline is disconnected twice (well, actually three times if you
        # consider that the client-level disconnect causes two pipeline-level disconnects for
        # reasons explained in comments in the client's .disconnect() method).
        #
        # This last disconnect that occurs as a result of the pipeline shutdown is a bit different
        # from the first though, in that it's more "final" and can't simply just be reconnected.

        # Note also that only the MQTT pipeline is shut down. The reason is twofold:
        #   1. There are no known issues related to graceful exit if the HTTP pipeline is not
        #      explicitly shut down
        #   2. The HTTP pipeline is planned for eventual removal from the client
        # In light of these two facts, it seemed irrelevant to spend time implementing shutdown
        # capability for HTTP pipeline.
        logger.info("Client shutdown complete")

    async def connect(self):
        """Connects the client to an Azure IoT Hub or Azure IoT Edge Hub instance.

        The destination is chosen based on the credentials passed via the auth_provider parameter
        that was provided when this object was initialized.

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.OperationTimeout` if the connection times out.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Connecting to Hub...")
        connect_async = async_adapter.emulate_async(self._mqtt_pipeline.connect)

        callback = async_adapter.AwaitableCallback()
        await connect_async(callback=callback)
        await handle_result(callback)

        logger.info("Successfully connected to Hub")

    async def disconnect(self):
        """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance.

        It is recommended that you make sure to call this coroutine when you are completely done
        with the your client instance.

        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Disconnecting from Hub...")

        logger.debug("Executing initial disconnect")
        disconnect_async = async_adapter.emulate_async(self._mqtt_pipeline.disconnect)
        callback = async_adapter.AwaitableCallback()
        await disconnect_async(callback=callback)
        await handle_result(callback)
        logger.debug("Successfully executed initial disconnect")

        # Note that in the process of stopping the handlers and resolving pending calls
        # a user-supplied handler may cause a reconnection to occur
        logger.debug("Stopping handlers...")
        self._handler_manager.stop(receiver_handlers_only=True)
        logger.debug("Successfully stopped handlers")

        # Disconnect again to ensure disconnection has ocurred due to the issue mentioned above
        logger.debug("Executing secondary disconnect...")
        disconnect_async = async_adapter.emulate_async(self._mqtt_pipeline.disconnect)
        callback = async_adapter.AwaitableCallback()
        await disconnect_async(callback=callback)
        await handle_result(callback)
        logger.debug("Successfully executed secondary disconnect")

        # It's also possible that in the (very short) time between stopping the handlers and
        # the second disconnect, additional items were received (e.g. C2D Message)
        # Currently, this isn't really possible to accurately check due to a
        # race condition / thread timing issue with inboxes where we can't guarantee how many
        # items are truly in them.
        # It has always been true of this client, even before handlers.
        #
        # However, even if the race condition is addressed, that will only allow us to log that
        # messages were lost. To actually fix the problem, IoTHub needs to support MQTT5 so that
        # we can unsubscribe from receiving data.

        logger.info("Successfully disconnected from Hub")

    async def update_sastoken(self, sastoken):
        """
        Update the client's SAS Token used for authentication, then reauthorizes the connection.

        This API can only be used if the client was initially created with a SAS Token.

        :param str sastoken: The new SAS Token string for the client to use

        :raises: ValueError if the sastoken parameter is invalid
        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be re-established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a re-establishing
            the connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.OperationTimeout` if the reauthorization
            attempt times out.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if the client was not initially
            created with a SAS token.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        self._replace_user_supplied_sastoken(sastoken)

        # Reauthorize the connection
        logger.info("Reauthorizing connection with Hub...")
        reauth_connection_async = async_adapter.emulate_async(
            self._mqtt_pipeline.reauthorize_connection
        )
        callback = async_adapter.AwaitableCallback()
        await reauth_connection_async(callback=callback)
        await handle_result(callback)
        # NOTE: Currently due to the MQTT3 implemenation, the pipeline reauthorization will return
        # after the disconnect. It does not wait for the reconnect to complete. This means that
        # any errors that may occur as part of the connect will not return via this callback.
        # They will instead go to the background exception handler.

        logger.info("Successfully reauthorized connection to Hub")

    async def send_message(self, message):
        """Sends a message to the default events endpoint on the Azure IoT Hub or Azure IoT Edge Hub instance.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param message: The actual message to send. Anything passed that is not an instance of the
            Message class will be converted to Message object.
        :type message: :class:`azure.iot.device.Message` or str

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.OperationTimeout` if connection attempt
            times out
        :raises: :class:`azure.iot.device.exceptions.NoConnectionError` if the client is not
            connected (and there is no auto-connect enabled)
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        :raises: ValueError if the message fails size validation.
        """
        if not isinstance(message, Message):
            message = Message(message)

        if message.get_size() > device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT:
            raise ValueError("Size of telemetry message can not exceed 256 KB.")

        logger.info("Sending message to Hub...")
        send_message_async = async_adapter.emulate_async(self._mqtt_pipeline.send_message)

        callback = async_adapter.AwaitableCallback()
        await send_message_async(message, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent message to Hub")

    @deprecation.deprecated(
        deprecated_in="2.3.0",
        current_version=device_constant.VERSION,
        details="We recommend that you use the .on_method_request_received property to set a handler instead",
    )
    async def receive_method_request(self, method_name=None):
        """Receive a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :param str method_name: Optionally provide the name of the method to receive requests for.
            If this parameter is not given, all methods not already being specifically targeted by
            a different call to receive_method will be received.

        :returns: MethodRequest object representing the received method request.
        :rtype: :class:`azure.iot.device.MethodRequest`
        """
        self._check_receive_mode_is_api()

        if not self._mqtt_pipeline.feature_enabled[constant.METHODS]:
            await self._enable_feature(constant.METHODS)

        method_inbox = self._inbox_manager.get_method_request_inbox(method_name)

        logger.info("Waiting for method request...")
        method_request = await method_inbox.get()
        logger.info("Received method request")
        return method_request

    async def send_method_response(self, method_response):
        """Send a response to a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param method_response: The MethodResponse to send
        :type method_response: :class:`azure.iot.device.MethodResponse`

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.OperationTimeout` if connection attempt
            times out
        :raises: :class:`azure.iot.device.exceptions.NoConnectionError` if the client is not
            connected (and there is no auto-connect enabled)
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Sending method response to Hub...")
        send_method_response_async = async_adapter.emulate_async(
            self._mqtt_pipeline.send_method_response
        )

        callback = async_adapter.AwaitableCallback()

        # TODO: maybe consolidate method_request, result and status into a new object
        await send_method_response_async(method_response, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent method response to Hub")

    async def get_twin(self):
        """
        Gets the device or module twin from the Azure IoT Hub or Azure IoT Edge Hub service.

        :returns: Complete Twin as a JSON dict
        :rtype: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.OperationTimeout` if connection attempt
            times out
        :raises: :class:`azure.iot.device.exceptions.NoConnectionError` if the client is not
            connected (and there is no auto-connect enabled)
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Getting twin")

        if not self._mqtt_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        get_twin_async = async_adapter.emulate_async(self._mqtt_pipeline.get_twin)

        callback = async_adapter.AwaitableCallback(return_arg_name="twin")
        await get_twin_async(callback=callback)
        twin = await handle_result(callback)
        logger.info("Successfully retrieved twin")
        return twin

    async def patch_twin_reported_properties(self, reported_properties_patch):
        """
        Update reported properties with the Azure IoT Hub or Azure IoT Edge Hub service.

        If the service returns an error on the patch operation, this function will raise the
        appropriate error.

        :param reported_properties_patch: Twin Reported Properties patch as a JSON dict
        :type reported_properties_patch: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.OperationTimeout` if connection attempt
            times out
        :raises: :class:`azure.iot.device.exceptions.NoConnectionError` if the client is not
            connected (and there is no auto-connect enabled)
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Patching twin reported properties")

        if not self._mqtt_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        patch_twin_async = async_adapter.emulate_async(
            self._mqtt_pipeline.patch_twin_reported_properties
        )

        callback = async_adapter.AwaitableCallback()
        await patch_twin_async(patch=reported_properties_patch, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent twin patch")

    @deprecation.deprecated(
        deprecated_in="2.3.0",
        current_version=device_constant.VERSION,
        details="We recommend that you use the .on_twin_desired_properties_patch_received property to set a handler instead",
    )
    async def receive_twin_desired_properties_patch(self):
        """
        Receive a desired property patch via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :returns: Twin Desired Properties patch as a JSON dict
        :rtype: dict
        """
        self._check_receive_mode_is_api()

        if not self._mqtt_pipeline.feature_enabled[constant.TWIN_PATCHES]:
            await self._enable_feature(constant.TWIN_PATCHES)
        twin_patch_inbox = self._inbox_manager.get_twin_patch_inbox()

        logger.info("Waiting for twin patches...")
        patch = await twin_patch_inbox.get()
        logger.info("twin patch received")
        return patch
Beispiel #4
0
class GenericIoTHubClient(AbstractIoTHubClient):
    """A super class representing a generic asynchronous client.
    This class needs to be extended for specific clients.
    """
    def __init__(self, **kwargs):
        """Initializer for a generic asynchronous client.

        This initializer should not be called directly.
        Instead, use one of the 'create_from_' classmethods to instantiate

        :param iothub_pipeline: The IoTHubPipeline used for the client
        :type iothub_pipeline: :class:`azure.iot.device.iothub.pipeline.IoTHubPipeline`
        :param edge_pipeline: The EdgePipeline used for the client
        :type edge_pipeline: :class:`azure.iot.device.iothub.pipeline.EdgePipeline`
        """
        # Depending on the subclass calling this __init__, there could be different arguments,
        # and the super() call could call a different class, due to the different MROs
        # in the class hierarchies of different clients. Thus, args here must be passed along as
        # **kwargs.
        super().__init__(**kwargs)
        self._inbox_manager = InboxManager(inbox_type=AsyncClientInbox)
        self._iothub_pipeline.on_connected = self._on_connected
        self._iothub_pipeline.on_disconnected = self._on_disconnected
        self._iothub_pipeline.on_method_request_received = self._inbox_manager.route_method_request
        self._iothub_pipeline.on_twin_patch_received = self._inbox_manager.route_twin_patch

    def _on_connected(self):
        """Helper handler that is called upon an iothub pipeline connect"""
        logger.info("Connection State - Connected")

    def _on_disconnected(self):
        """Helper handler that is called upon an iothub pipeline disconnect"""
        logger.info("Connection State - Disconnected")
        self._inbox_manager.clear_all_method_requests()
        logger.info("Cleared all pending method requests due to disconnect")

    async def connect(self):
        """Connects the client to an Azure IoT Hub or Azure IoT Edge Hub instance.

        The destination is chosen based on the credentials passed via the auth_provider parameter
        that was provided when this object was initialized.

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Connecting to Hub...")
        connect_async = async_adapter.emulate_async(
            self._iothub_pipeline.connect)

        callback = async_adapter.AwaitableCallback()
        await connect_async(callback=callback)
        await handle_result(callback)

        logger.info("Successfully connected to Hub")

    async def disconnect(self):
        """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance.

        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Disconnecting from Hub...")
        disconnect_async = async_adapter.emulate_async(
            self._iothub_pipeline.disconnect)

        callback = async_adapter.AwaitableCallback()
        await disconnect_async(callback=callback)
        await handle_result(callback)

        logger.info("Successfully disconnected from Hub")

    async def send_message(self, message):
        """Sends a message to the default events endpoint on the Azure IoT Hub or Azure IoT Edge Hub instance.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param message: The actual message to send. Anything passed that is not an instance of the
            Message class will be converted to Message object.
        :type message: :class:`azure.iot.device.Message` or str

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        if not isinstance(message, Message):
            message = Message(message)

        logger.info("Sending message to Hub...")
        send_message_async = async_adapter.emulate_async(
            self._iothub_pipeline.send_message)

        callback = async_adapter.AwaitableCallback()
        await send_message_async(message, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent message to Hub")

    async def receive_method_request(self, method_name=None):
        """Receive a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :param str method_name: Optionally provide the name of the method to receive requests for.
            If this parameter is not given, all methods not already being specifically targeted by
            a different call to receive_method will be received.

        :returns: MethodRequest object representing the received method request.
        :rtype: `azure.iot.device.MethodRequest`
        """
        if not self._iothub_pipeline.feature_enabled[constant.METHODS]:
            await self._enable_feature(constant.METHODS)

        method_inbox = self._inbox_manager.get_method_request_inbox(
            method_name)

        logger.info("Waiting for method request...")
        method_request = await method_inbox.get()
        logger.info("Received method request")
        return method_request

    async def send_method_response(self, method_response):
        """Send a response to a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param method_response: The MethodResponse to send
        :type method_response: :class:`azure.iot.device.MethodResponse`

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Sending method response to Hub...")
        send_method_response_async = async_adapter.emulate_async(
            self._iothub_pipeline.send_method_response)

        callback = async_adapter.AwaitableCallback()

        # TODO: maybe consolidate method_request, result and status into a new object
        await send_method_response_async(method_response, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent method response to Hub")

    async def _enable_feature(self, feature_name):
        """Enable an Azure IoT Hub feature

        :param feature_name: The name of the feature to enable.
            See azure.iot.device.common.pipeline.constant for possible values.
        """
        logger.info("Enabling feature:" + feature_name + "...")
        enable_feature_async = async_adapter.emulate_async(
            self._iothub_pipeline.enable_feature)

        callback = async_adapter.AwaitableCallback()
        await enable_feature_async(feature_name, callback=callback)
        await handle_result(callback)

        logger.info("Successfully enabled feature:" + feature_name)

    async def get_twin(self):
        """
        Gets the device or module twin from the Azure IoT Hub or Azure IoT Edge Hub service.

        :returns: Complete Twin as a JSON dict
        :rtype: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Getting twin")

        if not self._iothub_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        get_twin_async = async_adapter.emulate_async(
            self._iothub_pipeline.get_twin)

        callback = async_adapter.AwaitableCallback(return_arg_name="twin")
        await get_twin_async(callback=callback)
        twin = await handle_result(callback)
        logger.info("Successfully retrieved twin")
        return twin

    async def patch_twin_reported_properties(self, reported_properties_patch):
        """
        Update reported properties with the Azure IoT Hub or Azure IoT Edge Hub service.

        If the service returns an error on the patch operation, this function will raise the
        appropriate error.

        :param reported_properties_patch: Twin Reported Properties patch as a JSON dict
        :type reported_properties_patch: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Patching twin reported properties")

        if not self._iothub_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        patch_twin_async = async_adapter.emulate_async(
            self._iothub_pipeline.patch_twin_reported_properties)

        callback = async_adapter.AwaitableCallback()
        await patch_twin_async(patch=reported_properties_patch,
                               callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent twin patch")

    async def receive_twin_desired_properties_patch(self):
        """
        Receive a desired property patch via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :returns: Twin Desired Properties patch as a JSON dict
        :rtype: dict
        """
        if not self._iothub_pipeline.feature_enabled[constant.TWIN_PATCHES]:
            await self._enable_feature(constant.TWIN_PATCHES)
        twin_patch_inbox = self._inbox_manager.get_twin_patch_inbox()

        logger.info("Waiting for twin patches...")
        patch = await twin_patch_inbox.get()
        logger.info("twin patch received")
        return patch
class GenericIoTHubClient(AbstractIoTHubClient):
    """A super class representing a generic asynchronous client.
    This class needs to be extended for specific clients.
    """

    def __init__(self, **kwargs):
        """Initializer for a generic asynchronous client.

        This initializer should not be called directly.
        Instead, use one of the 'create_from_' classmethods to instantiate

        :param mqtt_pipeline: The MQTTPipeline used for the client
        :type mqtt_pipeline: :class:`azure.iot.device.iothub.pipeline.MQTTPipeline`
        :param http_pipeline: The HTTPPipeline used for the client
        :type http_pipeline: :class:`azure.iot.device.iothub.pipeline.HTTPPipeline`
        """
        # Depending on the subclass calling this __init__, there could be different arguments,
        # and the super() call could call a different class, due to the different MROs
        # in the class hierarchies of different clients. Thus, args here must be passed along as
        # **kwargs.
        super().__init__(**kwargs)
        self._inbox_manager = InboxManager(inbox_type=AsyncClientInbox)
        self._handler_manager = async_handler_manager.AsyncHandlerManager(self._inbox_manager)

        # Set pipeline handlers
        self._mqtt_pipeline.on_connected = self._on_connected
        self._mqtt_pipeline.on_disconnected = self._on_disconnected
        self._mqtt_pipeline.on_method_request_received = self._inbox_manager.route_method_request
        self._mqtt_pipeline.on_twin_patch_received = self._inbox_manager.route_twin_patch

    async def _enable_feature(self, feature_name):
        """Enable an Azure IoT Hub feature

        :param feature_name: The name of the feature to enable.
            See azure.iot.device.common.pipeline.constant for possible values.
        """
        logger.info("Enabling feature:" + feature_name + "...")
        if not self._mqtt_pipeline.feature_enabled[feature_name]:
            # Enable the feature if not already enabled
            enable_feature_async = async_adapter.emulate_async(self._mqtt_pipeline.enable_feature)

            callback = async_adapter.AwaitableCallback()
            await enable_feature_async(feature_name, callback=callback)
            await handle_result(callback)

            logger.info("Successfully enabled feature:" + feature_name)
        else:
            # This branch shouldn't be reached, but in case it is, log it
            logger.info("Feature ({}) already enabled - skipping".format(feature_name))

    async def _disable_feature(self, feature_name):
        """Disable an Azure IoT Hub feature

        :param feature_name: The name of the feature to enable.
            See azure.iot.device.common.pipeline.constant for possible values.
        """
        logger.info("Disabling feature: {}...".format(feature_name))
        if self._mqtt_pipeline.feature_enabled[feature_name]:
            # Disable the feature if not already disabled
            disable_feature_async = async_adapter.emulate_async(self._mqtt_pipeline.disable_feature)

            callback = async_adapter.AwaitableCallback()
            await disable_feature_async(feature_name, callback=callback)
            await handle_result(callback)

            logger.info("Successfully disabled feature: {}".format(feature_name))
        else:
            # This branch shouldn't be reached, but in case it is, log it
            logger.info("Feature ({}) already disabled - skipping".format(feature_name))

    async def connect(self):
        """Connects the client to an Azure IoT Hub or Azure IoT Edge Hub instance.

        The destination is chosen based on the credentials passed via the auth_provider parameter
        that was provided when this object was initialized.

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Connecting to Hub...")
        connect_async = async_adapter.emulate_async(self._mqtt_pipeline.connect)

        callback = async_adapter.AwaitableCallback()
        await connect_async(callback=callback)
        await handle_result(callback)

        logger.info("Successfully connected to Hub")

    async def disconnect(self):
        """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance.

        It is recommended that you make sure to call this coroutine when you are completely done
        with the your client instance.

        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Disconnecting from Hub...")

        logger.debug("Executing initial disconnect")
        disconnect_async = async_adapter.emulate_async(self._mqtt_pipeline.disconnect)
        callback = async_adapter.AwaitableCallback()
        await disconnect_async(callback=callback)
        await handle_result(callback)
        logger.debug("Successfully executed initial disconnect")

        # Note that in the process of stopping the handlers and resolving pending calls
        # a user-supplied handler may cause a reconnection to occur
        logger.debug("Stopping handlers...")
        self._handler_manager.stop()
        logger.debug("Successfully stopped handlers")

        # Disconnect again to ensure disconnection has ocurred due to the issue mentioned above
        logger.debug("Executing secondary disconnect...")
        disconnect_async = async_adapter.emulate_async(self._mqtt_pipeline.disconnect)
        callback = async_adapter.AwaitableCallback()
        await disconnect_async(callback=callback)
        await handle_result(callback)
        logger.debug("Successfully executed secondary disconnect")

        # It's also possible that in the (very short) time between stopping the handlers and
        # the second disconnect, additional items were received (e.g. C2D Message)
        # Currently, this isn't really possible to accurately check due to a race condition.
        # It has always been true of this client, even before handlers.
        # TODO: fix the race condition
        # However, even if the race condition is addressed, that will only allow us to log that
        # messages were lost. To actually fix the problem, IoTHub needs to support MQTT5 so that
        # we can unsubscribe from receiving data.

        logger.info("Successfully disconnected from Hub")

    async def send_message(self, message):
        """Sends a message to the default events endpoint on the Azure IoT Hub or Azure IoT Edge Hub instance.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param message: The actual message to send. Anything passed that is not an instance of the
            Message class will be converted to Message object.
        :type message: :class:`azure.iot.device.Message` or str

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        :raises: ValueError if the message fails size validation.
        """
        if not isinstance(message, Message):
            message = Message(message)

        if message.get_size() > device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT:
            raise ValueError("Size of telemetry message can not exceed 256 KB.")

        logger.info("Sending message to Hub...")
        send_message_async = async_adapter.emulate_async(self._mqtt_pipeline.send_message)

        callback = async_adapter.AwaitableCallback()
        await send_message_async(message, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent message to Hub")

    @deprecation.deprecated(
        deprecated_in="2.3.0",
        current_version=device_constant.VERSION,
        details="We recommend that you use the .on_method_request_received property to set a handler instead",
    )
    async def receive_method_request(self, method_name=None):
        """Receive a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :param str method_name: Optionally provide the name of the method to receive requests for.
            If this parameter is not given, all methods not already being specifically targeted by
            a different call to receive_method will be received.

        :returns: MethodRequest object representing the received method request.
        :rtype: :class:`azure.iot.device.MethodRequest`
        """
        self._check_receive_mode_is_api()

        if not self._mqtt_pipeline.feature_enabled[constant.METHODS]:
            await self._enable_feature(constant.METHODS)

        method_inbox = self._inbox_manager.get_method_request_inbox(method_name)

        logger.info("Waiting for method request...")
        method_request = await method_inbox.get()
        logger.info("Received method request")
        return method_request

    async def send_method_response(self, method_response):
        """Send a response to a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param method_response: The MethodResponse to send
        :type method_response: :class:`azure.iot.device.MethodResponse`

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Sending method response to Hub...")
        send_method_response_async = async_adapter.emulate_async(
            self._mqtt_pipeline.send_method_response
        )

        callback = async_adapter.AwaitableCallback()

        # TODO: maybe consolidate method_request, result and status into a new object
        await send_method_response_async(method_response, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent method response to Hub")

    async def get_twin(self):
        """
        Gets the device or module twin from the Azure IoT Hub or Azure IoT Edge Hub service.

        :returns: Complete Twin as a JSON dict
        :rtype: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Getting twin")

        if not self._mqtt_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        get_twin_async = async_adapter.emulate_async(self._mqtt_pipeline.get_twin)

        callback = async_adapter.AwaitableCallback(return_arg_name="twin")
        await get_twin_async(callback=callback)
        twin = await handle_result(callback)
        logger.info("Successfully retrieved twin")
        return twin

    async def patch_twin_reported_properties(self, reported_properties_patch):
        """
        Update reported properties with the Azure IoT Hub or Azure IoT Edge Hub service.

        If the service returns an error on the patch operation, this function will raise the
        appropriate error.

        :param reported_properties_patch: Twin Reported Properties patch as a JSON dict
        :type reported_properties_patch: dict

        :raises: :class:`azure.iot.device.exceptions.CredentialError` if credentials are invalid
            and a connection cannot be established.
        :raises: :class:`azure.iot.device.exceptions.ConnectionFailedError` if a establishing a
            connection results in failure.
        :raises: :class:`azure.iot.device.exceptions.ConnectionDroppedError` if connection is lost
            during execution.
        :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
            during execution.
        """
        logger.info("Patching twin reported properties")

        if not self._mqtt_pipeline.feature_enabled[constant.TWIN]:
            await self._enable_feature(constant.TWIN)

        patch_twin_async = async_adapter.emulate_async(
            self._mqtt_pipeline.patch_twin_reported_properties
        )

        callback = async_adapter.AwaitableCallback()
        await patch_twin_async(patch=reported_properties_patch, callback=callback)
        await handle_result(callback)

        logger.info("Successfully sent twin patch")

    @deprecation.deprecated(
        deprecated_in="2.3.0",
        current_version=device_constant.VERSION,
        details="We recommend that you use the .on_twin_desired_properties_patch_received property to set a handler instead",
    )
    async def receive_twin_desired_properties_patch(self):
        """
        Receive a desired property patch via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :returns: Twin Desired Properties patch as a JSON dict
        :rtype: dict
        """
        self._check_receive_mode_is_api()

        if not self._mqtt_pipeline.feature_enabled[constant.TWIN_PATCHES]:
            await self._enable_feature(constant.TWIN_PATCHES)
        twin_patch_inbox = self._inbox_manager.get_twin_patch_inbox()

        logger.info("Waiting for twin patches...")
        patch = await twin_patch_inbox.get()
        logger.info("twin patch received")
        return patch

    def _generic_handler_setter(self, handler_name, feature_name, new_handler):
        self._check_receive_mode_is_handler()
        # Set the handler on the handler manager
        setattr(self._handler_manager, handler_name, new_handler)

        # Enable the feature if necessary
        if new_handler is not None and not self._mqtt_pipeline.feature_enabled[feature_name]:
            # We have to call this on a loop running on a different thread in order to ensure
            # the setter can be called both within a coroutine (with a running event loop) and
            # outside of a coroutine (where no event loop is currently running)
            loop = loop_management.get_client_internal_loop()
            fut = asyncio.run_coroutine_threadsafe(self._enable_feature(feature_name), loop=loop)
            fut.result()

        # Disable the feature if necessary
        elif new_handler is None and self._mqtt_pipeline.feature_enabled[feature_name]:
            # We have to call this on a loop running on a different thread in order to ensure
            # the setter can be called both within a coroutine (with a running event loop) and
            # outside of a coroutine (where no event loop is currently running)
            loop = loop_management.get_client_internal_loop()
            fut = asyncio.run_coroutine_threadsafe(self._disable_feature(feature_name), loop=loop)
            fut.result()

    @property
    def on_twin_desired_properties_patch_received(self):
        """The handler function or coroutine that will be called when a twin desired properties
        patch is received.

        The function or coroutine definition should take one positional argument (the twin patch
        in the form of a JSON dictionary object)"""
        return self._handler_manager.on_twin_desired_properties_patch_received

    @on_twin_desired_properties_patch_received.setter
    def on_twin_desired_properties_patch_received(self, value):
        self._generic_handler_setter(
            "on_twin_desired_properties_patch_received", constant.TWIN_PATCHES, value
        )

    @property
    def on_method_request_received(self):
        """The handler function or coroutine that will be called when a method request is received.

        The function or coroutine definition should take one positional argument (the
        :class:`azure.iot.device.MethodRequest` object)"""
        return self._handler_manager.on_method_request_received

    @on_method_request_received.setter
    def on_method_request_received(self, value):
        self._generic_handler_setter("on_method_request_received", constant.METHODS, value)
class GenericIoTHubClient(AbstractIoTHubClient):
    """A super class representing a generic asynchronous client.
    This class needs to be extended for specific clients.
    """
    def __init__(self, transport):
        """Initializer for a generic asynchronous client.

        This initializer should not be called directly.
        Instead, the class method `from_authentication_provider` should be used to create a client object.

        :param transport: The transport that the client will use.
        """
        super().__init__(transport)
        self._inbox_manager = InboxManager(inbox_type=AsyncClientInbox)
        self._transport.on_transport_connected = self._on_state_change
        self._transport.on_transport_disconnected = self._on_state_change
        self._transport.on_transport_method_request_received = (
            self._inbox_manager.route_method_request)

    def _on_state_change(self, new_state):
        """Handler to be called by the transport upon a connection state change."""
        logger.info("Connection State - {}".format(new_state))

        if new_state == "disconnected":
            self._on_disconnected()

    def _on_disconnected(self):
        """Helper handler that is called upon a a transport disconnect"""
        self._inbox_manager.clear_all_method_requests()
        logger.info("Cleared all pending method requests due to disconnect")

    async def connect(self):
        """Connects the client to an Azure IoT Hub or Azure IoT Edge Hub instance.

        The destination is chosen based on the credentials passed via the auth_provider parameter
        that was provided when this object was initialized.
        """
        logger.info("Connecting to Hub...")
        connect_async = async_adapter.emulate_async(self._transport.connect)

        def sync_callback():
            logger.info("Successfully connected to Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        await connect_async(callback=callback)
        await callback.completion()

    async def disconnect(self):
        """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance.
        """
        logger.info("Disconnecting from Hub...")
        disconnect_async = async_adapter.emulate_async(
            self._transport.disconnect)

        def sync_callback():
            logger.info("Successfully disconnected from Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        await disconnect_async(callback=callback)
        await callback.completion()

    async def send_event(self, message):
        """Sends a message to the default events endpoint on the Azure IoT Hub or Azure IoT Edge Hub instance.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param message: The actual message to send. Anything passed that is not an instance of the
        Message class will be converted to Message object.
        """
        if not isinstance(message, Message):
            message = Message(message)

        logger.info("Sending message to Hub...")
        send_event_async = async_adapter.emulate_async(
            self._transport.send_event)

        def sync_callback():
            logger.info("Successfully sent message to Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        await send_event_async(message, callback=callback)
        await callback.completion()

    async def receive_method_request(self, method_name=None):
        """Receive a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If no method request is yet available, will wait until it is available.

        :param str method_name: Optionally provide the name of the method to receive requests for.
        If this parameter is not given, all methods not already being specifically targeted by
        a different call to receive_method will be received.

        :returns: MethodRequest object representing the received method request.
        """
        if not self._transport.feature_enabled[constant.METHODS]:
            await self._enable_feature(constant.METHODS)

        method_inbox = self._inbox_manager.get_method_request_inbox(
            method_name)

        logger.info("Waiting for method request...")
        method_request = await method_inbox.get()
        logger.info("Received method request")
        return method_request

    async def send_method_response(self, method_response):
        """Send a response to a method request via the Azure IoT Hub or Azure IoT Edge Hub.

        If the connection to the service has not previously been opened by a call to connect, this
        function will open the connection before sending the event.

        :param method_response: The MethodResponse to send
        """
        logger.info("Sending method response to Hub...")
        send_method_response_async = async_adapter.emulate_async(
            self._transport.send_method_response)

        def sync_callback():
            logger.info("Successfully sent method response to Hub")

        callback = async_adapter.AwaitableCallback(sync_callback)

        # TODO: maybe consolidate method_request, result and status into a new object
        await send_method_response_async(method_response, callback=callback)
        await callback.completion()

    async def _enable_feature(self, feature_name):
        """Enable an Azure IoT Hub feature in the transport

        :param feature_name: The name of the feature to enable.
        See azure.iot.device.common.transport.constant for possible values.
        """
        logger.info("Enabling feature:" + feature_name + "...")
        enable_feature_async = async_adapter.emulate_async(
            self._transport.enable_feature)

        def sync_callback():
            logger.info("Successfully enabled feature:" + feature_name)

        callback = async_adapter.AwaitableCallback(sync_callback)

        await enable_feature_async(feature_name, callback=callback)