Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c0f3ffb
Introducing IoT Hub dataplane RBAC support + various improvements (#341)
digimaun May 17, 2021
624d849
Iotc command versioning (#340)
valluriraj May 17, 2021
4bf999a
Add warning for qos deprecation and update contributing guide (#342)
avagraw May 18, 2021
b45c7f5
Integrate TQDM to show progress bar when simulator sends d2c messages
avagraw May 18, 2021
72df955
remove unused loop varable
avagraw May 18, 2021
1714e84
Update progress bar descriptionb
avagraw May 18, 2021
43160ec
Merge remote-tracking branch 'other/device_client_integration' into d…
avagraw May 20, 2021
b268dbe
Iotc command ga (#348)
valluriraj May 24, 2021
a4baacb
Use enum value instead of literal str. (#349)
digimaun May 25, 2021
79bf6cb
Managed identity support for device-identity import and export (#344)
c-ryan-k May 25, 2021
616e2c0
Update azext_metadata.json (#351)
digimaun May 25, 2021
9ee1ddc
Increment version to v0.10.13
digimaun May 25, 2021
3f12f21
Update README.md
digimaun May 25, 2021
058648a
Update HISTORY.rst
digimaun May 25, 2021
5a251d9
update twin reported properties during simulation
avagraw May 27, 2021
70f806f
update unit tests
avagraw May 27, 2021
759503a
Add dataplane reset (#352)
vilit1 May 27, 2021
f4333a9
styling updates
avagraw May 27, 2021
f82ef12
C2D messaging improvements. (#354)
digimaun May 27, 2021
48af558
Digital Twin wait commands (#345)
vilit1 May 27, 2021
354a98d
Add Identity Storage Account ID param to sentinel values (#355)
c-ryan-k May 28, 2021
0e983b8
Using SDK Listener for Twin properties update
avagraw Jun 1, 2021
21e2370
Merge remote-tracking branch 'upstream/device_client_integration' int…
avagraw Jun 1, 2021
b498a60
Module identity renew key (#356)
vilit1 Jun 1, 2021
72fa744
Update README.md
digimaun Jun 1, 2021
e9f8803
merge from remote
avagraw Jun 1, 2021
2ef0a41
Pipeline updates (#359)
c-ryan-k Jun 1, 2021
3b1ded1
Merge remote-tracking branch 'upstream/device_client_integration' int…
avagraw Jun 2, 2021
d4fb875
Structured mqtt formatting and eliminate dependency on six
avagraw Jun 2, 2021
1e9ada4
remove indent property not needed any more
avagraw Jun 2, 2021
3170fd8
Check for conditionals before running test jobs (#363)
c-ryan-k Jun 2, 2021
bb3c5d9
Update d2c and simulate commands to return errors for non SaS devices…
avagraw Jun 3, 2021
acf59c0
Support C2D Message decoding and Add TQDM for HTTP simulation
avagraw Jun 5, 2021
e64f460
Update MQTT operations to run on web sockets
avagraw Jun 7, 2021
50e96a2
Merge remote-tracking branch 'upstream/device_client_integration' int…
avagraw Jun 7, 2021
773bfbb
Remove duplicate test already in dev branch
avagraw Jun 7, 2021
fecc2d8
Device connection is automatic now
avagraw Jun 7, 2021
260e273
Styling update
avagraw Jun 7, 2021
b72ebb3
Merge remote-tracking branch 'upstream/device_client_integration' int…
avagraw Jun 10, 2021
c7e0896
Merging changes from dev branch
avagraw Jun 10, 2021
589d678
Feature to support setting twin reported properties during simulation
avagraw Jun 22, 2021
2c0ba67
Merging with upstream
avagraw Jun 22, 2021
bbb82ca
Remove extra quote
avagraw Jun 23, 2021
54a46e5
update twin reported properties during simulation
avagraw May 27, 2021
b7d9d12
update unit tests
avagraw May 27, 2021
1487c1f
styling updates
avagraw May 27, 2021
782e34b
Using SDK Listener for Twin properties update
avagraw Jun 1, 2021
a588704
Support C2D Message decoding and Add TQDM for HTTP simulation
avagraw Jun 5, 2021
a15c8a0
Merging changes
avagraw Jun 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion azext_iot/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@
be acknowledged with completion. For http simulation c2d acknowledgement is based on user
selection which can be complete, reject or abandon. Additionally, mqtt simulation is only
supported for symmetric key auth (SAS) based devices. The mqtt simulation also supports direct
method invocation which can be acknowledged by a response status code and response payload
method invocation which can be acknowledged by a response status code and response payload.

Note: The command by default will set content-type to application/json and content-encoding
to utf-8. This can be overriden.
Expand All @@ -881,6 +881,10 @@
text: az iot device simulate -n {iothub_name} -d {device_id} --method-response-code 201 --method-response-payload '{"result":"Direct method successful"}'
- name: Basic usage (mqtt) with sending direct method response status code and direct method response payload as path to local file
text: az iot device simulate -n {iothub_name} -d {device_id} --method-response-code 201 --method-response-payload '../my_direct_method_payload.json'
- name: Basic usage (mqtt) with sending the initial state of device twin properties as raw json for the target device
text: az iot device simulate -n {iothub_name} -d {device_id} --init-reported-properties '{"reported_prop_1":"val_1", "reported_prop_2":val_2}'
- name: Basic usage (mqtt) with sending the initial state of device twin properties as as path to local file for the target device
text: az iot device simulate -n {iothub_name} -d {device_id} --init-reported-properties '../my_device_twin_reported_properties.json'
- name: Basic usage (http)
text: az iot device simulate -n {iothub_name} -d {device_id} --protocol http
- name: Basic usage (http) with sending mixed properties
Expand Down
6 changes: 6 additions & 0 deletions azext_iot/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ def load_arguments(self, _):
help="Payload to be returned when direct method is executed on device. Provide file path or raw json. "
"Optional param, only supported for mqtt.",
)
context.argument(
"init_reported_properties",
options_list=["--init-reported-properties", "--irp"],
help="Initial state of twin reported properties for the target device when the simulator is run. "
"Optional param, only supported for mqtt.",
)

with self.argument_context("iot device c2d-message") as context:
context.argument(
Expand Down
17 changes: 16 additions & 1 deletion azext_iot/operations/_mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@


class mqtt_client(object):
def __init__(self, target, device_conn_string, device_id, method_response_code=None, method_response_payload=None):
def __init__(
self, target, device_conn_string, device_id,
method_response_code=None, method_response_payload=None, init_reported_properties=None
):
self.device_id = device_id
self.target = target
# The client automatically connects when we send/receive a message or method invocation
Expand All @@ -25,6 +28,7 @@ def __init__(self, target, device_conn_string, device_id, method_response_code=N
self.device_client.on_twin_desired_properties_patch_received = self.twin_patch_handler
self.printer = pprint.PrettyPrinter(indent=2)
self.default_data_encoding = 'utf-8'
self.init_reported_properties = init_reported_properties

def send_d2c_message(self, message_text, properties=None):
message = Message(message_text)
Expand Down Expand Up @@ -105,8 +109,19 @@ def twin_patch_handler(self, patch):
self.device_client.patch_twin_reported_properties(modified_properties)

def execute(self, data, properties={}, publish_delay=2, msg_count=100):
from azext_iot.operations.hub import _iot_device_twin_update
from tqdm import tqdm

try:
if self.init_reported_properties:
twin_properties = {
"properties": {
"desired": self.init_reported_properties
}
}

_iot_device_twin_update(self.target, self.device_id, twin_properties)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this functionality is to be able to init device side (reported) properties. The implementation here is setting service side desired properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to the desired properties is picked up by the MQTT listener, which automatically updates the reported properties, hence bringing the desired and reported properties in sync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you don't need to set desired properties to apply the initial reported state. Devices connecting to IoT Hub will provide their initial state through reported properties without needing to make a modification to desired. Desired twin properties are a solution side interaction and its strange that we would force an update to the desired twin state in order to update the reported side when using facilities of a device simulation command. The primary justification that I see right now is implementation convenience.

You can take a look at this device sample as an example/datapoint - the device connects to iothub initializing a set of reported properties (reflecting a pnp device) with no modification to the desired/solution side interaction.


for _ in tqdm(range(msg_count), desc='Device simulation in progress'):
self.send_d2c_message(message_text=data.generate(True), properties=properties)
sleep(publish_delay)
Expand Down
26 changes: 22 additions & 4 deletions azext_iot/operations/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -1705,15 +1705,24 @@ def iot_device_twin_update(
etag=None,
auth_type_dataplane=None,
):
from azext_iot.common.utility import verify_transform

discovery = IotHubDiscovery(cmd)
target = discovery.get_target(
hub_name=hub_name,
resource_group_name=resource_group_name,
login=login,
auth_type=auth_type_dataplane,
)
return _iot_device_twin_update(target, device_id, parameters, etag)


def _iot_device_twin_update(
target,
device_id,
parameters,
etag=None,
):
from azext_iot.common.utility import verify_transform

resolver = SdkResolver(target=target)
service_sdk = resolver.get_sdk(SdkType.service_sdk)

Expand Down Expand Up @@ -2418,7 +2427,8 @@ def iot_simulate_device(
resource_group_name=None,
login=None,
method_response_code=None,
method_response_payload=None
method_response_payload=None,
init_reported_properties=None
):
import sys
import uuid
Expand Down Expand Up @@ -2449,6 +2459,8 @@ def iot_simulate_device(
raise CLIError("'method-response-code' not supported, {} doesn't allow direct methods.".format(protocol_type))
if method_response_payload:
raise CLIError("'method-response-payload' not supported, {} doesn't allow direct methods.".format(protocol_type))
if init_reported_properties:
raise CLIError("'init-reported-properties' not supported, {} doesn't allow setting twin props".format(protocol_type))

properties_to_send = _iot_simulate_get_default_properties(protocol_type)
user_properties = validate_key_value_pairs(properties) or {}
Expand All @@ -2464,6 +2476,11 @@ def iot_simulate_device(
method_response_payload, argument_name="method-response-payload"
)

if init_reported_properties:
init_reported_properties = process_json_arg(
init_reported_properties, argument_name="init-reported-properties"
)

class generator(object):
def __init__(self):
self.calls = 0
Expand Down Expand Up @@ -2498,7 +2515,8 @@ def http_wrap(target, device_id, generator, msg_interval, msg_count):
device_conn_string=device_connection_string,
device_id=device_id,
method_response_code=method_response_code,
method_response_payload=method_response_payload
method_response_payload=method_response_payload,
init_reported_properties=init_reported_properties
)
client_mqtt.execute(data=generator(), properties=properties_to_send, publish_delay=msg_interval, msg_count=msg_count)
else:
Expand Down
6 changes: 6 additions & 0 deletions azext_iot/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"azext_iot.operations.hub._iot_hub_monitor_events"
)
path_iot_device_show = "azext_iot.operations.hub._iot_device_show"
path_update_device_twin = "azext_iot.operations.hub._iot_device_twin_update"
hub_entity = "myhub.azure-devices.net"

instance_name = generate_generic_id()
Expand Down Expand Up @@ -171,6 +172,11 @@ def fixture_monitor_events_entrypoint(mocker):
return mocker.patch(path_iot_hub_monitor_events_entrypoint)


@pytest.fixture()
def fixture_update_device_twin(mocker):
return mocker.patch(path_update_device_twin)


@pytest.fixture()
def fixture_iot_device_show_sas(mocker):
device = mocker.patch(path_iot_device_show)
Expand Down
57 changes: 32 additions & 25 deletions azext_iot/tests/iothub/test_iot_ext_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1998,28 +1998,31 @@ def test_generate_sas_token(self):

class TestDeviceSimulate:
@pytest.fixture(params=[204])
def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request, fixture_device, fixture_iot_device_show_sas):
def serviceclient(
self, mocker, fixture_ghcs, fixture_sas, request, fixture_device, fixture_iot_device_show_sas, fixture_update_device_twin
):
service_client = mocker.patch(path_service_client)
service_client.return_value = build_mock_response(mocker, request.param, {})
return service_client

@pytest.mark.parametrize(
"rs, mc, mi, protocol, properties, mrc, mrp",
"rs, mc, mi, protocol, properties, mrc, mrp, irp",
[
("complete", 1, 1, "http", None, None, None),
("reject", 1, 1, "http", None, None, None),
("abandon", 2, 1, "http", "iothub-app-myprop=myvalue;iothub-messageid=1", None, None),
("complete", 1, 1, "http", "invalidprop;content-encoding=utf-16", None, None),
("complete", 1, 1, "http", "iothub-app-myprop=myvalue;content-type=application/text", None, None),
("complete", 3, 1, "mqtt", None, None, None),
("complete", 3, 1, "mqtt", "invalid", None, None),
("complete", 2, 1, "mqtt", "myprop=myvalue;$.ce=utf-16", 201, None),
("complete", 2, 1, "mqtt", "myprop=myvalue;$.ce=utf-16", None, "{'result':'method succeded'}"),
("complete", 2, 1, "mqtt", "myinvalidprop;myvalidprop=myvalidpropvalue", 204, "{'result':'method succeded'}"),
("complete", 1, 1, "http", None, None, None, None),
("reject", 1, 1, "http", None, None, None, None),
("abandon", 2, 1, "http", "iothub-app-myprop=myvalue;iothub-messageid=1", None, None, None),
("complete", 1, 1, "http", "invalidprop;content-encoding=utf-16", None, None, None),
("complete", 1, 1, "http", "iothub-app-myprop=myvalue;content-type=application/text", None, None, None),
("complete", 3, 1, "mqtt", None, None, None, None),
("complete", 3, 1, "mqtt", "invalid", None, None, None),
("complete", 2, 1, "mqtt", "myprop=myvalue;$.ce=utf-16", 201, None, None),
("complete", 2, 1, "mqtt", "myprop=myvalue;$.ce=utf-16", None, "{'result':'method succeded'}", None),
("complete", 2, 1, "mqtt", "myinvalidprop;myvalidprop=myvalidpropvalue", 204, "{'result':'method succeded'}", None),
("complete", 2, 1, "mqtt", "myinvalidprop;myvalidprop=myvalidpropvalue", None, None, "{'rep_1':'val1', 'rep_2':2}"),
],
)
def test_device_simulate(
self, serviceclient, mqttclient, rs, mc, mi, protocol, properties, mrc, mrp
self, serviceclient, mqttclient, rs, mc, mi, protocol, properties, mrc, mrp, irp
):
from azext_iot.operations.hub import _iot_simulate_get_default_properties

Expand All @@ -2033,7 +2036,8 @@ def test_device_simulate(
protocol_type=protocol,
properties=properties,
method_response_code=mrc,
method_response_payload=mrp
method_response_payload=mrp,
init_reported_properties=irp
)

properties_to_send = _iot_simulate_get_default_properties(protocol)
Expand Down Expand Up @@ -2074,19 +2078,22 @@ def test_device_simulate(
assert serviceclient.call_count == 0

@pytest.mark.parametrize(
"rs, mc, mi, protocol, exception, mrc, mrp",
"rs, mc, mi, protocol, exception, mrc, mrp, irp",
[
("complete", 2, 0, "mqtt", CLIError, None, None),
("complete", 0, 1, "mqtt", CLIError, None, None),
("reject", 1, 1, "mqtt", CLIError, None, None),
("abandon", 1, 0, "http", CLIError, None, None),
("complete", 0, 1, "http", CLIError, 201, None),
("complete", 0, 1, "http", CLIError, None, "{'result':'method succeded'}"),
("complete", 0, 1, "http", CLIError, 201, "{'result':'method succeded'}"),
("complete", 2, 0, "mqtt", CLIError, None, None, None),
("complete", 0, 1, "mqtt", CLIError, None, None, None),
("complete", 1, 1, "mqtt", CLIError, 200, "invalid_method_response_payload", None),
("complete", 1, 1, "mqtt", CLIError, None, None, "invalid_reported_properties_format"),
("reject", 1, 1, "mqtt", CLIError, None, None, None),
("abandon", 1, 0, "http", CLIError, None, None, None),
("complete", 0, 1, "http", CLIError, 201, None, None),
("complete", 0, 1, "http", CLIError, None, "{'result':'method succeded'}", None),
("complete", 0, 1, "http", CLIError, 201, "{'result':'method succeded'}", None),
("complete", 0, 1, "http", CLIError, None, None, "{'rep_prop_1':'val1', 'rep_prop_2':'val2'}"),
],
)
def test_device_simulate_invalid_args(
self, serviceclient, rs, mc, mi, protocol, exception, mrc, mrp
self, serviceclient, rs, mc, mi, protocol, exception, mrc, mrp, irp
):
with pytest.raises(exception):
subject.iot_simulate_device(
Expand All @@ -2098,8 +2105,8 @@ def test_device_simulate_invalid_args(
msg_interval=mi,
protocol_type=protocol,
method_response_code=mrc,
method_response_payload=mrp

method_response_payload=mrp,
init_reported_properties=irp
)

def test_device_simulate_http_error(self, serviceclient_generic_error):
Expand Down
49 changes: 49 additions & 0 deletions azext_iot/tests/iothub/test_iot_messaging_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,55 @@ def test_uamqp_device_messaging(self):
expect_failure=True,
)

def test_mqtt_device_simulation_with_init_reported_properties(self):
device_count = 1
device_ids = self.generate_device_names(device_count)

self.cmd(
"iot hub device-identity create -d {} -n {} -g {}".format(
device_ids[0], LIVE_HUB, LIVE_RG
),
checks=[self.check("deviceId", device_ids[0])],
)

from azext_iot.operations.hub import iot_simulate_device
from azext_iot._factory import iot_hub_service_factory
from azure.cli.core.mock import DummyCli

cli_ctx = DummyCli()
client = iot_hub_service_factory(cli_ctx)

twin_init_props = {'prop_1': 'val_1', 'prop_2': 'val_2'}
twin_props_json = json.dumps(twin_init_props)

iot_simulate_device(
client,
device_ids[0],
LIVE_HUB,
"complete",
"Testing init reported twin properties",
2,
5,
"mqtt",
None,
None,
None,
None,
None,
twin_props_json
)

# get device twin
result = self.cmd(
"iot hub device-twin show -d {} --login {}".format(
device_ids[0], self.connection_string
)
).get_output_in_json()

assert result is not None
for key in twin_init_props:
assert result["properties"]["reported"][key] == twin_init_props[key]

def test_mqtt_device_direct_method_with_custom_response_status_payload(self):
device_count = 1
device_ids = self.generate_device_names(device_count)
Expand Down