def __init__(self, pipeline_configuration): """ Constructor for instantiating a pipeline adapter object. :param auth_provider: The authentication provider :param pipeline_configuration: The configuration generated based on user inputs """ # NOTE: This pipeline DOES NOT handle SasToken management! # (i.e. using a SasTokenStage) # It instead relies on the parallel MQTT pipeline to handle that. # # Because they share a pipeline configuration, and MQTT has renewal logic we can be sure # that the SasToken in the pipeline configuration is valid. # # Furthermore, because HTTP doesn't require constant connections or long running tokens, # there's no need to reauthorize connections, so we can just pass the token from the config # when needed for auth. # # This is not an ideal solution, but it's the simplest one for the time being. # Contains data and information shared globally within the pipeline self._nucleus = pipeline_nucleus.PipelineNucleus( pipeline_configuration) self._pipeline = (pipeline_stages_base.PipelineRootStage( self._nucleus).append_stage( pipeline_stages_iothub_http.IoTHubHTTPTranslationStage( )).append_stage(pipeline_stages_http.HTTPTransportStage())) callback = EventedCallback() op = pipeline_ops_base.InitializePipelineOperation(callback=callback) self._pipeline.run_op(op) callback.wait_for_completion()
def register(self): """ Register the device with the with the provisioning service This is a synchronous call, meaning that this function will not return until the registration process has completed successfully or the attempt has resulted in a failure. Before returning, the client will also disconnect from the provisioning service. If a registration attempt is made while a previous registration is in progress it may throw an error. :returns: RegistrationResult indicating the result of the registration. :rtype: :class:`azure.iot.device.RegistrationResult` :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("Registering with Provisioning Service...") if not self._pipeline.responses_enabled[dps_constant.REGISTER]: self._enable_responses() register_complete = EventedCallback(return_arg_name="result") self._pipeline.register(payload=self._provisioning_payload, callback=register_complete) result = handle_result(register_complete) log_on_register_complete(result) return result
def send_method_response(self, method_response): """Send a response to a method request via the Azure IoT Hub or Azure IoT Edge Hub. This is a synchronous event, meaning that this function will not return until the event has been sent to the service and the service has acknowledged receipt of the event. 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...") callback = EventedCallback() self._mqtt_pipeline.send_method_response(method_response, callback=callback) handle_result(callback) logger.info("Successfully sent method response to Hub")
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. Note also that this API may return before the reauthorization/reconnection is completed. This means that some errors that may occur as part of the reconnection could occur in the background, and will not be raised by this method. :param str sastoken: The new SAS Token string for the client to use :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. :raises: ValueError if the sastoken parameter is invalid """ self._replace_user_supplied_sastoken(sastoken) # Reauthorize the connection logger.info("Reauthorizing connection with Hub...") callback = EventedCallback() self._mqtt_pipeline.reauthorize_connection(callback=callback) 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")
def get_twin(self): """ Gets the device or module twin from the Azure IoT Hub or Azure IoT Edge Hub service. This is a synchronous call, meaning that this function will not return until the twin has been retrieved from the 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. """ if not self._mqtt_pipeline.feature_enabled[pipeline_constant.TWIN]: self._enable_feature(pipeline_constant.TWIN) callback = EventedCallback(return_arg_name="twin") self._mqtt_pipeline.get_twin(callback=callback) twin = handle_result(callback) logger.info("Successfully retrieved twin") return twin
def send_message_to_output(self, message, output_name): """Sends an event/message to the given module output. These are outgoing events and are meant to be "output events". This is a synchronous event, meaning that this function will not return until the event has been sent to the service and the service has acknowledged receipt of the event. 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: message to send to the given output. Anything passed that is not an instance of the Message class will be converted to Message object. :param output_name: Name of the output to send the event to. """ if not isinstance(message, Message): message = Message(message) message.output_name = output_name logger.info("Sending message to output:" + output_name + "...") callback = EventedCallback() self._iothub_pipeline.send_output_event(message, callback=callback) callback.wait_for_completion() logger.info("Successfully sent message to output: " + output_name)
def patch_twin_reported_properties(self, reported_properties_patch): """ Update reported properties with the Azure IoT Hub or Azure IoT Edge Hub service. This is a synchronous call, meaning that this function will not return until the patch has been sent to the service and acknowledged. 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. """ if not self._mqtt_pipeline.feature_enabled[pipeline_constant.TWIN]: self._enable_feature(pipeline_constant.TWIN) callback = EventedCallback() self._mqtt_pipeline.patch_twin_reported_properties( patch=reported_properties_patch, callback=callback) handle_result(callback) logger.info("Successfully patched twin")
def send_message_to_output(self, message, output_name): """Sends an event/message to the given module output. These are outgoing events and are meant to be "output events". This is a synchronous event, meaning that this function will not return until the event has been sent to the service and the service has acknowledged receipt of the event. 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: Message to send to the given output. Anything passed that is not an instance of the Message class will be converted to Message object. :param str output_name: Name of the output to send the event to. :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) message.output_name = output_name logger.info("Sending message to output:" + output_name + "...") callback = EventedCallback() self._iothub_pipeline.send_output_event(message, callback=callback) handle_result(callback) logger.info("Successfully sent message to output: " + output_name)
def send_message(self, message): """Sends a message to the default events endpoint on the Azure IoT Hub or Azure IoT Edge Hub instance. This is a synchronous event, meaning that this function will not return until the event has been sent to the service and the service has acknowledged receipt of the event. 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...") callback = EventedCallback() self._iothub_pipeline.send_message(message, callback=callback) handle_result(callback) logger.info("Successfully sent message to Hub")
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. This is a synchronous call, meaning that this function will not return until the connection to the service has been completely established. :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...") callback = EventedCallback() self._mqtt_pipeline.connect(callback=callback) handle_result(callback) logger.info("Successfully connected to Hub")
def test_calling_object_sets_event(self): callback = EventedCallback() assert not callback.completion_event.isSet() callback() sleep(0.1) # wait to give time to complete the callback assert callback.completion_event.isSet() assert not callback.exception callback.wait_for_completion()
def register(self): """ Register the device with the provisioning service This is a synchronous call, meaning that this function will not return until the registration process has completed successfully or the attempt has resulted in a failure. Before returning, the client will also disconnect from the provisioning service. If a registration attempt is made while a previous registration is in progress it may throw an error. Once the device is successfully registered, the client will no longer be operable. :returns: RegistrationResult indicating the result of the registration. :rtype: :class:`azure.iot.device.RegistrationResult` :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 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("Registering with Provisioning Service...") if not self._pipeline.responses_enabled[dps_constant.REGISTER]: self._enable_responses() # Register register_complete = EventedCallback(return_arg_name="result") self._pipeline.register(payload=self._provisioning_payload, callback=register_complete) result = handle_result(register_complete) log_on_register_complete(result) # Implicitly shut down the pipeline upon successful completion if result is not None and result.status == "assigned": logger.debug("Beginning pipeline shutdown operation") shutdown_complete = EventedCallback() self._pipeline.shutdown(callback=shutdown_complete) handle_result(shutdown_complete) logger.debug("Completed pipeline shutdown operation") return result
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 function when you are completely done with the your client instance. This is a synchronous call, meaning that this function will not return until the connection to the service has been completely closed. :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") callback = EventedCallback() self._mqtt_pipeline.disconnect(callback=callback) 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...") callback = EventedCallback() self._mqtt_pipeline.disconnect(callback=callback) 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. # This 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")
def test_calling_object_sets_event_with_return_arg_name( self, fake_return_arg_value): callback = EventedCallback(return_arg_name="arg_name") assert not callback.completion_event.isSet() callback(arg_name=fake_return_arg_value) sleep(0.1) # wait to give time to complete the callback assert callback.completion_event.isSet() assert not callback.exception assert callback.wait_for_completion() == fake_return_arg_value
def test_raises_error_without_return_arg_name(self, fake_error): callback = EventedCallback() assert not callback.completion_event.isSet() callback(error=fake_error) sleep(0.1) # wait to give time to complete the callback assert callback.completion_event.isSet() assert callback.exception == fake_error with pytest.raises(fake_error.__class__) as e_info: callback.wait_for_completion() assert e_info.value is fake_error
def test_raises_error_with_return_arg_name(self, arbitrary_exception): callback = EventedCallback(return_arg_name="arg_name") assert not callback.completion_event.isSet() callback(error=arbitrary_exception) sleep(0.1) # wait to give time to complete the callback assert callback.completion_event.isSet() assert callback.exception == arbitrary_exception with pytest.raises(arbitrary_exception.__class__) as e_info: callback.wait_for_completion() assert e_info.value is arbitrary_exception
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. """ callback = EventedCallback(return_arg_name="storage_info") self._http_pipeline.get_storage_info_for_blob(blob_name, callback=callback) storage_info = handle_result(callback) logger.info("Successfully retrieved storage_info") return storage_info
def disconnect(self): """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance. This is a synchronous call, meaning that this function will not return until the connection to the service has been completely closed. """ logger.info("Disconnecting from Hub...") callback = EventedCallback() self._iothub_pipeline.disconnect(callback=callback) callback.wait_for_completion() logger.info("Successfully disconnected from Hub")
def _enable_responses(self): """Enable to receive responses from Device Provisioning Service. This is a synchronous call, meaning that this function will not return until the feature has been enabled. """ logger.info("Enabling reception of response from Device Provisioning Service...") subscription_complete = EventedCallback() self._pipeline.enable_responses(callback=subscription_complete) handle_result(subscription_complete) logger.info("Successfully subscribed to Device Provisioning Service to receive responses")
def _enable_feature(self, feature_name): """Enable an Azure IoT Hub feature. This is a synchronous call, meaning that this function will not return until the feature has been enabled. :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 + "...") callback = EventedCallback() self._mqtt_pipeline.enable_feature(feature_name, callback=callback) callback.wait_for_completion() logger.info("Successfully enabled feature:" + feature_name)
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. This is a synchronous call, meaning that this function will not return until the connection to the service has been completely established. """ logger.info("Connecting to Hub...") callback = EventedCallback() self._iothub_pipeline.connect(callback=callback) callback.wait_for_completion() logger.info("Successfully connected to Hub")
def disconnect(self): """Disconnect the client from the Azure IoT Hub or Azure IoT Edge Hub instance. This is a synchronous call, meaning that this function will not return until the connection to the service has been completely closed. :raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure during execution. """ logger.info("Disconnecting from Hub...") callback = EventedCallback() self._mqtt_pipeline.disconnect(callback=callback) handle_result(callback) logger.info("Successfully disconnected from Hub")
def cancel(self): """ This is a synchronous call, meaning that this function will not return until the cancellation process has completed successfully or the attempt has resulted in a failure. Before returning the client will also disconnect from the provisioning service. In case there is no registration in process it will throw an error as there is no registration process to cancel. """ logger.info("Cancelling the current registration process") cancel_complete = EventedCallback() self._polling_machine.cancel(callback=cancel_complete) cancel_complete.wait_for_completion() logger.info("Successfully cancelled the current registration process")
def register(self): """ Register the device with the with thw provisioning service This is a synchronous call, meaning that this function will not return until the registration process has completed successfully or the attempt has resulted in a failure. Before returning the client will also disconnect from the provisioning service. If a registration attempt is made while a previous registration is in progress it may throw an error. """ logger.info("Registering with Provisioning Service...") register_complete = EventedCallback(return_arg_name="result") self._polling_machine.register(callback=register_complete) result = register_complete.wait_for_completion() log_on_register_complete(result) return result
def invoke_method(self, method_params, device_id, module_id=None): """Invoke a method from your client onto a device or module client, and receive the response to the method call. :param dict method_params: Should contain a method_name, payload, connect_timeout_in_seconds, response_timeout_in_seconds. :param str device_id: Device ID of the target device where the method will be invoked. :param str module_id: Module ID of the target module where the method will be invoked. (Optional) :returns: method_result should contain a status, and a payload :rtype: dict """ callback = EventedCallback(return_arg_name="invoke_method_response") self._http_pipeline.invoke_method( device_id, method_params, callback=callback, module_id=module_id ) invoke_method_response = handle_result(callback) logger.info("Successfully invoked method") return invoke_method_response
def send_message_to_output(self, message, output_name): """Sends an event/message to the given module output. These are outgoing events and are meant to be "output events". This is a synchronous event, meaning that this function will not return until the event has been sent to the service and the service has acknowledged receipt of the event. 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: Message to send to the given output. 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 :param str output_name: Name of the output to send the event to. :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 message can not exceed 256 KB.") message.output_name = output_name logger.info("Sending message to output:" + output_name + "...") callback = EventedCallback() self._mqtt_pipeline.send_output_message(message, callback=callback) handle_result(callback) logger.info("Successfully sent message to output: " + output_name)
def get_twin(self): """ Gets the device or module twin from the Azure IoT Hub or Azure IoT Edge Hub service. This is a synchronous call, meaning that this function will not return until the twin has been retrieved from the service. :returns: Twin object which was retrieved from the hub """ if not self._iothub_pipeline.feature_enabled[constant.TWIN]: self._enable_feature(constant.TWIN) callback = EventedCallback(return_arg_name="twin") self._iothub_pipeline.get_twin(callback=callback) twin = callback.wait_for_completion() logger.info("Successfully retrieved twin") return twin
def __init__(self, pipeline_configuration): """ Constructor for instantiating a pipeline adapter object. :param auth_provider: The authentication provider :param pipeline_configuration: The configuration generated based on user inputs """ self._pipeline = (pipeline_stages_base.PipelineRootStage( pipeline_configuration).append_stage( pipeline_stages_base.SasTokenRenewalStage()).append_stage( pipeline_stages_iothub_http.IoTHubHTTPTranslationStage( )).append_stage(pipeline_stages_http.HTTPTransportStage())) callback = EventedCallback() op = pipeline_ops_base.InitializePipelineOperation(callback=callback) self._pipeline.run_op(op) callback.wait_for_completion()
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. """ callback = EventedCallback() self._http_pipeline.notify_blob_upload_status( correlation_id=correlation_id, is_success=is_success, status_code=status_code, status_description=status_description, callback=callback, ) handle_result(callback) logger.info("Successfully notified blob upload status")
def _enable_feature(self, feature_name): """Enable an Azure IoT Hub feature. This is a synchronous call, meaning that this function will not return until the feature has been enabled. :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]: callback = EventedCallback() self._mqtt_pipeline.enable_feature(feature_name, callback=callback) callback.wait_for_completion() 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 disabled - skipping".format(feature_name))