From d00392a63745d6178db70ed645d166d654e4da08 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 13 Apr 2021 09:23:34 -0700 Subject: [PATCH 01/13] Changes to work with the latest miniMQTT updates --- adafruit_azureiot/constants.py | 2 +- adafruit_azureiot/device_registration.py | 6 ++++-- adafruit_azureiot/iot_mqtt.py | 4 ++-- adafruit_azureiot/iotcentral_device.py | 11 ++++++++++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/adafruit_azureiot/constants.py b/adafruit_azureiot/constants.py index 46880d9..5b1c73f 100644 --- a/adafruit_azureiot/constants.py +++ b/adafruit_azureiot/constants.py @@ -13,7 +13,7 @@ """ # The version of the IoT Central MQTT API this code is built against -IOTC_API_VERSION = "2016-11-14" +IOTC_API_VERSION = "2019-03-30" # The version of the Azure Device Provisioning Service this code is built against DPS_API_VERSION = "2018-11-01" diff --git a/adafruit_azureiot/device_registration.py b/adafruit_azureiot/device_registration.py index 4d0e6a5..4376ebe 100644 --- a/adafruit_azureiot/device_registration.py +++ b/adafruit_azureiot/device_registration.py @@ -59,10 +59,11 @@ def _parse_http_status(status_code: int, status_reason: str) -> None: ) def __init__( - self, socket, id_scope: str, device_id: str, key: str, logger: Logger = None + self, socket, iface, id_scope: str, device_id: str, key: str, logger: Logger = None ): """Creates an instance of the device registration service :param socket: The network socket + :param iface: The network interface :param str id_scope: The ID scope of the device to register :param str device_id: The device ID of the device to register :param str key: The primary or secondary key of the device to register @@ -73,7 +74,8 @@ def __init__( self._key = key self._logger = logger if logger is not None else logging.getLogger("log") - requests.set_socket(socket) + socket.set_interface(iface) + requests.set_socket(socket, iface) def _loop_assign(self, operation_id, headers) -> str: uri = "https://%s/%s/registrations/%s/operations/%s?api-version=%s" % ( diff --git a/adafruit_azureiot/iot_mqtt.py b/adafruit_azureiot/iot_mqtt.py index b7c43e6..80376bd 100644 --- a/adafruit_azureiot/iot_mqtt.py +++ b/adafruit_azureiot/iot_mqtt.py @@ -118,10 +118,10 @@ def _create_mqtt_client(self) -> None: port=8883, keep_alive=120, is_ssl=True, - client_id=self._device_id, - log=True, + client_id=self._device_id ) + self._mqtts.logger = self._logger self._mqtts.logger.setLevel(self._logger.getEffectiveLevel()) # set actions to take throughout connection lifecycle diff --git a/adafruit_azureiot/iotcentral_device.py b/adafruit_azureiot/iotcentral_device.py index 572a4f0..9fd8a83 100644 --- a/adafruit_azureiot/iotcentral_device.py +++ b/adafruit_azureiot/iotcentral_device.py @@ -136,7 +136,12 @@ def connect(self) -> None: :raises RuntimeError: if the internet connection is not responding or is unable to connect """ self._device_registration = DeviceRegistration( - self._socket, self._id_scope, self._device_id, self._key, self._logger + self._socket, + self._iface, + self._id_scope, + self._device_id, + self._key, + self._logger ) token_expiry = int(time.time() + self._token_expires) @@ -152,6 +157,10 @@ def connect(self) -> None: self._logger, ) + self._logger.debug("Hostname: " + hostname) + self._logger.debug("Device Id: " + self._device_id) + self._logger.debug("Shared Access Key: " + self._key) + self._mqtt.connect() self._mqtt.subscribe_to_twins() From 0465f8c3b938b184863e0deec1db49eb675e46d1 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 13 Apr 2021 09:40:32 -0700 Subject: [PATCH 02/13] Making it pass checks --- .gitignore | 1 + .pylintrc | 4 ++-- adafruit_azureiot/device_registration.py | 8 +++++++- adafruit_azureiot/iot_mqtt.py | 2 +- adafruit_azureiot/iotcentral_device.py | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 5a26e76..17a41ef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ bundles dist **/*.egg-info .vscode/settings.json +.venv \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 628123d..113d27a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -55,7 +55,7 @@ confidence= # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" # disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call -disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation,similarities # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -398,7 +398,7 @@ valid-metaclass-classmethod-first-arg=mcs [DESIGN] # Maximum number of arguments for function / method -max-args=6 +max-args=7 # Maximum number of attributes for a class (see R0902). # max-attributes=7 diff --git a/adafruit_azureiot/device_registration.py b/adafruit_azureiot/device_registration.py index 4376ebe..0ac1fb8 100644 --- a/adafruit_azureiot/device_registration.py +++ b/adafruit_azureiot/device_registration.py @@ -59,7 +59,13 @@ def _parse_http_status(status_code: int, status_reason: str) -> None: ) def __init__( - self, socket, iface, id_scope: str, device_id: str, key: str, logger: Logger = None + self, + socket, + iface, + id_scope: str, + device_id: str, + key: str, + logger: Logger = None, ): """Creates an instance of the device registration service :param socket: The network socket diff --git a/adafruit_azureiot/iot_mqtt.py b/adafruit_azureiot/iot_mqtt.py index 80376bd..1351338 100644 --- a/adafruit_azureiot/iot_mqtt.py +++ b/adafruit_azureiot/iot_mqtt.py @@ -118,7 +118,7 @@ def _create_mqtt_client(self) -> None: port=8883, keep_alive=120, is_ssl=True, - client_id=self._device_id + client_id=self._device_id, ) self._mqtts.logger = self._logger diff --git a/adafruit_azureiot/iotcentral_device.py b/adafruit_azureiot/iotcentral_device.py index 9fd8a83..72b5c48 100644 --- a/adafruit_azureiot/iotcentral_device.py +++ b/adafruit_azureiot/iotcentral_device.py @@ -141,7 +141,7 @@ def connect(self) -> None: self._id_scope, self._device_id, self._key, - self._logger + self._logger, ) token_expiry = int(time.time() + self._token_expires) From fb3d4d3f9ee6a05f59f61a4c0a3756f0dca65f35 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 13 Apr 2021 09:47:16 -0700 Subject: [PATCH 03/13] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 17a41ef..28342c8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ bundles dist **/*.egg-info .vscode/settings.json -.venv \ No newline at end of file +.venv From 12c1f04b9cd611ba70eefda162cac4d5d1d48b77 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 13 Apr 2021 14:06:06 -0700 Subject: [PATCH 04/13] Fixing logging --- adafruit_azureiot/iot_mqtt.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/adafruit_azureiot/iot_mqtt.py b/adafruit_azureiot/iot_mqtt.py index 1351338..2a3fd7d 100644 --- a/adafruit_azureiot/iot_mqtt.py +++ b/adafruit_azureiot/iot_mqtt.py @@ -111,6 +111,8 @@ def _gen_sas_token(self) -> str: def _create_mqtt_client(self) -> None: minimqtt.set_socket(self._socket, self._iface) + self._logger.debug(str.replace(f'- iot_mqtt :: _on_connect :: username = {self._username}, password = {self._passwd}', '%', '%%')) + self._mqtts = MQTT( broker=self._hostname, username=self._username, @@ -121,12 +123,10 @@ def _create_mqtt_client(self) -> None: client_id=self._device_id, ) - self._mqtts.logger = self._logger - self._mqtts.logger.setLevel(self._logger.getEffectiveLevel()) + self._mqtts.enable_logger(logging, self._logger.getEffectiveLevel()) # set actions to take throughout connection lifecycle self._mqtts.on_connect = self._on_connect - self._mqtts.on_log = self._on_log self._mqtts.on_publish = self._on_publish self._mqtts.on_disconnect = self._on_disconnect @@ -146,12 +146,6 @@ def _on_connect(self, client, userdata, _, rc) -> None: self._auth_response_received = True self._callback.connection_status_change(True) - # pylint: disable=C0103, W0613 - def _on_log(self, client, userdata, level, buf) -> None: - self._logger.info("mqtt-log : " + buf) - if level <= 8: - self._logger.error("mqtt-log : " + buf) - def _on_disconnect(self, client, userdata, rc) -> None: self._logger.info("- iot_mqtt :: _on_disconnect :: rc = " + str(rc)) self._auth_response_received = True @@ -348,7 +342,7 @@ def __init__( self._hostname = hostname self._key = key self._token_expires = token_expires - self._username = "{}/{}/api-version={}".format( + self._username = "{}/{}/?api-version={}".format( self._hostname, device_id, constants.IOTC_API_VERSION ) self._passwd = self._gen_sas_token() From 4566553e7e2f11c68ba6ad73beb6489c3fc96410 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 13 Apr 2021 14:07:45 -0700 Subject: [PATCH 05/13] Update constants.py --- adafruit_azureiot/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_azureiot/constants.py b/adafruit_azureiot/constants.py index 5b1c73f..619875d 100644 --- a/adafruit_azureiot/constants.py +++ b/adafruit_azureiot/constants.py @@ -13,10 +13,10 @@ """ # The version of the IoT Central MQTT API this code is built against -IOTC_API_VERSION = "2019-03-30" +IOTC_API_VERSION = "2019-10-01" # The version of the Azure Device Provisioning Service this code is built against -DPS_API_VERSION = "2018-11-01" +DPS_API_VERSION = "2019-03-31" # The Azure Device Provisioning service endpoint that this library uses to provision IoT Central devices DPS_END_POINT = "global.azure-devices-provisioning.net" From 3b5e98b798567cdf2ba1f83a9173d0a6738154ea Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 13 Apr 2021 14:13:30 -0700 Subject: [PATCH 06/13] Update iot_mqtt.py --- adafruit_azureiot/iot_mqtt.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/adafruit_azureiot/iot_mqtt.py b/adafruit_azureiot/iot_mqtt.py index 2a3fd7d..3168a30 100644 --- a/adafruit_azureiot/iot_mqtt.py +++ b/adafruit_azureiot/iot_mqtt.py @@ -111,7 +111,13 @@ def _gen_sas_token(self) -> str: def _create_mqtt_client(self) -> None: minimqtt.set_socket(self._socket, self._iface) - self._logger.debug(str.replace(f'- iot_mqtt :: _on_connect :: username = {self._username}, password = {self._passwd}', '%', '%%')) + self._logger.debug( + str.replace( + f"- iot_mqtt :: _on_connect :: username = {self._username}, password = {self._passwd}", + "%", + "%%", + ) + ) self._mqtts = MQTT( broker=self._hostname, From 942d0d1b32e1e11f8d23922b55a8c53a0bc2d997 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Wed, 14 Apr 2021 10:54:51 -0700 Subject: [PATCH 07/13] Fixing linter settings --- .pylintrc | 2 +- adafruit_azureiot/device_registration.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 113d27a..8332e94 100644 --- a/.pylintrc +++ b/.pylintrc @@ -398,7 +398,7 @@ valid-metaclass-classmethod-first-arg=mcs [DESIGN] # Maximum number of arguments for function / method -max-args=7 +max-args=6 # Maximum number of attributes for a class (see R0902). # max-attributes=7 diff --git a/adafruit_azureiot/device_registration.py b/adafruit_azureiot/device_registration.py index 0ac1fb8..61e8734 100644 --- a/adafruit_azureiot/device_registration.py +++ b/adafruit_azureiot/device_registration.py @@ -58,6 +58,7 @@ def _parse_http_status(status_code: int, status_reason: str) -> None: "Error {0}: {1}".format(status_code, status_reason) ) + # pylint: disable=R0913 def __init__( self, socket, From 6f71151ac82db8bf88dbd9e2082e88c30026f4ec Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Wed, 14 Apr 2021 10:54:57 -0700 Subject: [PATCH 08/13] Updating tracking codes --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 1ac29a3..f980214 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ Adafruit_CircuitPython_AzureIoT :target: https://github.com/adafruit/Adafruit_CircuitPython_AzureIoT/actions/ :alt: Build Status -A CircuitPython device library for `Microsoft Azure IoT Services `_ from a CircuitPython device. This library only supports key-base authentication, it currently doesn't support X.509 certificates. +A CircuitPython device library for `Microsoft Azure IoT Services `_ from a CircuitPython device. This library only supports key-base authentication, it currently doesn't support X.509 certificates. Installing from PyPI ===================== @@ -55,7 +55,7 @@ This is easily achieved by downloading Usage Example ============= -This library supports both `Azure IoT Hub `_ and `Azure IoT Central `__. +This library supports both `Azure IoT Hub `_ and `Azure IoT Central `__. To create an Azure IoT Hub instance or an Azure IoT Central app, you will need an Azure subscription. If you don't have an Azure subscription, you can sign up for free: @@ -169,9 +169,9 @@ Azure IoT Central To use Azure IoT Central, you will need to create an Azure IoT Central app, create a device template and register a device against the template. -- Head to `Azure IoT Central `__ -- Follow the instructions in the `Microsoft Docs `__ to create an application. Every tier is free for up to 2 devices. -- Follow the instructions in the `Microsoft Docs `__ to create a device template. +- Head to `Azure IoT Central `__ +- Follow the instructions in the `Microsoft Docs `__ to create an application. Every tier is free for up to 2 devices. +- Follow the instructions in the `Microsoft Docs `__ to create a device template. - Create a device based off the template, and select **Connect** to get the device connection details. Store the ID Scope, Device ID and either the Primary or secondary Key in your ``secrets.py`` file. .. image:: iot-central-connect-button.png @@ -254,8 +254,8 @@ Learning more about Azure IoT services If you want to learn more about setting up or using Azure IoT Services, check out the following resources: -- `Azure IoT documentation on Microsoft Docs `_ -- `IoT learning paths and modules on Microsoft Learn `_ - Free, online, self-guided hands on learning with Azure IoT services +- `Azure IoT documentation on Microsoft Docs `_ +- `IoT learning paths and modules on Microsoft Learn `_ - Free, online, self-guided hands on learning with Azure IoT services Contributing ============ From adaa63504f01ef399eeacbda3c5399354bb85646 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Wed, 14 Apr 2021 17:18:48 -0700 Subject: [PATCH 09/13] Moving from HTTPS to MQTT for device registration --- adafruit_azureiot/device_registration.py | 260 +++++++++-------------- 1 file changed, 100 insertions(+), 160 deletions(-) diff --git a/adafruit_azureiot/device_registration.py b/adafruit_azureiot/device_registration.py index 61e8734..5f2c877 100644 --- a/adafruit_azureiot/device_registration.py +++ b/adafruit_azureiot/device_registration.py @@ -13,19 +13,16 @@ * Author(s): Jim Bennett, Elena Horton """ -import gc import json import time -import adafruit_requests as requests import adafruit_logging as logging from adafruit_logging import Logger +import adafruit_minimqtt.adafruit_minimqtt as minimqtt +from adafruit_minimqtt.adafruit_minimqtt import MQTT from . import constants from .quote import quote from .keys import compute_derived_symmetric_key -# Azure HTTP error status codes -AZURE_HTTP_ERROR_CODES = [400, 401, 404, 403, 412, 429, 500] - class DeviceRegistrationError(Exception): """ @@ -43,21 +40,6 @@ class DeviceRegistration: to IoT Central over MQTT """ - _loop_interval = 2 - - @staticmethod - def _parse_http_status(status_code: int, status_reason: str) -> None: - """Parses status code, throws error based on Azure IoT Common Error Codes. - :param int status_code: HTTP status code. - :param str status_reason: Description of HTTP status. - :raises DeviceRegistrationError: if the status code is an error code - """ - for error in AZURE_HTTP_ERROR_CODES: - if error == status_code: - raise DeviceRegistrationError( - "Error {0}: {1}".format(status_code, status_reason) - ) - # pylint: disable=R0913 def __init__( self, @@ -70,7 +52,6 @@ def __init__( ): """Creates an instance of the device registration service :param socket: The network socket - :param iface: The network interface :param str id_scope: The ID scope of the device to register :param str device_id: The device ID of the device to register :param str key: The primary or secondary key of the device to register @@ -81,106 +62,95 @@ def __init__( self._key = key self._logger = logger if logger is not None else logging.getLogger("log") - socket.set_interface(iface) - requests.set_socket(socket, iface) + self._mqtt_connected = False + self._mqtt = None + self._auth_response_received = False + self._operation_id = None + self._hostname = None - def _loop_assign(self, operation_id, headers) -> str: - uri = "https://%s/%s/registrations/%s/operations/%s?api-version=%s" % ( - constants.DPS_END_POINT, - self._id_scope, - self._device_id, - operation_id, - constants.DPS_API_VERSION, + self._socket = socket + self._iface = iface + + # pylint: disable=W0613 + # pylint: disable=C0103 + def _on_connect(self, client, userdata, _, rc) -> None: + self._logger.info( + f"- device_registration :: _on_connect :: rc = {str(rc)}, userdata = {str(userdata)}" ) - self._logger.info("- iotc :: _loop_assign :: " + uri) + if rc == 0: + self._mqtt_connected = True + self._auth_response_received = True + + # pylint: disable=W0613 + def _handle_dps_update(self, client, topic: str, msg: str) -> None: + self._logger.info(f"Received registration results on topic {topic} - {msg}") + message = json.loads(msg) + + if topic.startswith("$dps/registrations/res/202"): + self._operation_id = message["operationId"] + elif topic.startswith("$dps/registrations/res/200"): + self._hostname = message["registrationState"]["assignedHub"] - response = self._run_get_request_with_retry(uri, headers) + def _connect_to_mqtt(self) -> None: + self._mqtt.on_connect = self._on_connect - try: - data = response.json() - except ValueError as error: - err = "ERROR: " + str(error) + " => " + str(response) - self._logger.error(err) - raise DeviceRegistrationError(err) from error + self._mqtt.connect() - loop_try = 0 + self._logger.info( + " - device_registration :: connect :: created mqtt client. connecting.." + ) + while not self._auth_response_received: + self._mqtt.loop() - if data is not None and "status" in data: - if data["status"] == "assigning": - time.sleep(self._loop_interval) - if loop_try < 20: - loop_try = loop_try + 1 - return self._loop_assign(operation_id, headers) + self._logger.info( + f" - device_registration :: connect :: on_connect must be fired. Connected ? {str(self._mqtt_connected)}" + ) - err = "ERROR: Unable to provision the device." - self._logger.error(err) - raise DeviceRegistrationError(err) + if not self._mqtt_connected: + raise DeviceRegistrationError("Cannot connect to MQTT") - if data["status"] == "assigned": - state = data["registrationState"] - return state["assignedHub"] - else: - data = str(data) + def _start_registration(self) -> None: + self._mqtt.add_topic_callback( + "$dps/registrations/res/#", self._handle_dps_update + ) + self._mqtt.subscribe("$dps/registrations/res/#") - err = "DPS L => " + str(data) - self._logger.error(err) - raise DeviceRegistrationError(err) + message = json.dumps({"registrationId": self._device_id}) + + self._mqtt.publish( + f"$dps/registrations/PUT/iotdps-register/?$rid={self._device_id}", message + ) - def _run_put_request_with_retry(self, url, body, headers): retry = 0 - response = None - - while True: - gc.collect() - try: - self._logger.debug("Trying to send...") - response = requests.put(url, json=body, headers=headers) - self._logger.debug("Sent!") - break - except RuntimeError as runtime_error: - self._logger.info( - "Could not send data, retrying after 0.5 seconds: " - + str(runtime_error) - ) - retry = retry + 1 - - if retry >= 10: - self._logger.error("Failed to send data") - raise - - time.sleep(0.5) - continue - - gc.collect() - return response - - def _run_get_request_with_retry(self, url, headers): + + while self._operation_id is None and retry < 10: + time.sleep(1) + retry = retry + 1 + self._mqtt.loop() + + if self._operation_id is None: + raise DeviceRegistrationError( + "Cannot register device - no response from broker for registration result" + ) + + def _wait_for_operation(self) -> None: + message = json.dumps({"operationId": self._operation_id}) + self._mqtt.publish( + f"$dps/registrations/GET/iotdps-get-operationstatus/?$rid={self._device_id}&operationId={self._operation_id}", + message, + ) + retry = 0 - response = None - - while True: - gc.collect() - try: - self._logger.debug("Trying to send...") - response = requests.get(url, headers=headers) - self._logger.debug("Sent!") - break - except RuntimeError as runtime_error: - self._logger.info( - "Could not send data, retrying after 0.5 seconds: " - + str(runtime_error) - ) - retry = retry + 1 - - if retry >= 10: - self._logger.error("Failed to send data") - raise - - time.sleep(0.5) - continue - - gc.collect() - return response + + while self._hostname is None and retry < 10: + time.sleep(1) + retry = retry + 1 + self._mqtt.loop() + + if self._hostname is None: + raise DeviceRegistrationError( + "Cannot register device - no response from broker for operation status" + ) def register_device(self, expiry: int) -> str: """ @@ -192,65 +162,35 @@ def register_device(self, expiry: int) -> str: :raises DeviceRegistrationError: if the device cannot be registered successfully :raises RuntimeError: if the internet connection is not responding or is unable to connect """ + + username = f"{self._id_scope}/registrations/{self._device_id}/api-version={constants.DPS_API_VERSION}" + # pylint: disable=C0103 sr = self._id_scope + "%2Fregistrations%2F" + self._device_id sig_no_encode = compute_derived_symmetric_key( self._key, sr + "\n" + str(expiry) ) sig_encoded = quote(sig_no_encode, "~()*!.'") - auth_string = ( - "SharedAccessSignature sr=" - + sr - + "&sig=" - + sig_encoded - + "&se=" - + str(expiry) - + "&skn=registration" + auth_string = f"SharedAccessSignature sr={sr}&sig={sig_encoded}&se={str(expiry)}&skn=registration" + + minimqtt.set_socket(self._socket, self._iface) + + self._mqtt = MQTT( + broker=constants.DPS_END_POINT, + username=username, + password=auth_string, + port=8883, + keep_alive=120, + is_ssl=True, + client_id=self._device_id, ) - headers = { - "content-type": "application/json; charset=utf-8", - "user-agent": "iot-central-client/1.0", - "Accept": "*/*", - } - - if auth_string is not None: - headers["authorization"] = auth_string - - body = {"registrationId": self._device_id} + self._mqtt.enable_logger(logging, self._logger.getEffectiveLevel()) - uri = "https://%s/%s/registrations/%s/register?api-version=%s" % ( - constants.DPS_END_POINT, - self._id_scope, - self._device_id, - constants.DPS_API_VERSION, - ) - - self._logger.info("Connecting...") - self._logger.info("URL: " + uri) - self._logger.info("body: " + json.dumps(body)) - - response = self._run_put_request_with_retry(uri, body, headers) - - data = None - try: - data = response.json() - except ValueError as error: - err = ( - "ERROR: non JSON is received from " - + constants.DPS_END_POINT - + " => " - + str(response) - + " .. message : " - + str(error) - ) - self._logger.error(err) - raise DeviceRegistrationError(err) from error + self._connect_to_mqtt() + self._start_registration() + self._wait_for_operation() - if "errorCode" in data: - err = "DPS => " + str(data) - self._logger.error(err) - raise DeviceRegistrationError(err) + self._mqtt.disconnect() - time.sleep(1) - return self._loop_assign(data["operationId"], headers) + return str(self._hostname) From 84e2a26d02475455bd815b8869fe3b1dd946451e Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Wed, 14 Apr 2021 17:29:53 -0700 Subject: [PATCH 10/13] Adding correct retry wait --- adafruit_azureiot/device_registration.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/adafruit_azureiot/device_registration.py b/adafruit_azureiot/device_registration.py index 5f2c877..3394f81 100644 --- a/adafruit_azureiot/device_registration.py +++ b/adafruit_azureiot/device_registration.py @@ -87,6 +87,13 @@ def _handle_dps_update(self, client, topic: str, msg: str) -> None: message = json.loads(msg) if topic.startswith("$dps/registrations/res/202"): + # Get the retry after and wait for that before responding + parts = str.split(topic, "retry-after=") + waittime = int(parts[1]) + + self._logger.debug(f"Retrying after {waittime}s") + + time.sleep(waittime) self._operation_id = message["operationId"] elif topic.startswith("$dps/registrations/res/200"): self._hostname = message["registrationState"]["assignedHub"] From 21a96c475ebf9b42581fc1bb8d66adb4eeddf061 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Fri, 16 Apr 2021 08:57:28 -0700 Subject: [PATCH 11/13] PR feedback --- adafruit_azureiot/device_registration.py | 3 +-- adafruit_azureiot/iot_mqtt.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/adafruit_azureiot/device_registration.py b/adafruit_azureiot/device_registration.py index 3394f81..2353d81 100644 --- a/adafruit_azureiot/device_registration.py +++ b/adafruit_azureiot/device_registration.py @@ -18,7 +18,6 @@ import adafruit_logging as logging from adafruit_logging import Logger import adafruit_minimqtt.adafruit_minimqtt as minimqtt -from adafruit_minimqtt.adafruit_minimqtt import MQTT from . import constants from .quote import quote from .keys import compute_derived_symmetric_key @@ -182,7 +181,7 @@ def register_device(self, expiry: int) -> str: minimqtt.set_socket(self._socket, self._iface) - self._mqtt = MQTT( + self._mqtt = minimqtt.MQTT( broker=constants.DPS_END_POINT, username=username, password=auth_string, diff --git a/adafruit_azureiot/iot_mqtt.py b/adafruit_azureiot/iot_mqtt.py index 3168a30..c649448 100644 --- a/adafruit_azureiot/iot_mqtt.py +++ b/adafruit_azureiot/iot_mqtt.py @@ -16,7 +16,6 @@ import json import time import adafruit_minimqtt.adafruit_minimqtt as minimqtt -from adafruit_minimqtt.adafruit_minimqtt import MQTT import adafruit_logging as logging from .iot_error import IoTError from .keys import compute_derived_symmetric_key @@ -119,7 +118,7 @@ def _create_mqtt_client(self) -> None: ) ) - self._mqtts = MQTT( + self._mqtts = minimqtt.MQTT( broker=self._hostname, username=self._username, password=self._passwd, From 73e223eb2a7d319be0ff388f9d33f74bdd72c220 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Fri, 16 Apr 2021 09:10:34 -0700 Subject: [PATCH 12/13] PR feedback --- adafruit_azureiot/device_registration.py | 8 +++----- adafruit_azureiot/iot_mqtt.py | 11 ++--------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/adafruit_azureiot/device_registration.py b/adafruit_azureiot/device_registration.py index 2353d81..9071981 100644 --- a/adafruit_azureiot/device_registration.py +++ b/adafruit_azureiot/device_registration.py @@ -61,7 +61,6 @@ def __init__( self._key = key self._logger = logger if logger is not None else logging.getLogger("log") - self._mqtt_connected = False self._mqtt = None self._auth_response_received = False self._operation_id = None @@ -76,8 +75,7 @@ def _on_connect(self, client, userdata, _, rc) -> None: self._logger.info( f"- device_registration :: _on_connect :: rc = {str(rc)}, userdata = {str(userdata)}" ) - if rc == 0: - self._mqtt_connected = True + self._auth_response_received = True # pylint: disable=W0613 @@ -109,10 +107,10 @@ def _connect_to_mqtt(self) -> None: self._mqtt.loop() self._logger.info( - f" - device_registration :: connect :: on_connect must be fired. Connected ? {str(self._mqtt_connected)}" + f" - device_registration :: connect :: on_connect must be fired. Connected ? {str(self._mqtt.is_connected())}" ) - if not self._mqtt_connected: + if not self._mqtt.is_connected(): raise DeviceRegistrationError("Cannot connect to MQTT") def _start_registration(self) -> None: diff --git a/adafruit_azureiot/iot_mqtt.py b/adafruit_azureiot/iot_mqtt.py index c649448..57646eb 100644 --- a/adafruit_azureiot/iot_mqtt.py +++ b/adafruit_azureiot/iot_mqtt.py @@ -146,8 +146,7 @@ def _on_connect(self, client, userdata, _, rc) -> None: + ", userdata = " + str(userdata) ) - if rc == 0: - self._mqtt_connected = True + self._auth_response_received = True self._callback.connection_status_change(True) @@ -159,9 +158,6 @@ def _on_disconnect(self, client, userdata, rc) -> None: self._logger.error("on(disconnect) : Not authorized") self.disconnect() - if rc == 1: - self._mqtt_connected = False - if rc != 5: self._callback.connection_status_change(False) @@ -340,7 +336,6 @@ def __init__( self._callback = callback self._socket = socket self._iface = iface - self._mqtt_connected = False self._auth_response_received = False self._mqtts = None self._device_id = device_id @@ -397,7 +392,6 @@ def connect(self) -> bool: if not self.is_connected(): return False - self._mqtt_connected = True self._auth_response_received = True self._subscribe_to_core_topics() @@ -424,7 +418,6 @@ def disconnect(self) -> None: return self._logger.info("- iot_mqtt :: disconnect :: ") - self._mqtt_connected = False self._mqtts.disconnect() def reconnect(self) -> None: @@ -438,7 +431,7 @@ def is_connected(self) -> bool: :returns: True if there is an open connection, False if not :rtype: bool """ - return self._mqtt_connected + return self._mqtts.is_connected() def loop(self) -> None: """Listens for MQTT messages""" From a4d3b59d5ac18e8ff3e88f3a99e36606f1b20bd8 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Fri, 16 Apr 2021 09:16:02 -0700 Subject: [PATCH 13/13] Black updates --- adafruit_azureiot/iot_mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_azureiot/iot_mqtt.py b/adafruit_azureiot/iot_mqtt.py index 57646eb..cdc7228 100644 --- a/adafruit_azureiot/iot_mqtt.py +++ b/adafruit_azureiot/iot_mqtt.py @@ -146,7 +146,7 @@ def _on_connect(self, client, userdata, _, rc) -> None: + ", userdata = " + str(userdata) ) - + self._auth_response_received = True self._callback.connection_status_change(True)