This commit is contained in:
lucadruda 2021-11-08 16:34:58 +01:00
Родитель 30ed120210
Коммит 37cdabb318
14 изменённых файлов: 199 добавлений и 87 удалений

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

@ -1,3 +1,14 @@
1.1.2 (2021-11-8)
-----------------
- added credentials cache serialize/deserialize methods
- added "Property" model
- added storage tests
1.1.1 (2021-06-25)
-----------------
- minor fixes for value wrapping
1.1.0 (2021-02-05)
-----------------
- dropped Python 2.7 support (client should continue to work without tests)

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

@ -5,6 +5,8 @@ import sys
from random import randint
from iotc.models import Property,Command
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__), "samples.ini"))
@ -29,11 +31,6 @@ key = config["DEVICE_M3"]["DeviceKey"]
class MemStorage(Storage):
def retrieve(self):
# return CredentialsCache(
# hub_name,
# device_id,
# key,
# )
return None
def persist(self, credentials):
@ -48,8 +45,8 @@ except:
model_id = None
async def on_props(property_name, property_value, component_name):
print("Received {}:{}".format(property_name, property_value))
async def on_props(prop:Property):
print(f"Received {prop.name}:{prop.value}")
return True

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

@ -4,10 +4,10 @@ from iotc import (
IOTCConnectType,
IOTCLogLevel,
IOTCEvents,
Command,
CredentialsCache,
Storage,
)
from iotc.models import Command, Property
import os
import asyncio
import configparser
@ -76,8 +76,8 @@ except:
model_id = None
async def on_props(property_name, property_value, component_name):
print("Received {}:{}".format(property_name, property_value))
async def on_props(prop: Property):
print(f"Received {prop.name}:{prop.value}")
return True

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

@ -1,3 +1,10 @@
from iotc.models import Command, Property, CredentialsCache, Storage
from iotc.aio import IoTCClient
from iotc import (
IOTCConnectType,
IOTCLogLevel,
IOTCEvents,
)
import os
import asyncio
import configparser
@ -12,22 +19,13 @@ 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,
Command,
CredentialsCache,
Storage,
)
from iotc.aio import IoTCClient
class FileLogger:
def __init__(self,logpath,logname="iotc_py_log"):
self._logger=logging.getLogger(logname)
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)
handler = logging.handlers.RotatingFileHandler(
os.path.join(logpath, logname), maxBytes=20, backupCount=5)
self._logger.addHandler(handler)
async def _log(self, message):
@ -46,7 +44,6 @@ class FileLogger:
self._log_level = log_level
device_id = config["DEVICE_M3"]["DeviceId"]
scope_id = config["DEVICE_M3"]["ScopeId"]
key = config["DEVICE_M3"]["DeviceKey"]
@ -54,7 +51,6 @@ hub_name = config["DEVICE_M3"]["HubName"]
log_path = config['FileLog']['LogsPath']
class MemStorage(Storage):
def retrieve(self):
return CredentialsCache(
@ -75,8 +71,8 @@ except:
model_id = None
async def on_props(property_name, property_value, component_name):
print("Received {}:{}".format(property_name, property_value))
async def on_props(prop: Property):
print(f"Received {prop.name}:{prop.value}")
return True
@ -85,8 +81,9 @@ async def on_commands(command: Command):
await command.reply()
async def on_enqueued_commands(command:Command):
print("Received offline command {} with value {}".format(command.name, command.value))
async def on_enqueued_commands(command: Command):
print("Received offline command {} with value {}".format(
command.name, command.value))
# change connect type to reflect the used key (device or group)
@ -106,16 +103,17 @@ client.on(IOTCEvents.IOTC_PROPERTIES, on_props)
client.on(IOTCEvents.IOTC_COMMAND, on_commands)
client.on(IOTCEvents.IOTC_ENQUEUED_COMMAND, on_enqueued_commands)
async def main():
await client.connect()
await client.send_property({"writeableProp": 50})
while not client.terminated():
if client.is_connected():
await client.send_telemetry(
{
"temperature": randint(20, 45)
},{
}, {
"$.sub": "firstcomponent"
}
)

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

@ -1,3 +1,6 @@
from iotc.models import Storage, CredentialsCache, Command, Property
from iotc.aio import IoTCClient
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents
import os
import asyncio
import configparser
@ -5,20 +8,19 @@ import sys
from random import randint
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__),'samples.ini'))
config.read(os.path.join(os.path.dirname(__file__), 'samples.ini'))
# Change config section name to reflect sample.ini
device_id = config['DEVICE_A']['DeviceId']
scope_id = config['DEVICE_A']['ScopeId']
hub_name = config["DEVICE_A"]["HubName"]
x509 = {'cert_file': config['DEVICE_A']['CertFilePath'],'key_file':config['DEVICE_A']['KeyFilePath'],'cert_phrase':config['DEVICE_A']['CertPassphrase']}
x509 = {'cert_file': config['DEVICE_A']['CertFilePath'], 'key_file': config['DEVICE_A']
['KeyFilePath'], 'cert_phrase': config['DEVICE_A']['CertPassphrase']}
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents
from iotc.aio import IoTCClient
class MemStorage(Storage):
def retrieve(self):
@ -40,8 +42,8 @@ except:
model_id = None
async def on_props(property_name, property_value, component_name):
print("Received {}:{}".format(property_name, property_value))
async def on_props(prop: Property):
print(f"Received {prop.name}:{prop.value}")
return True
@ -50,8 +52,9 @@ async def on_commands(command: Command):
await command.reply()
async def on_enqueued_commands(command:Command):
print("Received offline command {} with value {}".format(command.name, command.value))
async def on_enqueued_commands(command: Command):
print("Received offline command {} with value {}".format(
command.name, command.value))
# change connect type to reflect the used key (device or group)
@ -70,16 +73,17 @@ client.on(IOTCEvents.IOTC_PROPERTIES, on_props)
client.on(IOTCEvents.IOTC_COMMAND, on_commands)
client.on(IOTCEvents.IOTC_ENQUEUED_COMMAND, on_enqueued_commands)
async def main():
await client.connect()
await client.send_property({"writeableProp": 50})
while not client.terminated():
if client.is_connected():
await client.send_telemetry(
{
"temperature": randint(20, 45)
},{
}, {
"$.sub": "firstcomponent"
}
)

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

@ -1,5 +1,13 @@
from iotc.models import Command, Storage, CredentialsCache, Property
from iotc import IoTCClient
from iotc import (
IOTCConnectType,
IOTCLogLevel,
IOTCEvents,
)
import os
import configparser
from re import M
import sys
import time
@ -11,15 +19,6 @@ 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,
Command,
CredentialsCache,
Storage,
)
from iotc import IoTCClient
device_id = config["DEVICE_M3"]["DeviceId"]
scope_id = config["DEVICE_M3"]["ScopeId"]
@ -47,8 +46,8 @@ except:
model_id = None
def on_props(property_name, property_value, component_name):
print("Received {}:{}".format(property_name, property_value))
def on_props(prop: Property):
print(f"Received {prop.name}:{prop.value}")
return True
@ -58,7 +57,8 @@ def on_commands(command):
def on_enqueued_commands(command):
print("Received offline command {} with value {}".format(command.name, command.value))
print("Received offline command {} with value {}".format(
command.name, command.value))
# change connect type to reflect the used key (device or group)
@ -81,7 +81,7 @@ client.on(IOTCEvents.IOTC_ENQUEUED_COMMAND, on_enqueued_commands)
def main():
client.connect()
client.send_property({"writeableProp": 50})
while not client.terminated():
if client.is_connected():
client.send_telemetry(
@ -93,4 +93,5 @@ def main():
)
time.sleep(3)
main()

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

@ -1,3 +1,5 @@
from iotc.models import CredentialsCache, Storage, Property, Command
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents, IoTCClient
import os
import configparser
import sys
@ -5,20 +7,19 @@ from random import randint
import time
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__),'samples.ini'))
config.read(os.path.join(os.path.dirname(__file__), 'samples.ini'))
# Change config section name to reflect sample.ini
device_id = config['DEVICE_A']['DeviceId']
scope_id = config['DEVICE_A']['ScopeId']
hub_name = config["DEVICE_A"]["HubName"]
x509 = {'cert_file': config['DEVICE_A']['CertFilePath'],'key_file':config['DEVICE_A']['KeyFilePath'],'cert_phrase':config['DEVICE_A']['CertPassphrase']}
x509 = {'cert_file': config['DEVICE_A']['CertFilePath'], 'key_file': config['DEVICE_A']
['KeyFilePath'], 'cert_phrase': config['DEVICE_A']['CertPassphrase']}
if config['DEFAULT'].getboolean('Local'):
sys.path.insert(0, 'src')
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents,IoTCClient
class MemStorage(Storage):
def retrieve(self):
@ -40,8 +41,8 @@ except:
model_id = None
def on_props(property_name, property_value, component_name):
print("Received {}:{}".format(property_name, property_value))
def on_props(prop: Property):
print(f"Received {prop.name}:{prop.value}")
return True
@ -50,8 +51,9 @@ def on_commands(command: Command):
command.reply()
def on_enqueued_commands(command:Command):
print("Received offline command {} with value {}".format(command.name, command.value))
def on_enqueued_commands(command: Command):
print("Received offline command {} with value {}".format(
command.name, command.value))
# change connect type to reflect the used key (device or group)
@ -70,10 +72,11 @@ client.on(IOTCEvents.IOTC_PROPERTIES, on_props)
client.on(IOTCEvents.IOTC_COMMAND, on_commands)
client.on(IOTCEvents.IOTC_ENQUEUED_COMMAND, on_enqueued_commands)
def main():
client.connect()
client.send_property({"writeableProp": 50})
while not client.terminated():
if client.is_connected():
client.send_telemetry(
@ -85,4 +88,5 @@ def main():
)
time.sleep(3)
main()

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

@ -5,7 +5,7 @@ import sys
with open("README.md", "r") as fh:
long_description = fh.read()
version = "1.1.1"
version = "1.1.2"
setuptools.setup(
name='iotc',

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

@ -8,7 +8,7 @@ from azure.iot.device import IoTHubDeviceClient
from azure.iot.device import ProvisioningDeviceClient
from azure.iot.device import Message, MethodResponse
from datetime import datetime
from .models import Command, CredentialsCache, Storage, GracefulExit
from .models import Command, CredentialsCache, Property, Storage, GracefulExit
try:
__version__ = pkg_resources.get_distribution("iotc").version
@ -266,7 +266,8 @@ class IoTCClient(AbstractClient):
component_name=None,
):
if callback is not None:
ret = callback(property_name, property_value, component_name)
prop = Property(property_name, property_value, component_name)
ret = callback(prop)
else:
ret = True
if ret:
@ -571,8 +572,8 @@ class IoTCClient(AbstractClient):
self._device_client.on_method_request_received = self._on_commands
self._device_client.on_message_received = self._on_enqueued_commands
self._conn_thread=threading.Thread(target=self._on_connection_state)
self._conn_thread.daemon=True
self._conn_thread = threading.Thread(target=self._on_connection_state)
self._conn_thread.daemon = True
self._conn_thread.start()
signal.signal(signal.SIGINT, self.disconnect)

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

@ -2,6 +2,8 @@ import sys
import signal
import asyncio
import pkg_resources
from iotc.models import Property
from .. import (
AbstractClient,
IOTCLogLevel,
@ -116,7 +118,8 @@ class IoTCClient(AbstractClient):
component_name=None,
):
if callback is not None:
ret = await callback(property_name, property_value, component_name)
prop = Property(property_name, property_value, component_name)
ret = await callback(prop)
else:
ret = True
if ret:
@ -362,6 +365,9 @@ class IoTCClient(AbstractClient):
else None,
)
if self._storage is not None:
self._storage.persist(_credentials)
except Exception as e:
await self._logger.info(
"ERROR: Failed to get device provisioning information. {}".format(
@ -409,7 +415,7 @@ class IoTCClient(AbstractClient):
self._device_client.on_method_request_received = self._on_commands
self._device_client.on_message_received = self._on_enqueued_commands
if hasattr(self,'_conn_thread') and self._conn_thread is not None:
if hasattr(self, '_conn_thread') and self._conn_thread is not None:
try:
self._conn_thread.cancel()
await self._conn_thread
@ -432,7 +438,7 @@ class IoTCClient(AbstractClient):
async def disconnect(self):
await self._logger.info("Received shutdown signal")
self._terminate = True
if hasattr(self,'_conn_thread') and self._conn_thread is not None:
if hasattr(self, '_conn_thread') and self._conn_thread is not None:
tasks = asyncio.gather(
self._conn_thread
)

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

@ -41,6 +41,18 @@ class CredentialsCache(object):
self._hub_name, self._device_id
)
def todict(self):
return {
"device_id": self.device_id,
"hub_name": self.hub_name,
"connection_string": self.connection_string,
"device_key": self.device_key,
}
@classmethod
def from_dict(classref, data: dict):
return CredentialsCache(data["hub_name"], data["device_id"], data["device_key"])
class Storage(object):
__metaclass__ = abc.ABCMeta
@ -75,3 +87,31 @@ class Command(object):
@property
def component_name(self):
return self._component_name
def __eq__(self, o: object) -> bool:
return self.name == o.name and self.value == o.value and self.component_name == o.component_name
class Property(object):
def __init__(self, property_name, property_value, component_name=None):
self._property_name = property_name
self._property_value = property_value
if component_name is not None:
self._component_name = component_name
else:
self._component_name = None
@property
def name(self):
return self._property_name
@property
def value(self):
return self._property_value
@property
def component_name(self):
return self._component_name
def __eq__(self, o: object) -> bool:
return self.name == o.name and self.value == o.value and self.component_name == o.component_name

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

@ -1,3 +1,4 @@
from iotc.models import Property
from iotc.test import dummy_storage
from iotc.aio import IoTCClient
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents, Command
@ -80,7 +81,7 @@ async def test_on_properties_triggered(mocker, iotc_client):
iotc_client.on(IOTCEvents.IOTC_PROPERTIES, prop_stub)
await iotc_client.connect()
await iotc_client._device_client.on_twin_desired_properties_patch_received(DEFAULT_COMPONENT_PROP)
prop_stub.assert_called_with("prop1", "value1", None)
prop_stub.assert_called_with(Property("prop1", "value1"))
@pytest.mark.asyncio
@ -91,7 +92,7 @@ async def test_on_properties_triggered_with_component(mocker, iotc_client):
iotc_client.on(IOTCEvents.IOTC_PROPERTIES, prop_stub)
await iotc_client.connect()
await iotc_client._device_client.on_twin_desired_properties_patch_received(COMPONENT_PROP)
prop_stub.assert_called_with("prop1", "value1", "component1")
prop_stub.assert_called_with(Property("prop1", "value1", "component1"))
@pytest.mark.asyncio
@ -104,10 +105,10 @@ async def test_on_properties_triggered_with_complex_component(mocker, iotc_clien
await iotc_client._device_client.on_twin_desired_properties_patch_received(COMPLEX_COMPONENT_PROP)
prop_stub.assert_has_calls(
[
mocker.call("prop1", {"item1": "value1"}, "component1"),
mocker.call("prop1", "value1", "component2"),
mocker.call("prop2", 2, "component2"),
mocker.call("prop2", {"item2": "value2"}, None),
mocker.call(Property("prop1", {"item1": "value1"}, "component1")),
mocker.call(Property("prop1", "value1", "component2")),
mocker.call(Property("prop2", 2, "component2")),
mocker.call(Property("prop2", {"item2": "value2"})),
]
)

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

@ -1,5 +1,6 @@
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents, IoTCClient, Command
from iotc import IOTCConnectType, IOTCLogLevel, IOTCEvents, IoTCClient
from iotc.test import dummy_storage
from iotc.models import Command, Property
from azure.iot.device import MethodRequest, Message
import pytest
import configparser
@ -76,8 +77,9 @@ def test_on_properties_triggered(mocker, iotc_client):
prop_stub = mocker.MagicMock()
iotc_client.on(IOTCEvents.IOTC_PROPERTIES, prop_stub)
iotc_client.connect()
iotc_client._device_client.on_twin_desired_properties_patch_received(DEFAULT_COMPONENT_PROP)
prop_stub.assert_called_with("prop1", {"value":"value1"}, None)
iotc_client._device_client.on_twin_desired_properties_patch_received(
DEFAULT_COMPONENT_PROP)
prop_stub.assert_called_with(Property("prop1", {"value": "value1"}))
def test_on_properties_triggered_with_component(mocker, iotc_client):
@ -86,8 +88,10 @@ def test_on_properties_triggered_with_component(mocker, iotc_client):
prop_stub.return_value = True
iotc_client.on(IOTCEvents.IOTC_PROPERTIES, prop_stub)
iotc_client.connect()
iotc_client._device_client.on_twin_desired_properties_patch_received(COMPONENT_PROP)
prop_stub.assert_called_with("prop1", {"value": "value1"}, "component1")
iotc_client._device_client.on_twin_desired_properties_patch_received(
COMPONENT_PROP)
prop_stub.assert_called_with(
Property("prop1", {"value": "value1"}, "component1"))
def test_on_properties_triggered_with_complex_component(mocker, iotc_client):
@ -96,13 +100,14 @@ def test_on_properties_triggered_with_complex_component(mocker, iotc_client):
prop_stub.return_value = True
iotc_client.on(IOTCEvents.IOTC_PROPERTIES, prop_stub)
iotc_client.connect()
iotc_client._device_client.on_twin_desired_properties_patch_received(COMPLEX_COMPONENT_PROP)
iotc_client._device_client.on_twin_desired_properties_patch_received(
COMPLEX_COMPONENT_PROP)
prop_stub.assert_has_calls(
[
mocker.call("prop1", {"item1": "value1"}, "component1"),
mocker.call("prop1", "value1", "component2"),
mocker.call("prop2", 2, "component2"),
mocker.call("prop2", {"item2": "value2"}, None),
mocker.call(Property("prop1", {"item1": "value1"}, "component1")),
mocker.call(Property("prop1", "value1", "component2")),
mocker.call(Property("prop2", 2, "component2")),
mocker.call(Property("prop2", {"item2": "value2"})),
], any_order=True
)

44
src/iotc/test/utils.py Normal file
Просмотреть файл

@ -0,0 +1,44 @@
import pytest
from iotc.models import CredentialsCache
from iotc.models import Storage
class MemStorage(Storage):
def __init__(self, initial=None):
if initial:
self.creds = initial
else:
self.creds = {}
def persist(self, credentials: CredentialsCache):
self.creds = credentials.todict()
def retrieve(self):
return CredentialsCache.from_dict(self.creds)
def test_storage_persist():
creds = CredentialsCache('hub_name', 'device_id', 'device_key')
storage = MemStorage()
storage.persist(creds)
assert storage.creds['hub_name'] == creds.hub_name
assert storage.creds['device_id'] == creds.device_id
assert storage.creds['device_key'] == creds.device_key
def test_storage_retrieve():
storage = MemStorage(
{'hub_name': 'hub_name', 'device_id': 'device_id', 'device_key': 'device_key'})
creds = storage.retrieve()
assert creds.hub_name == 'hub_name'
assert creds.device_id == 'device_id'
assert creds.device_key == 'device_key'
def test_connection_string():
storage = MemStorage()
creds = CredentialsCache('hub_name', 'device_id', 'device_key')
storage.persist(creds)
cstring = "HostName=hub_name;DeviceId=device_id;SharedAccessKey=device_key"
assert cstring == creds.connection_string
assert (storage.retrieve()).connection_string == cstring