Skip to content

Commit 93d414e

Browse files
authored
Merge pull request #31 from jimbobbennett/master
Updating to latest requests and miniMQTT
2 parents c81e8f3 + a4d3b59 commit 93d414e

File tree

7 files changed

+146
-191
lines changed

7 files changed

+146
-191
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ bundles
1414
dist
1515
**/*.egg-info
1616
.vscode/settings.json
17+
.venv

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ confidence=
5555
# no Warning level messages displayed, use"--disable=all --enable=classes
5656
# --disable=W"
5757
# 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
58-
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
58+
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
5959

6060
# Enable the message, report, category or checker with the given id(s). You can
6161
# either give multiple identifier separated by comma (,) or put this option

README.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Adafruit_CircuitPython_AzureIoT
1313
:target: https://github.com/adafruit/Adafruit_CircuitPython_AzureIoT/actions/
1414
:alt: Build Status
1515

16-
A CircuitPython device library for `Microsoft Azure IoT Services <https://azure.microsoft.com/overview/iot/?WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`_ from a CircuitPython device. This library only supports key-base authentication, it currently doesn't support X.509 certificates.
16+
A CircuitPython device library for `Microsoft Azure IoT Services <https://azure.microsoft.com/overview/iot/?WT.mc_id=academic-3168-jabenn>`_ from a CircuitPython device. This library only supports key-base authentication, it currently doesn't support X.509 certificates.
1717

1818
Installing from PyPI
1919
=====================
@@ -55,7 +55,7 @@ This is easily achieved by downloading
5555
Usage Example
5656
=============
5757

58-
This library supports both `Azure IoT Hub <https://azure.microsoft.com/services/iot-hub/?WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`_ and `Azure IoT Central <https://azure.microsoft.com/services/iot-central/?WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`__.
58+
This library supports both `Azure IoT Hub <https://azure.microsoft.com/services/iot-hub/?WT.mc_id=academic-3168-jabenn>`_ and `Azure IoT Central <https://azure.microsoft.com/services/iot-central/?WT.mc_id=academic-3168-jabenn>`__.
5959

6060
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:
6161

@@ -169,9 +169,9 @@ Azure IoT Central
169169

170170
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.
171171

172-
- Head to `Azure IoT Central <https://apps.azureiotcentral.com/?WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`__
173-
- Follow the instructions in the `Microsoft Docs <https://docs.microsoft.com/azure/iot-central/core/quick-deploy-iot-central?WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`__ to create an application. Every tier is free for up to 2 devices.
174-
- Follow the instructions in the `Microsoft Docs <https://docs.microsoft.com/azure/iot-central/core/quick-create-simulated-device?WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`__ to create a device template.
172+
- Head to `Azure IoT Central <https://apps.azureiotcentral.com/?WT.mc_id=academic-3168-jabenn>`__
173+
- Follow the instructions in the `Microsoft Docs <https://docs.microsoft.com/azure/iot-central/core/quick-deploy-iot-central?WT.mc_id=academic-3168-jabenn>`__ to create an application. Every tier is free for up to 2 devices.
174+
- Follow the instructions in the `Microsoft Docs <https://docs.microsoft.com/azure/iot-central/core/quick-create-simulated-device?WT.mc_id=academic-3168-jabenn>`__ to create a device template.
175175
- 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.
176176

177177
.. image:: iot-central-connect-button.png
@@ -254,8 +254,8 @@ Learning more about Azure IoT services
254254

255255
If you want to learn more about setting up or using Azure IoT Services, check out the following resources:
256256

257-
- `Azure IoT documentation on Microsoft Docs <https://docs.microsoft.com/azure/iot-fundamentals/?WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`_
258-
- `IoT learning paths and modules on Microsoft Learn <https://docs.microsoft.com/learn/browse/?term=iot&WT.mc_id=AdafruitCircuitPythonAzureIoT-github-jabenn>`_ - Free, online, self-guided hands on learning with Azure IoT services
257+
- `Azure IoT documentation on Microsoft Docs <https://docs.microsoft.com/azure/iot-fundamentals/?WT.mc_id=academic-3168-jabenn>`_
258+
- `IoT learning paths and modules on Microsoft Learn <https://docs.microsoft.com/learn/browse/?term=iot&WT.mc_id=academic-3168-jabenn>`_ - Free, online, self-guided hands on learning with Azure IoT services
259259

260260
Contributing
261261
============

adafruit_azureiot/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"""
1414

1515
# The version of the IoT Central MQTT API this code is built against
16-
IOTC_API_VERSION = "2016-11-14"
16+
IOTC_API_VERSION = "2019-10-01"
1717

1818
# The version of the Azure Device Provisioning Service this code is built against
19-
DPS_API_VERSION = "2018-11-01"
19+
DPS_API_VERSION = "2019-03-31"
2020

2121
# The Azure Device Provisioning service endpoint that this library uses to provision IoT Central devices
2222
DPS_END_POINT = "global.azure-devices-provisioning.net"

adafruit_azureiot/device_registration.py

Lines changed: 112 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,15 @@
1313
* Author(s): Jim Bennett, Elena Horton
1414
"""
1515

16-
import gc
1716
import json
1817
import time
19-
import adafruit_requests as requests
2018
import adafruit_logging as logging
2119
from adafruit_logging import Logger
20+
import adafruit_minimqtt.adafruit_minimqtt as minimqtt
2221
from . import constants
2322
from .quote import quote
2423
from .keys import compute_derived_symmetric_key
2524

26-
# Azure HTTP error status codes
27-
AZURE_HTTP_ERROR_CODES = [400, 401, 404, 403, 412, 429, 500]
28-
2925

3026
class DeviceRegistrationError(Exception):
3127
"""
@@ -43,23 +39,15 @@ class DeviceRegistration:
4339
to IoT Central over MQTT
4440
"""
4541

46-
_loop_interval = 2
47-
48-
@staticmethod
49-
def _parse_http_status(status_code: int, status_reason: str) -> None:
50-
"""Parses status code, throws error based on Azure IoT Common Error Codes.
51-
:param int status_code: HTTP status code.
52-
:param str status_reason: Description of HTTP status.
53-
:raises DeviceRegistrationError: if the status code is an error code
54-
"""
55-
for error in AZURE_HTTP_ERROR_CODES:
56-
if error == status_code:
57-
raise DeviceRegistrationError(
58-
"Error {0}: {1}".format(status_code, status_reason)
59-
)
60-
42+
# pylint: disable=R0913
6143
def __init__(
62-
self, socket, id_scope: str, device_id: str, key: str, logger: Logger = None
44+
self,
45+
socket,
46+
iface,
47+
id_scope: str,
48+
device_id: str,
49+
key: str,
50+
logger: Logger = None,
6351
):
6452
"""Creates an instance of the device registration service
6553
:param socket: The network socket
@@ -73,105 +61,100 @@ def __init__(
7361
self._key = key
7462
self._logger = logger if logger is not None else logging.getLogger("log")
7563

76-
requests.set_socket(socket)
64+
self._mqtt = None
65+
self._auth_response_received = False
66+
self._operation_id = None
67+
self._hostname = None
7768

78-
def _loop_assign(self, operation_id, headers) -> str:
79-
uri = "https://%s/%s/registrations/%s/operations/%s?api-version=%s" % (
80-
constants.DPS_END_POINT,
81-
self._id_scope,
82-
self._device_id,
83-
operation_id,
84-
constants.DPS_API_VERSION,
69+
self._socket = socket
70+
self._iface = iface
71+
72+
# pylint: disable=W0613
73+
# pylint: disable=C0103
74+
def _on_connect(self, client, userdata, _, rc) -> None:
75+
self._logger.info(
76+
f"- device_registration :: _on_connect :: rc = {str(rc)}, userdata = {str(userdata)}"
8577
)
86-
self._logger.info("- iotc :: _loop_assign :: " + uri)
8778

88-
response = self._run_get_request_with_retry(uri, headers)
79+
self._auth_response_received = True
80+
81+
# pylint: disable=W0613
82+
def _handle_dps_update(self, client, topic: str, msg: str) -> None:
83+
self._logger.info(f"Received registration results on topic {topic} - {msg}")
84+
message = json.loads(msg)
85+
86+
if topic.startswith("$dps/registrations/res/202"):
87+
# Get the retry after and wait for that before responding
88+
parts = str.split(topic, "retry-after=")
89+
waittime = int(parts[1])
90+
91+
self._logger.debug(f"Retrying after {waittime}s")
8992

90-
try:
91-
data = response.json()
92-
except ValueError as error:
93-
err = "ERROR: " + str(error) + " => " + str(response)
94-
self._logger.error(err)
95-
raise DeviceRegistrationError(err) from error
93+
time.sleep(waittime)
94+
self._operation_id = message["operationId"]
95+
elif topic.startswith("$dps/registrations/res/200"):
96+
self._hostname = message["registrationState"]["assignedHub"]
9697

97-
loop_try = 0
98+
def _connect_to_mqtt(self) -> None:
99+
self._mqtt.on_connect = self._on_connect
98100

99-
if data is not None and "status" in data:
100-
if data["status"] == "assigning":
101-
time.sleep(self._loop_interval)
102-
if loop_try < 20:
103-
loop_try = loop_try + 1
104-
return self._loop_assign(operation_id, headers)
101+
self._mqtt.connect()
105102

106-
err = "ERROR: Unable to provision the device."
107-
self._logger.error(err)
108-
raise DeviceRegistrationError(err)
103+
self._logger.info(
104+
" - device_registration :: connect :: created mqtt client. connecting.."
105+
)
106+
while not self._auth_response_received:
107+
self._mqtt.loop()
108+
109+
self._logger.info(
110+
f" - device_registration :: connect :: on_connect must be fired. Connected ? {str(self._mqtt.is_connected())}"
111+
)
112+
113+
if not self._mqtt.is_connected():
114+
raise DeviceRegistrationError("Cannot connect to MQTT")
109115

110-
if data["status"] == "assigned":
111-
state = data["registrationState"]
112-
return state["assignedHub"]
113-
else:
114-
data = str(data)
116+
def _start_registration(self) -> None:
117+
self._mqtt.add_topic_callback(
118+
"$dps/registrations/res/#", self._handle_dps_update
119+
)
120+
self._mqtt.subscribe("$dps/registrations/res/#")
121+
122+
message = json.dumps({"registrationId": self._device_id})
115123

116-
err = "DPS L => " + str(data)
117-
self._logger.error(err)
118-
raise DeviceRegistrationError(err)
124+
self._mqtt.publish(
125+
f"$dps/registrations/PUT/iotdps-register/?$rid={self._device_id}", message
126+
)
119127

120-
def _run_put_request_with_retry(self, url, body, headers):
121128
retry = 0
122-
response = None
123-
124-
while True:
125-
gc.collect()
126-
try:
127-
self._logger.debug("Trying to send...")
128-
response = requests.put(url, json=body, headers=headers)
129-
self._logger.debug("Sent!")
130-
break
131-
except RuntimeError as runtime_error:
132-
self._logger.info(
133-
"Could not send data, retrying after 0.5 seconds: "
134-
+ str(runtime_error)
135-
)
136-
retry = retry + 1
137-
138-
if retry >= 10:
139-
self._logger.error("Failed to send data")
140-
raise
141-
142-
time.sleep(0.5)
143-
continue
144-
145-
gc.collect()
146-
return response
147-
148-
def _run_get_request_with_retry(self, url, headers):
129+
130+
while self._operation_id is None and retry < 10:
131+
time.sleep(1)
132+
retry = retry + 1
133+
self._mqtt.loop()
134+
135+
if self._operation_id is None:
136+
raise DeviceRegistrationError(
137+
"Cannot register device - no response from broker for registration result"
138+
)
139+
140+
def _wait_for_operation(self) -> None:
141+
message = json.dumps({"operationId": self._operation_id})
142+
self._mqtt.publish(
143+
f"$dps/registrations/GET/iotdps-get-operationstatus/?$rid={self._device_id}&operationId={self._operation_id}",
144+
message,
145+
)
146+
149147
retry = 0
150-
response = None
151-
152-
while True:
153-
gc.collect()
154-
try:
155-
self._logger.debug("Trying to send...")
156-
response = requests.get(url, headers=headers)
157-
self._logger.debug("Sent!")
158-
break
159-
except RuntimeError as runtime_error:
160-
self._logger.info(
161-
"Could not send data, retrying after 0.5 seconds: "
162-
+ str(runtime_error)
163-
)
164-
retry = retry + 1
165-
166-
if retry >= 10:
167-
self._logger.error("Failed to send data")
168-
raise
169-
170-
time.sleep(0.5)
171-
continue
172-
173-
gc.collect()
174-
return response
148+
149+
while self._hostname is None and retry < 10:
150+
time.sleep(1)
151+
retry = retry + 1
152+
self._mqtt.loop()
153+
154+
if self._hostname is None:
155+
raise DeviceRegistrationError(
156+
"Cannot register device - no response from broker for operation status"
157+
)
175158

176159
def register_device(self, expiry: int) -> str:
177160
"""
@@ -183,65 +166,35 @@ def register_device(self, expiry: int) -> str:
183166
:raises DeviceRegistrationError: if the device cannot be registered successfully
184167
:raises RuntimeError: if the internet connection is not responding or is unable to connect
185168
"""
169+
170+
username = f"{self._id_scope}/registrations/{self._device_id}/api-version={constants.DPS_API_VERSION}"
171+
186172
# pylint: disable=C0103
187173
sr = self._id_scope + "%2Fregistrations%2F" + self._device_id
188174
sig_no_encode = compute_derived_symmetric_key(
189175
self._key, sr + "\n" + str(expiry)
190176
)
191177
sig_encoded = quote(sig_no_encode, "~()*!.'")
192-
auth_string = (
193-
"SharedAccessSignature sr="
194-
+ sr
195-
+ "&sig="
196-
+ sig_encoded
197-
+ "&se="
198-
+ str(expiry)
199-
+ "&skn=registration"
178+
auth_string = f"SharedAccessSignature sr={sr}&sig={sig_encoded}&se={str(expiry)}&skn=registration"
179+
180+
minimqtt.set_socket(self._socket, self._iface)
181+
182+
self._mqtt = minimqtt.MQTT(
183+
broker=constants.DPS_END_POINT,
184+
username=username,
185+
password=auth_string,
186+
port=8883,
187+
keep_alive=120,
188+
is_ssl=True,
189+
client_id=self._device_id,
200190
)
201191

202-
headers = {
203-
"content-type": "application/json; charset=utf-8",
204-
"user-agent": "iot-central-client/1.0",
205-
"Accept": "*/*",
206-
}
207-
208-
if auth_string is not None:
209-
headers["authorization"] = auth_string
210-
211-
body = {"registrationId": self._device_id}
192+
self._mqtt.enable_logger(logging, self._logger.getEffectiveLevel())
212193

213-
uri = "https://%s/%s/registrations/%s/register?api-version=%s" % (
214-
constants.DPS_END_POINT,
215-
self._id_scope,
216-
self._device_id,
217-
constants.DPS_API_VERSION,
218-
)
219-
220-
self._logger.info("Connecting...")
221-
self._logger.info("URL: " + uri)
222-
self._logger.info("body: " + json.dumps(body))
223-
224-
response = self._run_put_request_with_retry(uri, body, headers)
225-
226-
data = None
227-
try:
228-
data = response.json()
229-
except ValueError as error:
230-
err = (
231-
"ERROR: non JSON is received from "
232-
+ constants.DPS_END_POINT
233-
+ " => "
234-
+ str(response)
235-
+ " .. message : "
236-
+ str(error)
237-
)
238-
self._logger.error(err)
239-
raise DeviceRegistrationError(err) from error
194+
self._connect_to_mqtt()
195+
self._start_registration()
196+
self._wait_for_operation()
240197

241-
if "errorCode" in data:
242-
err = "DPS => " + str(data)
243-
self._logger.error(err)
244-
raise DeviceRegistrationError(err)
198+
self._mqtt.disconnect()
245199

246-
time.sleep(1)
247-
return self._loop_assign(data["operationId"], headers)
200+
return str(self._hostname)

0 commit comments

Comments
 (0)