This commit is contained in:
ludruda 2020-06-01 16:58:43 +02:00
Родитель e5ee8f0f67
Коммит 505ea11f97
19 изменённых файлов: 524 добавлений и 307 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -11,6 +11,6 @@ exp.py
.vscode/
# publish
azure_iotcentral_device_client.egg-info
*.egg-info
build
dist

Просмотреть файл

@ -23,13 +23,20 @@ These clients are available with an asynchronous API, as well as a blocking sync
## Samples
Check out the [sample repository](samples) for example code showing how the SDK can be used in the various scenarios:
* Sending telemetry and receiving properties and commands with device connected through symmetric key (Python 2.7+)
* [py3](samples/py3.py) - Sending telemetry and receiving properties and commands with device connected through **symmetric key** (Python 3.7+)
* [py3_x509](samples/py3_x509.py) - Sending telemetry and receiving properties and commands with device connected through **x509 certificates** (Python 3.7+)
* [py3_file_logger](samples/py3_file_logger.py) - Print logs on file with rotation (Python 3.7+)
* [py3_eventhub_logger](samples/py3_eventhub_logger.py) - Redirect logs to Azure Event Hub (Python 3.7+)
* Sending telemetry and receiving properties and commands with device connected through symmetric key (Python 3.7+)
* Sending telemetry and receiving properties and commands with device connected through x509 certificates (Python 2.7+)
* Sending telemetry and receiving properties and commands with device connected through x509 certificates (Python 3.7+)
Samples by default parse a configuration file including required credentials. Just create a file called **samples.ini** inside the _samples_ folder with this content:
**The following samples are legacy samples**, they use the sycnhronous API intended for use with Python 2.7, or in compatibility scenarios with later versions. We recommend you use the asynchronous API and Python3 samples above instead.
* [py2](samples/py2.py) - Sending telemetry and receiving properties and commands with device connected through **symmetric key** (Python 2.7+)
* [py2_x509](samples/py2_x509.py) - Sending telemetry and receiving properties and commands with device connected through **x509 certificates** (Python 2.7+)
Samples by default parse a configuration file including required credentials. Just create a file called **samples.ini** inside the _samples_ folder with a content like this:
```ini
[SymmetricKey]
@ -46,6 +53,14 @@ CertPassphrase = optional password
```
The configuration file can include one of the sections or both. Section names must match references in the sample file.
### Run samples with local changes
It is possible to run samples against the local copy of the device client. This is particularly useful when testing patches not yet published upstream.
Just add an entry to **samples.ini** in the _DEFAULT_ section:
```ini
[DEFAULT]
Local = yes
```
## Importing the module
Sync client (Python 2.7+ and 3.7+) can be imported in this way:
@ -98,12 +113,33 @@ await iotc.connect()
```
After successfull connection, IOTC context is available for further commands.
### Reconnection
The device client automatically handle reconnection in case of network failures or disconnections. However if process runs for long time (e.g. unmonitored devices) a reconnection might fail because of credentials expiration.
To control reconnection and reset credentials the function _is_connected()_ is available and can be used to test connection status inside a loop or before running operations.
e.g.
```py
retry = 0 # stop reconnection attempts
while true:
if iotc.is_connected():
# do something
else
if retry == 10:
sys.exit("error")
retry += 1
iotc.connect()
```
## Operations
### Send telemetry
e.g. Send telemetry every 3 seconds
```py
while iotc.isConnected():
while iotc.is_connected():
await iotc.send_telemetry({
'accelerometerX': str(randint(20, 45)),
'accelerometerY': str(randint(20, 45)),
@ -116,11 +152,11 @@ Properties can be custom or part of the reserved ones (see list [here](https://g
### Send property update
```py
iotc.sendProperty({'fieldName':'fieldValue'});
iotc.sendProperty({'fieldName':'fieldValue'})
```
### Listen to properties update
```py
iotc.on(IOTCEvents.IOTC_PROPERTIES, callback);
iotc.on(IOTCEvents.IOTC_PROPERTIES, callback)
```
To provide setting sync aknowledgement, the callback must reply **True** if the new value has been applied or **False** otherwise
```py
@ -128,7 +164,7 @@ async def onProps(propName, propValue):
print(propValue)
return True
iotc.on(IOTCEvents.IOTC_PROPERTIES, onProps);
iotc.on(IOTCEvents.IOTC_PROPERTIES, onProps)
```
### Listen to commands
@ -144,6 +180,23 @@ async def onCommands(command, ack):
await ack(command.name, 'Command received', command.request_id)
```
## Logging
The default log prints to console operations status and errors.
This is the _IOTC_LOGGING_API_ONLY_ logging level.
The function set_log_level() can be used to change options or disable logs. It accepts a _IOTCLogLevel_ value among the following:
- IOTC_LOGGING_DISABLED (log disabled)
- IOTC_LOGGING_API_ONLY (information and errors, default)
- IOTC_LOGGING_ALL (all messages, debug and underlying errors)
The device client also accepts an optional Logger instance to redirect logs to other targets than console.
The custom class must implement three methods:
- info(message)
- debug(message)
- set_log_level(message);
## One-touch device provisioning and approval
A device can send custom data during provision process: if a device is aware of its IoT Central template Id, then it can be automatically provisioned.

Просмотреть файл

@ -2,7 +2,7 @@
shopt -s expand_aliases
source ~/.bashrc
rm -rf build/ dist/ src/azure/iotcentral/device/client/iotc_device.egg-info src/azure/iotcentral/device/client/_pycache_ src/azure/iotcentral/device/client/_init_.pyc
rm -rf build/ dist/ src/iotc/iotc_device.egg-info src/iotc/_pycache_ src/iotc/_init_.pyc
TEST=""
if [[ $1 == 'test' ]]; then

Просмотреть файл

@ -1,9 +1,8 @@
import sys
import os
import configparser
import time
from random import randint
from azure.iotcentral.device.client import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
config = configparser.ConfigParser()
@ -13,6 +12,12 @@ device_id = config['SymmetricKey']['DeviceId']
scope_id = config['SymmetricKey']['ScopeId']
key = config['SymmetricKey']['Key']
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
# optional model Id for auto-provisioning
try:
model_id = config['SymmetricKey']['ModelId']

Просмотреть файл

@ -1,8 +1,9 @@
import sys
import os
import configparser
import time
from random import randint
from azure.iotcentral.device.client import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__),'samples.ini'))
@ -11,6 +12,11 @@ device_id = config['x509']['DeviceId']
scope_id = config['x509']['ScopeId']
key = {'certFile': config['x509']['CertFilePath'],'keyFile':config['x509']['KeyFilePath'],'certPhrase':config['x509']['CertPassphrase']}
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
# optional model Id for auto-provisioning
try:
model_id = config['x509']['ModelId']

Просмотреть файл

@ -1,12 +1,19 @@
import os
import asyncio
import configparser
from azure.iotcentral.device.client.aio import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
import sys
from random import randint
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__),'samples.ini'))
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents
from iotc.aio import IoTCClient
device_id = config['SymmetricKey']['DeviceId']
scope_id = config['SymmetricKey']['ScopeId']
key = config['SymmetricKey']['Key']

Просмотреть файл

@ -0,0 +1,90 @@
import sys
import os
import asyncio
import configparser
from random import randint
from azure.eventhub.aio import EventHubProducerClient
from azure.eventhub import EventData
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__), 'samples.ini'))
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents
from iotc.aio import IoTCClient
device_id = config['SymmetricKey']['DeviceId']
scope_id = config['SymmetricKey']['ScopeId']
key = config['SymmetricKey']['Key']
event_hub_conn_str = config['EventHub']['ConnectionString']
event_hub_name = config['EventHub']['EventHubName']
# optional model Id for auto-provisioning
try:
model_id = config['SymmetricKey']['ModelId']
except:
model_id = None
class EventHubLogger:
def __init__(self, conn_str, eventhub_name):
self._producer = EventHubProducerClient.from_connection_string(conn_str, eventhub_name=eventhub_name)
async def _create_batch(self):
self._event_data_batch = await self._producer.create_batch()
async def _log(self, message):
event_data_batch = await self._producer.create_batch()
event_data_batch.add(EventData(message))
await self._producer.send_batch(event_data_batch)
async def info(self, message):
if self._log_level != IOTCLogLevel.IOTC_LOGGING_DISABLED:
await self._log(message)
async def debug(self, message):
if self._log_level == IOTCLogLevel.IOTC_LOGGING_ALL:
await self._log(message)
def set_log_level(self, log_level):
self._log_level = log_level
async def on_props(propName, propValue):
print(propValue)
return True
async def on_commands(command, ack):
print(command.name)
await ack(command.name, 'Command received', command.request_id)
# change connect type to reflect the used key (device or group)
iotc = IoTCClient(device_id, scope_id,
IOTCConnectType.IOTC_CONNECT_DEVICE_KEY, key, EventHubLogger(event_hub_conn_str, event_hub_name))
if model_id != None:
iotc.set_model_id(model_id)
iotc.set_log_level(IOTCLogLevel.IOTC_LOGGING_ALL)
iotc.on(IOTCEvents.IOTC_PROPERTIES, on_props)
iotc.on(IOTCEvents.IOTC_COMMAND, on_commands)
# iotc.setQosLevel(IOTQosLevel.IOTC_QOS_AT_MOST_ONCE)
async def main():
await iotc.connect()
while iotc.is_connected():
await iotc.send_telemetry({
'accelerometerX': str(randint(20, 45)),
'accelerometerY': str(randint(20, 45)),
"accelerometerZ": str(randint(20, 45))
})
await asyncio.sleep(3)
asyncio.run(main())

Просмотреть файл

@ -0,0 +1,89 @@
import sys
import os
import asyncio
import configparser
import logging
import logging.handlers
from random import randint
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__), 'samples.ini'))
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents
from iotc.aio import IoTCClient
device_id = config['SymmetricKey']['DeviceId']
scope_id = config['SymmetricKey']['ScopeId']
key = config['SymmetricKey']['Key']
logpath = config['FileLog']['LogsPath']
# optional model Id for auto-provisioning
try:
model_id = config['SymmetricKey']['ModelId']
except:
model_id = None
class FileLogger:
def __init__(self,logpath,logname="iotc_py_log"):
self._logger=logging.getLogger(logname)
self._logger.setLevel(logging.DEBUG)
handler= logging.handlers.RotatingFileHandler(
os.path.join(logpath,logname), maxBytes=20, backupCount=5)
self._logger.addHandler(handler)
async def _log(self, message):
print(message)
self._logger.debug(message)
async def info(self, message):
if self._log_level != IOTCLogLevel.IOTC_LOGGING_DISABLED:
await self._log(message)
async def debug(self, message):
if self._log_level == IOTCLogLevel.IOTC_LOGGING_ALL:
await self._log(message)
def set_log_level(self, log_level):
self._log_level = log_level
async def on_props(propName, propValue):
print(propValue)
return True
async def on_commands(command, ack):
print(command.name)
await ack(command.name, 'Command received', command.request_id)
# change connect type to reflect the used key (device or group)
iotc = IoTCClient(device_id, scope_id,
IOTCConnectType.IOTC_CONNECT_DEVICE_KEY, key, FileLogger(logpath))
if model_id != None:
iotc.set_model_id(model_id)
iotc.set_log_level(IOTCLogLevel.IOTC_LOGGING_ALL)
iotc.on(IOTCEvents.IOTC_PROPERTIES, on_props)
iotc.on(IOTCEvents.IOTC_COMMAND, on_commands)
# iotc.setQosLevel(IOTQosLevel.IOTC_QOS_AT_MOST_ONCE)
async def main():
await iotc.connect()
while iotc.is_connected():
await iotc.send_telemetry({
'accelerometerX': str(randint(20, 45)),
'accelerometerY': str(randint(20, 45)),
"accelerometerZ": str(randint(20, 45))
})
await asyncio.sleep(3)
asyncio.run(main())

Просмотреть файл

@ -1,7 +1,7 @@
import os
import asyncio
import configparser
from azure.iotcentral.device.client.aio import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
import sys
from random import randint
config = configparser.ConfigParser()
@ -11,6 +11,13 @@ device_id = config['x509']['DeviceId']
scope_id = config['x509']['ScopeId']
key = {'certFile': config['x509']['CertFilePath'],'keyFile':config['x509']['KeyFilePath'],'certPhrase':config['x509']['CertPassphrase']}
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents
from iotc.aio import IoTCClient
# optional model Id for auto-provisioning
try:
model_id = config['x509']['ModelId']

Просмотреть файл

@ -2,7 +2,7 @@ import setuptools
import sys
sys.path.insert(0, 'src')
from azure.iotcentral.device.client import __version__, __name__
from iotc import __version__, __name__
with open("README.md", "r") as fh:
long_description = fh.read()
@ -30,5 +30,5 @@ setuptools.setup(
'Programming Language :: Python :: 3.8',
],
include_package_data=True,
install_requires=["azure-iot-device"]
install_requires=["azure-iot-device","uuid","hmac","hashlib","base64","json"]
)

Просмотреть файл

@ -1 +0,0 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

Просмотреть файл

@ -1 +0,0 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

Просмотреть файл

@ -1 +0,0 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

Просмотреть файл

@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
-----END CERTIFICATE-----

Просмотреть файл

@ -1,5 +1,3 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
import sys
import threading
@ -10,55 +8,51 @@ from azure.iot.device import ProvisioningDeviceClient
from azure.iot.device import Message, MethodResponse
from datetime import datetime
__version__ = "0.2.0-beta.12"
__name__ = "azure-iotcentral-device-client"
try:
import hmac
except ImportError:
print("ERROR: missing dependency `hmac`")
sys.exit()
try:
import hashlib
except ImportError:
print("ERROR: missing dependency `hashlib`")
sys.exit()
try:
import base64
except ImportError:
print("ERROR: missing dependency `base64`")
sys.exit()
try:
import json
except ImportError:
print("ERROR: missing dependency `json`")
sys.exit()
try:
import uuid
except ImportError:
print("ERROR: missing dependency `uuid`")
sys.exit()
__version__ = "0.2.1-beta.2"
__name__ = "iotc"
def version():
print(__version__)
try:
import hmac
except ImportError:
print("ERROR: missing dependency `micropython-hmac`")
sys.exit()
try:
import hashlib
except ImportError:
print("ERROR: missing dependency `micropython-hashlib`")
sys.exit()
try:
import base64
except ImportError:
print("ERROR: missing dependency `micropython-base64`")
sys.exit()
try:
import json
except ImportError:
print("ERROR: missing dependency `micropython-json`")
sys.exit()
try:
import uuid
except ImportError:
print("ERROR: missing dependency `micropython-uuid`")
sys.exit()
g_is_micro_python = ('implementation' in dir(sys)) and ('name' in dir(
sys.implementation)) and (sys.implementation.name == 'micropython')
class IOTCConnectType:
IOTC_CONNECT_SYMM_KEY = 1
IOTC_CONNECT_X509_CERT = 2
IOTC_CONNECT_DEVICE_KEY = 3
class IOTCLogLevel:
IOTC_LOGGING_DISABLED = 1
IOTC_LOGGING_API_ONLY = 2
@ -112,7 +106,6 @@ class IoTCClient:
self._cred_type = cred_type
self._key_or_cert = key_or_cert
self._model_id = None
self._connected = False
self._events = {}
self._prop_thread = None
self._cmd_thread = None
@ -120,7 +113,12 @@ class IoTCClient:
if logger is None:
self._logger = ConsoleLogger(IOTCLogLevel.IOTC_LOGGING_API_ONLY)
else:
self._logger = logger
if hasattr(logger, "info") & hasattr(logger, "debug") & hasattr(logger, "set_log_level"):
self._logger = logger
else:
print("ERROR: Logger object has unsupported format. It must implement the following functions\n\
info(message);\ndebug(message);\nset_log_level(message);")
sys.exit()
def is_connected(self):
"""
@ -128,10 +126,10 @@ class IoTCClient:
:returns: Connection state
:rtype: bool
"""
if self._connected:
return True
if not self._device_client:
print("ERROR: A connection was never attempted. You need to first call connect() before querying the connection state")
else:
return False
return self._device_client.connected
def set_global_endpoint(self, endpoint):
"""
@ -300,7 +298,6 @@ class IoTCClient:
# Connect to iothub
try:
self._device_client.connect()
self._connected = True
self._logger.debug('Device connected')
except:
t, v, tb = sys.exc_info()
@ -319,7 +316,6 @@ class IoTCClient:
def _compute_derived_symmetric_key(self, secret, reg_id):
# pylint: disable=no-member
global g_is_micro_python
try:
secret = base64.b64decode(secret)
except:
@ -327,7 +323,4 @@ class IoTCClient:
"ERROR: broken base64 secret => `" + secret + "`")
sys.exit()
if g_is_micro_python == False:
return base64.b64encode(hmac.new(secret, msg=reg_id.encode('utf8'), digestmod=hashlib.sha256).digest()).decode('utf-8')
else:
return base64.b64encode(hmac.new(secret, msg=reg_id.encode('utf8'), digestmod=hashlib._sha256.sha256).digest())
return base64.b64encode(hmac.new(secret, msg=reg_id.encode('utf8'), digestmod=hashlib.sha256).digest()).decode('utf-8')

Просмотреть файл

@ -1,99 +1,55 @@
import sys
import asyncio
from azure.iot.device import X509
from azure.iot.device.aio import IoTHubDeviceClient
from azure.iot.device.aio import ProvisioningDeviceClient
from azure.iot.device import Message, MethodResponse
from datetime import datetime
__version__ = "0.2.0-beta.12"
__name__ = "azure-iotcentral-device-client"
def version():
print(__version__)
from .. import IOTCLogLevel, IOTCEvents, IOTCConnectType
from azure.iot.device import X509, MethodResponse, Message
from azure.iot.device.aio import IoTHubDeviceClient, ProvisioningDeviceClient
try:
import hmac
except ImportError:
print("ERROR: missing dependency `micropython-hmac`")
print("ERROR: missing dependency `hmac`")
sys.exit()
try:
import hashlib
except ImportError:
print("ERROR: missing dependency `micropython-hashlib`")
print("ERROR: missing dependency `hashlib`")
sys.exit()
try:
import base64
except ImportError:
print("ERROR: missing dependency `micropython-base64`")
print("ERROR: missing dependency `base64`")
sys.exit()
try:
import json
except ImportError:
print("ERROR: missing dependency `micropython-json`")
print("ERROR: missing dependency `json`")
sys.exit()
try:
import uuid
except ImportError:
print("ERROR: missing dependency `micropython-uuid`")
print("ERROR: missing dependency `uuid`")
sys.exit()
g_is_micro_python = ('implementation' in dir(sys)) and ('name' in dir(
sys.implementation)) and (sys.implementation.name == 'micropython')
class IOTCConnectType:
IOTC_CONNECT_SYMM_KEY = 1
IOTC_CONNECT_X509_CERT = 2
IOTC_CONNECT_DEVICE_KEY = 3
class IOTCLogLevel:
IOTC_LOGGING_DISABLED = 1
IOTC_LOGGING_API_ONLY = 2
IOTC_LOGGING_ALL = 16
class IOTCConnectionState:
IOTC_CONNECTION_EXPIRED_SAS_TOKEN = 1
IOTC_CONNECTION_DEVICE_DISABLED = 2
IOTC_CONNECTION_BAD_CREDENTIAL = 4
IOTC_CONNECTION_RETRY_EXPIRED = 8
IOTC_CONNECTION_NO_NETWORK = 16
IOTC_CONNECTION_COMMUNICATION_ERROR = 32
IOTC_CONNECTION_OK = 64
class IOTCMessageStatus:
IOTC_MESSAGE_ACCEPTED = 1
IOTC_MESSAGE_REJECTED = 2
IOTC_MESSAGE_ABANDONED = 4
class IOTCEvents:
IOTC_COMMAND = 2,
IOTC_PROPERTIES = 4,
__version__ = "0.2.1-beta.2"
__name__ = "iotc"
class ConsoleLogger:
def __init__(self, log_level):
self._log_level = log_level
def _log(self, message):
async def _log(self, message):
print(message)
def info(self, message):
async def info(self, message):
if self._log_level != IOTCLogLevel.IOTC_LOGGING_DISABLED:
self._log(message)
def debug(self, message):
async def debug(self, message):
if self._log_level == IOTCLogLevel.IOTC_LOGGING_ALL:
self._log(message)
@ -117,7 +73,12 @@ class IoTCClient:
if logger is None:
self._logger = ConsoleLogger(IOTCLogLevel.IOTC_LOGGING_API_ONLY)
else:
self._logger = logger
if hasattr(logger,"info") & hasattr(logger,"debug") & hasattr(logger,"set_log_level"):
self._logger = logger
else:
print("ERROR: Logger object has unsupported format. It must implement the following functions\n\
info(message);\ndebug(message);\nset_log_level(message);")
sys.exit()
def is_connected(self):
"""
@ -130,7 +91,6 @@ class IoTCClient:
else:
return False
def set_global_endpoint(self, endpoint):
"""
Set the device provisioning endpoint.
@ -162,16 +122,16 @@ class IoTCClient:
return 0
async def _on_properties(self):
self._logger.debug('Setup properties listener')
await self._logger.debug('Setup properties listener')
while True:
try:
prop_cb = self._events[IOTCEvents.IOTC_PROPERTIES]
except KeyError:
self._logger.debug('Properties callback not found')
await self._logger.debug('Properties callback not found')
await asyncio.sleep(10)
continue
patch = await self._device_client.receive_twin_desired_properties_patch()
self._logger.debug('Received desired properties. {}'.format(patch))
await self._logger.debug('Received desired properties. {}'.format(patch))
for prop in patch:
if prop == '$version':
@ -179,7 +139,7 @@ class IoTCClient:
ret = await prop_cb(prop, patch[prop]['value'])
if ret:
self._logger.debug('Acknowledging {}'.format(prop))
await self._logger.debug('Acknowledging {}'.format(prop))
await self.send_property({
'{}'.format(prop): {
"value": patch[prop]["value"],
@ -188,10 +148,10 @@ class IoTCClient:
'message': 'Property received'}
})
else:
self._logger.debug(
await self._logger.debug(
'Property "{}" unsuccessfully processed'.format(prop))
async def _cmd_ack(self,name, value, requestId):
async def _cmd_ack(self, name, value, requestId):
await self.send_property({
'{}'.format(name): {
'value': value,
@ -200,18 +160,18 @@ class IoTCClient:
})
async def _on_commands(self):
self._logger.debug('Setup commands listener')
await self._logger.debug('Setup commands listener')
while True:
try:
cmd_cb = self._events[IOTCEvents.IOTC_COMMAND]
except KeyError:
self._logger.debug('Commands callback not found')
await self._logger.debug('Commands callback not found')
await asyncio.sleep(10)
continue
# Wait for unknown method calls
method_request = await self._device_client.receive_method_request()
self._logger.debug(
await self._logger.debug(
'Received command {}'.format(method_request.name))
await self._device_client.send_method_response(MethodResponse.create_from_method_request(
method_request, 200, {
@ -219,7 +179,6 @@ class IoTCClient:
))
await cmd_cb(method_request, self._cmd_ack)
async def _send_message(self, payload, properties):
msg = Message(payload)
msg.message_id = uuid.uuid4()
@ -233,7 +192,7 @@ class IoTCClient:
Send a property message
:param dict payload: The properties payload. Can contain multiple properties in the form {'<propName>':{'value':'<propValue>'}}
"""
self._logger.debug('Sending property {}'.format(json.dumps(payload)))
await self._logger.debug('Sending property {}'.format(json.dumps(payload)))
await self._device_client.patch_twin_reported_properties(payload)
async def send_telemetry(self, payload, properties=None):
@ -242,7 +201,7 @@ class IoTCClient:
:param dict payload: The telemetry payload. Can contain multiple telemetry fields in the form {'<fieldName1>':<fieldValue1>,...,'<fieldNameN>':<fieldValueN>}
:param dict optional properties: An object with custom properties to add to the message.
"""
self._logger.info('Sending telemetry message: {}'.format(payload))
await self._logger.info('Sending telemetry message: {}'.format(payload))
await self._send_message(json.dumps(payload), properties)
async def connect(self):
@ -252,24 +211,26 @@ class IoTCClient:
"""
if self._cred_type in (IOTCConnectType.IOTC_CONNECT_DEVICE_KEY, IOTCConnectType.IOTC_CONNECT_SYMM_KEY):
if self._cred_type == IOTCConnectType.IOTC_CONNECT_SYMM_KEY:
self._key_or_cert = self._compute_derived_symmetric_key(
self._key_or_cert = await self._compute_derived_symmetric_key(
self._key_or_cert, self._device_id)
self._logger.debug('Device key: {}'.format(self._key_or_cert))
await self._logger.debug('Device key: {}'.format(self._key_or_cert))
self._provisioning_client = ProvisioningDeviceClient.create_from_symmetric_key(
self._global_endpoint, self._device_id, self._scope_id, self._key_or_cert)
self._global_endpoint, self._device_id, self._scope_id, self._key_or_cert)
else:
self._key_file = self._key_or_cert['key_file']
self._cert_file = self._key_or_cert['cert_file']
try:
self._cert_phrase=self._key_or_cert['cert_phrase']
x509=X509(self._cert_file,self._key_file,self._cert_phrase)
self._cert_phrase = self._key_or_cert['cert_phrase']
x509 = X509(self._cert_file, self._key_file, self._cert_phrase)
except:
self._logger.debug('No passphrase available for certificate. Trying without it')
x509=X509(self._cert_file,self._key_file)
await self._logger.debug(
'No passphrase available for certificate. Trying without it')
x509 = X509(self._cert_file, self._key_file)
# Certificate provisioning
self._provisioning_client=ProvisioningDeviceClient.create_from_x509_certificate(provisioning_host=self._global_endpoint,registration_id=self._device_id,id_scope=self._scope_id,x509=x509)
self._provisioning_client = ProvisioningDeviceClient.create_from_x509_certificate(
provisioning_host=self._global_endpoint, registration_id=self._device_id, id_scope=self._scope_id, x509=x509)
if self._model_id:
self._provisioning_client.provisioning_payload = {
@ -277,27 +238,29 @@ class IoTCClient:
try:
registration_result = await self._provisioning_client.register()
assigned_hub = registration_result.registration_state.assigned_hub
self._logger.debug(assigned_hub)
await self._logger.debug(assigned_hub)
self._hub_conn_string = 'HostName={};DeviceId={};SharedAccessKey={}'.format(
assigned_hub, self._device_id, self._key_or_cert)
self._logger.debug(
await self._logger.debug(
'IoTHub Connection string: {}'.format(self._hub_conn_string))
if self._cred_type in (IOTCConnectType.IOTC_CONNECT_DEVICE_KEY, IOTCConnectType.IOTC_CONNECT_SYMM_KEY):
self._device_client = IoTHubDeviceClient.create_from_connection_string(self._hub_conn_string)
self._device_client = IoTHubDeviceClient.create_from_connection_string(
self._hub_conn_string)
else:
self._device_client = IoTHubDeviceClient.create_from_x509_certificate(x509=x509,hostname=assigned_hub,device_id=registration_result.registration_state.device_id)
self._device_client = IoTHubDeviceClient.create_from_x509_certificate(
x509=x509, hostname=assigned_hub, device_id=registration_result.registration_state.device_id)
except:
self._logger.info(
await self._logger.info(
'ERROR: Failed to get device provisioning information')
sys.exit()
# Connect to iothub
try:
await self._device_client.connect()
self._connected = True
self._logger.debug('Device connected')
await self._logger.debug('Device connected')
except:
self._logger.info('ERROR: Failed to connect to Hub')
await self._logger.info('ERROR: Failed to connect to Hub')
sys.exit()
# setup listeners
@ -308,17 +271,13 @@ class IoTCClient:
# self._on_commands()
# )
def _compute_derived_symmetric_key(self, secret, reg_id):
async def _compute_derived_symmetric_key(self, secret, reg_id):
# pylint: disable=no-member
global g_is_micro_python
try:
secret = base64.b64decode(secret)
except:
self._logger.debug(
await self._logger.debug(
"ERROR: broken base64 secret => `" + secret + "`")
sys.exit()
if g_is_micro_python == False:
return base64.b64encode(hmac.new(secret, msg=reg_id.encode('utf8'), digestmod=hashlib.sha256).digest()).decode('utf-8')
else:
return base64.b64encode(hmac.new(secret, msg=reg_id.encode('utf8'), digestmod=hashlib._sha256.sha256).digest())
return base64.b64encode(hmac.new(secret, msg=reg_id.encode('utf8'), digestmod=hashlib.sha256).digest()).decode('utf-8')

Просмотреть файл

@ -27,4 +27,24 @@ python3 -m pytest test/v3
Python 2.7+
```
python2 -m pytest test/v2
```
```
Tests by default parse a configuration file including required credentials. Just create a file called **tests.ini** inside the _test_ folder with a content like this:
```ini
[TESTS]
GroupKey = ....
DeviceKey = ....
DeviceId = ....
ScopeId = ....
AssignedHub = ....
ExpectedHub = ....
```
### Run locally
It is possible to run tests against the local copy of the device client. This is particularly useful when testing patches not yet published upstream.
Just add an entry to the configuration file under the _TESTS_ section
```ini
[TESTS]
Local = yes
```

Просмотреть файл

@ -3,14 +3,19 @@ import mock
import time
import configparser
import os
import sys
from azure.iot.device.provisioning.models import RegistrationResult
from azure.iot.device.iothub.models import MethodRequest
from azure.iotcentral.device.client import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__),'../tests.ini'))
config.read(os.path.join(os.path.dirname(__file__), '../tests.ini'))
if config['TESTS'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
groupKey = config['TESTS']['GroupKey']
deviceKey = config['TESTS']['DeviceKey']
@ -68,7 +73,6 @@ def init(mocker):
return iotc
def test_computeKey(mocker):
iotc = init(mocker)
assert iotc._compute_derived_symmetric_key(

Просмотреть файл

@ -4,62 +4,70 @@ import time
import asyncio
import configparser
import os
import sys
from contextlib import suppress
from azure.iot.device.provisioning.models import RegistrationResult
from azure.iot.device.iothub.models import MethodRequest
from azure.iotcentral.device.client.aio import IoTCClient, IOTCConnectType, IOTCLogLevel, IOTCEvents
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__),'../tests.ini'))
config.read(os.path.join(os.path.dirname(__file__), '../tests.ini'))
groupKey=config['TESTS']['GroupKey']
deviceKey=config['TESTS']['DeviceKey']
device_id=config['TESTS']['DeviceId']
scopeId=config['TESTS']['ScopeId']
assignedHub=config['TESTS']['AssignedHub']
expectedHub=config['TESTS']['ExpectedHub']
if config['TESTS'].getboolean('Local'):
sys.path.insert(0, 'src')
propPayload={'prop1':{'value':40},'$version':5}
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents
from iotc.aio import IoTCClient
cmdRequestId='abcdef'
cmdName='command1'
cmdPayload='payload'
methodRequest=MethodRequest(cmdRequestId,cmdName,cmdPayload)
groupKey = config['TESTS']['GroupKey']
deviceKey = config['TESTS']['DeviceKey']
device_id = config['TESTS']['DeviceId']
scopeId = config['TESTS']['ScopeId']
assignedHub = config['TESTS']['AssignedHub']
expectedHub = config['TESTS']['ExpectedHub']
propPayload = {'prop1': {'value': 40}, '$version': 5}
cmdRequestId = 'abcdef'
cmdName = 'command1'
cmdPayload = 'payload'
methodRequest = MethodRequest(cmdRequestId, cmdName, cmdPayload)
class NewRegState():
def __init__(self):
self.assigned_hub=assignedHub
def assigned_hub(self):
return self.assigned_hub
def __init__(self):
self.assigned_hub = assignedHub
def assigned_hub(self):
return self.assigned_hub
class NewProvClient():
async def register(self):
reg=RegistrationResult('3o375i827i852','assigned',NewRegState())
return reg
async def register(self):
reg = RegistrationResult('3o375i827i852', 'assigned', NewRegState())
return reg
class NewDeviceClient():
async def connect(self):
return True
async def connect(self):
return True
async def receive_twin_desired_properties_patch(self):
await asyncio.sleep(3)
return propPayload
async def receive_method_request(self):
await asyncio.sleep(3)
return methodRequest
async def receive_twin_desired_properties_patch(self):
await asyncio.sleep(3)
return propPayload
async def receive_method_request(self):
await asyncio.sleep(3)
return methodRequest
async def send_method_response(self, payload):
return True
async def patch_twin_reported_properties(self, payload):
return True
async def send_method_response(self,payload):
return True
async def patch_twin_reported_properties(self,payload):
return True
def async_return(result):
f = asyncio.Future()
@ -68,128 +76,128 @@ def async_return(result):
async def stop_threads(iotc):
iotc._prop_thread.cancel()
iotc._cmd_thread.cancel()
iotc._prop_thread.cancel()
iotc._cmd_thread.cancel()
@pytest.mark.asyncio
def init(mocker):
iotc=IoTCClient(device_id, scopeId,
IOTCConnectType.IOTC_CONNECT_SYMM_KEY, groupKey)
mocker.patch('azure.iotcentral.device.client.aio.ProvisioningDeviceClient.create_from_symmetric_key',return_value=NewProvClient())
mocker.patch('azure.iotcentral.device.client.aio.IoTHubDeviceClient.create_from_connection_string',return_value=NewDeviceClient())
return iotc
iotc = IoTCClient(device_id, scopeId,
IOTCConnectType.IOTC_CONNECT_SYMM_KEY, groupKey)
mocker.patch('azure.iotcentral.device.client.aio.ProvisioningDeviceClient.create_from_symmetric_key',
return_value=NewProvClient())
mocker.patch('azure.iotcentral.device.client.aio.IoTHubDeviceClient.create_from_connection_string',
return_value=NewDeviceClient())
return iotc
@pytest.mark.asyncio
async def test_computeKey(mocker):
iotc=init(mocker)
assert iotc._compute_derived_symmetric_key(groupKey,device_id) == deviceKey
iotc = init(mocker)
key = await iotc._compute_derived_symmetric_key(
groupKey, device_id)
assert key == deviceKey
@pytest.mark.asyncio
async def test_deviceKeyGeneration(mocker):
iotc = init(mocker)
await iotc.connect()
assert iotc._key_or_cert == deviceKey
await stop_threads(iotc)
iotc = init(mocker)
await iotc.connect()
assert iotc._key_or_cert == deviceKey
await stop_threads(iotc)
@pytest.mark.asyncio
async def test_hubConnectionString(mocker):
iotc = init(mocker)
await iotc.connect()
assert iotc._hub_conn_string == expectedHub
await stop_threads(iotc)
iotc = init(mocker)
await iotc.connect()
assert iotc._hub_conn_string == expectedHub
await stop_threads(iotc)
@pytest.mark.asyncio
async def test_onproperties_before(mocker):
iotc = init(mocker)
iotc = init(mocker)
async def onProps(propname,propvalue):
assert propname == 'prop1'
assert propvalue == 40
await stop_threads(iotc)
async def onProps(propname, propvalue):
assert propname == 'prop1'
assert propvalue == 40
await stop_threads(iotc)
mocker.patch.object(iotc,'send_property',return_value=True)
iotc.on(IOTCEvents.IOTC_PROPERTIES,onProps)
await iotc.connect()
try:
await iotc._prop_thread
await iotc._cmd_thread
except asyncio.CancelledError:
pass
mocker.patch.object(iotc, 'send_property', return_value=True)
iotc.on(IOTCEvents.IOTC_PROPERTIES, onProps)
await iotc.connect()
try:
await iotc._prop_thread
await iotc._cmd_thread
except asyncio.CancelledError:
pass
@pytest.mark.asyncio
async def test_onproperties_after(mocker):
iotc = init(mocker)
async def onProps(propname,propvalue):
assert propname == 'prop1'
assert propvalue == 40
await stop_threads(iotc)
return True
iotc = init(mocker)
mocker.patch.object(iotc,'send_property',return_value=True)
await iotc.connect()
iotc.on(IOTCEvents.IOTC_PROPERTIES,onProps)
async def onProps(propname, propvalue):
assert propname == 'prop1'
assert propvalue == 40
await stop_threads(iotc)
return True
try:
await iotc._prop_thread
except asyncio.CancelledError:
pass
mocker.patch.object(iotc, 'send_property', return_value=True)
await iotc.connect()
iotc.on(IOTCEvents.IOTC_PROPERTIES, onProps)
try:
await iotc._prop_thread
except asyncio.CancelledError:
pass
@pytest.mark.asyncio
async def test_on_commands_before(mocker):
iotc = init(mocker)
iotc = init(mocker)
async def onCmds(command,ack):
ret=ack()
assert ret=='mocked'
await stop_threads(iotc)
return True
async def onCmds(command, ack):
ret = ack()
assert ret == 'mocked'
await stop_threads(iotc)
return True
def mockedAck():
return 'mocked'
def mockedAck():
return 'mocked'
mocker.patch.object(iotc, '_cmd_ack', mockedAck)
mocker.patch.object(iotc,'_cmd_ack',mockedAck)
iotc.on(IOTCEvents.IOTC_COMMAND, onCmds)
await iotc.connect()
try:
await iotc._cmd_thread
except asyncio.CancelledError:
pass
iotc.on(IOTCEvents.IOTC_COMMAND,onCmds)
await iotc.connect()
try:
await iotc._cmd_thread
except asyncio.CancelledError:
pass
@pytest.mark.asyncio
async def test_onCommands_after(mocker):
iotc = init(mocker)
iotc = init(mocker)
async def onCmds(command,ack):
ret=ack()
assert ret=='mocked'
await stop_threads(iotc)
return True
async def onCmds(command, ack):
ret = ack()
assert ret == 'mocked'
await stop_threads(iotc)
return True
def mockedAck():
return 'mocked'
def mockedAck():
return 'mocked'
mocker.patch.object(iotc, '_cmd_ack', mockedAck)
mocker.patch.object(iotc,'_cmd_ack',mockedAck)
await iotc.connect()
iotc.on(IOTCEvents.IOTC_COMMAND,onCmds)
try:
await iotc._cmd_thread
except asyncio.CancelledError:
pass
await iotc.connect()
iotc.on(IOTCEvents.IOTC_COMMAND, onCmds)
try:
await iotc._cmd_thread
except asyncio.CancelledError:
pass