diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36a252d6..07b368ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ env: CI_BUILD_AND_TEST_ROLE: arn:aws:iam::180635532705:role/V2_SDK_Unit_Testing CI_JOBS_SERVICE_CLIENT_ROLE: arn:aws:iam::180635532705:role/CI_JobsServiceClient_Role CI_SERVICE_ROLE_CFG_FOLDER: "./aws-iot-device-sdk-python-v2/servicetests/test_cases" + CI_SHADOW_SERVICE_CLIENT_ROLE: arn:aws:iam::180635532705:role/CI_ShadowServiceClient_Role jobs: @@ -207,6 +208,32 @@ jobs: chmod a+x builder ./builder build -p ${{ env.PACKAGE_NAME }} + - name: configure AWS credentials (service tests Shadow) + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ env.CI_SHADOW_SERVICE_CLIENT_ROLE }} + aws-region: ${{ env.AWS_DEFAULT_REGION }} + - name: run MQTT5 Shadow Update + working-directory: ./aws-iot-device-sdk-python-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt5_shadow_cfg.json + - name: run MQTT3 Shadow Update + working-directory: ./aws-iot-device-sdk-python-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt3_shadow_cfg.json + - name: run MQTT5 Named Shadow Update + working-directory: ./aws-iot-device-sdk-python-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt5_named_shadow_cfg.json + - name: run MQTT3 Named Shadow Update + working-directory: ./aws-iot-device-sdk-python-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt3_named_shadow_cfg.json + - name: configure AWS credentials (service tests Jobs) uses: aws-actions/configure-aws-credentials@v2 with: diff --git a/samples/utils/command_line_utils.py b/samples/utils/command_line_utils.py index 1a93ed4f..ddb63fd7 100644 --- a/samples/utils/command_line_utils.py +++ b/samples/utils/command_line_utils.py @@ -289,6 +289,8 @@ class CmdData: input_job_time : int # Shadow input_shadow_property : str + input_shadow_value : str + input_shadow_name : str # PKCS12 input_pkcs12_file : str input_pkcs12_password : str @@ -703,7 +705,10 @@ def parse_sample_input_shadow(): cmdUtils.register_command(CommandLineUtils.m_cmd_port, "", "Connection port. AWS IoT supports 443 and 8883 (optional, default=8883).", type=int) cmdUtils.register_command(CommandLineUtils.m_cmd_client_id, "", "Client ID to use for MQTT connection (optional, default='test-*').", default="test-" + str(uuid4())) cmdUtils.register_command(CommandLineUtils.m_cmd_thing_name, "", "The name assigned to your IoT Thing", required=True) - cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_property, "", "The name of the shadow property you want to change (optional, default='color'", default="color") + cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_property, "", "The name of the shadow property you want to change (optional, default=''", default="") + cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_value, "", "The desired value of the shadow property you want to set (optional)") + cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_name, "", "Shadow name (optional, default='')", type=str) + cmdUtils.register_command(CommandLineUtils.m_cmd_mqtt_version, "", "mqtt version (optional, default='5')", default=5, type=int) cmdUtils.get_args() cmdData = CommandLineUtils.CmdData() @@ -717,7 +722,10 @@ def parse_sample_input_shadow(): cmdData.input_proxy_port = int(cmdUtils.get_command(CommandLineUtils.m_cmd_proxy_port)) cmdData.input_thing_name = cmdUtils.get_command_required(CommandLineUtils.m_cmd_thing_name) cmdData.input_shadow_property = cmdUtils.get_command_required(CommandLineUtils.m_cmd_shadow_property) + cmdData.input_shadow_value = cmdUtils.get_command(CommandLineUtils.m_cmd_shadow_value, None) + cmdData.input_shadow_name = cmdUtils.get_command(CommandLineUtils.m_cmd_shadow_name, None) cmdData.input_is_ci = cmdUtils.get_command(CommandLineUtils.m_cmd_is_ci, None) != None + cmdData.input_mqtt_version = int(cmdUtils.get_command(CommandLineUtils.m_cmd_mqtt_version, 5)) return cmdData def parse_sample_input_websocket_connect(): @@ -876,6 +884,8 @@ def parse_sample_input_pkcs12_connect(): m_cmd_count = "count" m_cmd_group_identifier = "group_identifier" m_cmd_shadow_property = "shadow_property" + m_cmd_shadow_value = "shadow_value" + m_cmd_shadow_name = "shadow_name" m_cmd_pkcs12_file = "pkcs12_file" m_cmd_pkcs12_password = "pkcs12_password" m_cmd_region = "region" diff --git a/servicetests/test_cases/mqtt3_named_shadow_cfg.json b/servicetests/test_cases/mqtt3_named_shadow_cfg.json new file mode 100644 index 00000000..eabb3860 --- /dev/null +++ b/servicetests/test_cases/mqtt3_named_shadow_cfg.json @@ -0,0 +1,40 @@ +{ + "language": "Python", + "runnable_file": "./tests/ShadowUpdate/shadow_update.py", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "3" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + }, + { + "name": "--shadow_name", + "data": "testShadow" + } + ] +} diff --git a/servicetests/test_cases/mqtt3_shadow_cfg.json b/servicetests/test_cases/mqtt3_shadow_cfg.json new file mode 100644 index 00000000..7ca75d4d --- /dev/null +++ b/servicetests/test_cases/mqtt3_shadow_cfg.json @@ -0,0 +1,36 @@ +{ + "language": "Python", + "runnable_file": "./tests/ShadowUpdate/shadow_update.py", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "3" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + } + ] +} diff --git a/servicetests/test_cases/mqtt5_named_shadow_cfg.json b/servicetests/test_cases/mqtt5_named_shadow_cfg.json new file mode 100644 index 00000000..6153dde3 --- /dev/null +++ b/servicetests/test_cases/mqtt5_named_shadow_cfg.json @@ -0,0 +1,40 @@ +{ + "language": "Python", + "runnable_file": "./tests/ShadowUpdate/shadow_update.py", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "5" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + }, + { + "name": "--shadow_name", + "data": "testShadow" + } + ] +} diff --git a/servicetests/test_cases/mqtt5_shadow_cfg.json b/servicetests/test_cases/mqtt5_shadow_cfg.json new file mode 100644 index 00000000..c5ac2a1f --- /dev/null +++ b/servicetests/test_cases/mqtt5_shadow_cfg.json @@ -0,0 +1,36 @@ +{ + "language": "Python", + "runnable_file": "./tests/ShadowUpdate/shadow_update.py", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "5" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + } + ] +} diff --git a/servicetests/test_cases/test_shadow_update.py b/servicetests/test_cases/test_shadow_update.py new file mode 100644 index 00000000..46c71ef6 --- /dev/null +++ b/servicetests/test_cases/test_shadow_update.py @@ -0,0 +1,128 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-3.0. + +import argparse +import json +import os +import sys +import uuid +import time + +import boto3 + +import run_in_ci +import ci_iot_thing + + +def get_shadow_attrs(config_file): + with open(config_file) as f: + json_data = json.load(f) + shadow_name = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_name"), "") + shadow_property = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_property"), "") + shadow_desired_value = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_value"), "") + return [shadow_name, shadow_property, shadow_desired_value] + + +def main(): + argument_parser = argparse.ArgumentParser( + description="Run Shadow test in CI") + argument_parser.add_argument( + "--config-file", required=True, + help="JSON file providing command-line arguments for a test") + argument_parser.add_argument( + "--input-uuid", required=False, help="UUID for thing name. UUID will be generated if this option is omit") + argument_parser.add_argument( + "--region", required=False, default="us-east-1", help="The name of the region to use") + parsed_commands = argument_parser.parse_args() + + [shadow_name, shadow_property, shadow_desired_value] = get_shadow_attrs(parsed_commands.config_file) + print(f"Shadow name: '{shadow_name}'") + print(f"Shadow property: '{shadow_property}'") + print(f"Shadow desired value: '{shadow_desired_value}'") + + try: + iot_data_client = boto3.client('iot-data', region_name=parsed_commands.region) + secrets_client = boto3.client("secretsmanager", region_name=parsed_commands.region) + except Exception as e: + print(f"ERROR: Could not make Boto3 iot-data client. Credentials likely could not be sourced. Exception: {e}", + file=sys.stderr) + return -1 + + input_uuid = parsed_commands.input_uuid if parsed_commands.input_uuid else str(uuid.uuid4()) + + thing_name = "ServiceTest_Shadow_" + input_uuid + policy_name = secrets_client.get_secret_value( + SecretId="ci/ShadowServiceClientTest/policy_name")["SecretString"] + + # Temporary certificate/key file path. + certificate_path = os.path.join(os.getcwd(), "tests/ShadowUpdate/certificate.pem.crt") + key_path = os.path.join(os.getcwd(), "tests/ShadowUpdate/private.pem.key") + + try: + ci_iot_thing.create_iot_thing( + thing_name=thing_name, + region=parsed_commands.region, + policy_name=policy_name, + certificate_path=certificate_path, + key_path=key_path) + except Exception as e: + print(f"ERROR: Failed to create IoT thing: {e}") + sys.exit(-1) + + # Perform Shadow test. If it's successful, a shadow should appear for a specified thing. + try: + test_result = run_in_ci.setup_and_launch(parsed_commands.config_file, input_uuid) + except Exception as e: + print(f"ERROR: Failed to create shadow test: {e}") + test_result = -1 + + # Test reported success, verify that shadow was indeed updated. + if test_result == 0: + print("Verifying that shadow was updated") + shadow_value = None + try: + if shadow_name: + thing_shadow = iot_data_client.get_thing_shadow(thingName=thing_name, shadowName=shadow_name) + else: + thing_shadow = iot_data_client.get_thing_shadow(thingName=thing_name) + + payload = thing_shadow['payload'].read() + data = json.loads(payload) + shadow_value = data.get('state', {}).get('reported', {}).get(shadow_property, None) + if shadow_value != shadow_desired_value: + print(f"ERROR: Could not verify thing shadow: {shadow_property} is not set to desired value " + f"'{shadow_desired_value}'; shadow actual state: {data}") + test_result = -1 + except KeyError as e: + print(f"ERROR: Could not verify thing shadow: key {e} does not exist in shadow response: {thing_shadow}") + test_result = -1 + except Exception as e: + print(f"ERROR: Could not verify thing shadow: {e}") + test_result = -1 + + if test_result == 0: + print("Test succeeded") + + # Delete a thing created for this test run. + # NOTE We want to try to delete thing even if test was unsuccessful. + try: + ci_iot_thing.delete_iot_thing(thing_name, parsed_commands.region) + except Exception as e: + print(f"ERROR: Failed to delete thing: {e}") + # Fail the test if unable to delete thing, so this won't remain unnoticed. + test_result = -1 + + try: + if os.path.isfile(certificate_path): + os.remove(certificate_path) + if os.path.isfile(key_path): + os.remove(key_path) + except Exception as e: + print(f"WARNING: Failed to delete local files: {e}") + + if test_result != 0: + sys.exit(-1) + + +if __name__ == "__main__": + main() diff --git a/servicetests/tests/JobsExecution/jobs.py b/servicetests/tests/JobsExecution/jobs.py index 80101787..c9be3fda 100644 --- a/servicetests/tests/JobsExecution/jobs.py +++ b/servicetests/tests/JobsExecution/jobs.py @@ -76,7 +76,6 @@ def exit(msg_or_exception): print("Disconnecting...") locked_data.disconnect_called = True if cmdData.input_mqtt_version == 5: - locked_data.disconnect_called = True mqtt5_client.stop() else: future = mqtt_connection.disconnect() @@ -335,7 +334,6 @@ def on_lifecycle_stopped(lifecycle_stopped_data: mqtt5.LifecycleStoppedData): print("Unsopported MQTT version number\n") sys.exit(-1) - print("Connected!") try: diff --git a/servicetests/tests/ShadowUpdate/shadow_update.py b/servicetests/tests/ShadowUpdate/shadow_update.py new file mode 100644 index 00000000..00d5116d --- /dev/null +++ b/servicetests/tests/ShadowUpdate/shadow_update.py @@ -0,0 +1,475 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0. + +from time import sleep +from awscrt import mqtt, mqtt5, http +from awsiot import iotshadow, mqtt_connection_builder, mqtt5_client_builder +from concurrent.futures import Future +import sys +import threading +import traceback +from uuid import uuid4 +from utils.command_line_utils import CommandLineUtils + +# - Overview - +# This sample uses the AWS IoT Device Shadow Service to keep a property in +# sync between device and server. Imagine a light whose color may be changed +# through an app, or set by a local user. +# +# - Instructions - +# Once connected, type a value in the terminal and press Enter to update +# the property's "reported" value. The sample also responds when the "desired" +# value changes on the server. To observe this, edit the Shadow document in +# the AWS Console and set a new "desired" value. +# +# - Detail - +# On startup, the sample requests the shadow document to learn the property's +# initial state. The Test subscribes to modified events from the server, +# which are sent when a property's value changes + +# cmdData is the arguments/input from the command line placed into a single struct for +# use in this sample. This handles all of the command line parsing, validating, etc. +# See the Utils/CommandLineUtils for more information. +cmdData = CommandLineUtils.parse_sample_input_shadow() + +# Using globals to simplify sample code +is_sample_done = threading.Event() +mqtt_connection = None +shadow_thing_name = cmdData.input_thing_name +shadow_property = cmdData.input_shadow_property +mqtt_qos = None + +# MQTT5 specific +mqtt5_client = None +future_connection_success = Future() +update_received = Future() + + +SHADOW_VALUE_DEFAULT = "off" + +class LockedData: + def __init__(self): + self.lock = threading.Lock() + self.shadow_value = None + self.disconnect_called = False + self.request_tokens = set() + +locked_data = LockedData() + +# Function for gracefully quitting this sample +def exit(msg_or_exception): + if isinstance(msg_or_exception, Exception): + print("Exiting sample due to exception.") + traceback.print_exception(msg_or_exception.__class__, msg_or_exception, sys.exc_info()[2]) + else: + print("Exiting sample:", msg_or_exception) + + with locked_data.lock: + if not locked_data.disconnect_called: + locked_data.disconnect_called = True + if cmdData.input_mqtt_version == 5: + print("Stop the client...") + mqtt5_client.stop() + else: + print("Disconnecting...") + future = mqtt_connection.disconnect() + future.add_done_callback(on_disconnected) + + +def on_disconnected(disconnect_future): + # type: (Future) -> None + print("Disconnected.") + + # Signal that sample is finished + is_sample_done.set() + + +def on_get_shadow_accepted(response): + # type: (iotshadow.GetShadowResponse) -> None + try: + with locked_data.lock: + # check that this is a response to a request from this session + try: + locked_data.request_tokens.remove(response.client_token) + except KeyError: + print("Ignoring get_shadow_accepted message due to unexpected token.") + return + + if response.state: + if response.state.delta: + value = response.state.delta.get(shadow_property) + if value: + print(" Shadow contains delta value '{}'.".format(value)) + change_shadow_value(value) + return + + if response.state.reported: + value = response.state.reported.get(shadow_property) + if value: + print(" Shadow contains reported value '{}'.".format(value)) + return + + print(" Shadow document lacks '{}' property. Setting defaults...".format(shadow_property)) + change_shadow_value(SHADOW_VALUE_DEFAULT) + return + + except Exception as e: + exit(e) + + +def on_get_shadow_rejected(error): + # type: (iotshadow.ErrorResponse) -> None + try: + # check that this is a response to a request from this session + with locked_data.lock: + try: + locked_data.request_tokens.remove(error.client_token) + except KeyError: + print("Ignoring get_shadow_rejected message due to unexpected token.") + return + + if error.code == 404: + print("Thing has no shadow document. Creating with defaults...") + change_shadow_value(SHADOW_VALUE_DEFAULT) + else: + exit("Get request was rejected. code:{} message:'{}'".format( + error.code, error.message)) + except Exception as e: + exit(e) + + +def on_publish_update_shadow(future): + # type: (Future) -> None + try: + future.result() + print("Update request published.") + except Exception as e: + print("Failed to publish update request.") + exit(e) + + +def on_update_shadow_accepted(response): + # type: (iotshadow.UpdateShadowResponse) -> None + try: + # check that this is a response to a request from this session + with locked_data.lock: + try: + locked_data.request_tokens.remove(response.client_token) + except KeyError: + print("Ignoring update_shadow_accepted message due to unexpected token.") + return + try: + if response.state.reported is not None: + if shadow_property in response.state.reported: + print("Finished updating reported shadow value to '{}'.".format( + response.state.reported[shadow_property])) # type: ignore + else: + print("Could not find shadow property with name: '{}'.".format(shadow_property)) # type: ignore + else: + print("Shadow states cleared.") # when the shadow states are cleared, reported and desired are set to None + print("1- Enter desired value: ") # remind user they can input new values + except BaseException: + exit("Updated shadow is missing the target property") + + except Exception as e: + exit(e) + + +def on_update_shadow_rejected(error): + # type: (iotshadow.ErrorResponse) -> None + try: + # check that this is a response to a request from this session + with locked_data.lock: + try: + locked_data.request_tokens.remove(error.client_token) + except KeyError: + print("Ignoring update_shadow_rejected message due to unexpected token.") + return + + exit("Update request was rejected. code:{} message:'{}'".format( + error.code, error.message)) + + except Exception as e: + exit(e) + + +def change_shadow_value(value): + with locked_data.lock: + + print("Changed local shadow value to '{}'.".format(value)) + locked_data.shadow_value = value + + print("Updating reported shadow value to '{}'...".format(value)) + + # use a unique token so we can correlate this "request" message to + # any "response" messages received on the /accepted and /rejected topics + token = str(uuid4()) + + # if the value is "none" then set it to a Python none object to + request = iotshadow.UpdateShadowRequest( + thing_name=shadow_thing_name, + state=iotshadow.ShadowState( + reported={shadow_property: value}, + desired={shadow_property: value}, + ), + client_token=token, + ) + future = shadow_client.publish_update_shadow(request, mqtt_qos) + locked_data.request_tokens.add(token) + future.add_done_callback(on_publish_update_shadow) + + +def update_event_received(response): + print("Update Event Received\n") + print("Current response", response.current) + print("Previous response", response.previous) + global update_received + update_received.set_result(0) + + +# MQTT5 specific functions +# Callback for the lifecycle event Connection Success +def on_lifecycle_connection_success(lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData): + print("Lifecycle Connection Success") + global future_connection_success + future_connection_success.set_result(lifecycle_connect_success_data) + + +# Callback for the lifecycle event on Client Stopped +def on_lifecycle_stopped(lifecycle_stopped_data: mqtt5.LifecycleStoppedData): + print("Client Stopped.") + # Signal that sample is finished + is_sample_done.set() + + +def update_named_shadow(): + print("Updating named shadow") + # named shadow here + named_shadow = cmdData.input_shadow_name + try: + # Subscribe to necessary topics. + # Note that is **is** important to wait for "accepted/rejected" subscriptions + # to succeed before publishing the corresponding "request". + + print("Subscribing to Update responses...") + update_accepted_subscribed_future, _ = shadow_client.subscribe_to_update_named_shadow_accepted( + request=iotshadow.UpdateNamedShadowSubscriptionRequest(shadow_name=named_shadow, thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_update_shadow_accepted) + + update_rejected_subscribed_future, _ = shadow_client.subscribe_to_update_named_shadow_rejected( + request=iotshadow.UpdateNamedShadowSubscriptionRequest(shadow_name=named_shadow, thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_update_shadow_rejected) + + # Wait for subscriptions to succeed + update_accepted_subscribed_future.result() + update_rejected_subscribed_future.result() + + print("Subscribing to Get responses...") + get_accepted_subscribed_future, _ = shadow_client.subscribe_to_get_named_shadow_accepted( + request=iotshadow.GetNamedShadowSubscriptionRequest(shadow_name=named_shadow,thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_get_shadow_accepted) + + get_rejected_subscribed_future, _ = shadow_client.subscribe_to_get_named_shadow_rejected( + request=iotshadow.GetNamedShadowSubscriptionRequest(shadow_name=named_shadow, thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_get_shadow_rejected) + + # Wait for subscriptions to succeed + get_accepted_subscribed_future.result() + get_rejected_subscribed_future.result() + + # Issue request for shadow's current state. + # The response will be received by the on_get_accepted() callback + print("Requesting current shadow state...") + + with locked_data.lock: + # use a unique token so we can correlate this "request" message to + # any "response" messages received on the /accepted and /rejected topics + token = str(uuid4()) + + publish_get_future = shadow_client.publish_get_named_shadow( + request=iotshadow.GetNamedShadowRequest(shadow_name=named_shadow, thing_name=shadow_thing_name, client_token=token), + qos=mqtt_qos) + + locked_data.request_tokens.add(token) + + # Ensure that publish succeeds + publish_get_future.result() + + subscribe_future, _ = shadow_client.subscribe_to_named_shadow_updated_events( + request=iotshadow.NamedShadowUpdatedSubscriptionRequest(shadow_name=named_shadow, thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=update_event_received) + subscribe_future.result() + + state=iotshadow.ShadowState( + reported={shadow_property: cmdData.input_shadow_value}, + desired={shadow_property: cmdData.input_shadow_value}, + token=token) + + update_thing_update_future = shadow_client.publish_update_named_shadow(request = iotshadow.UpdateNamedShadowRequest + (shadow_name = named_shadow, thing_name = shadow_thing_name, state=state), qos=mqtt_qos) + + # Wait for subscriptions to succeed + update_thing_update_future.result() + + except Exception as e: + exit(e) + + +def update_shadow(): + print("Updating classic shadow") + try: + # Subscribe to necessary topics. + # Note that is **is** important to wait for "accepted/rejected" subscriptions + # to succeed before publishing the corresponding "request". + print("Subscribing to Update responses...") + update_accepted_subscribed_future, _ = shadow_client.subscribe_to_update_shadow_accepted( + request=iotshadow.UpdateShadowSubscriptionRequest(thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_update_shadow_accepted) + + update_rejected_subscribed_future, _ = shadow_client.subscribe_to_update_shadow_rejected( + request=iotshadow.UpdateShadowSubscriptionRequest(thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_update_shadow_rejected) + + # Wait for subscriptions to succeed + update_accepted_subscribed_future.result() + update_rejected_subscribed_future.result() + + print("Subscribing to Get responses...") + get_accepted_subscribed_future, _ = shadow_client.subscribe_to_get_shadow_accepted( + request=iotshadow.GetShadowSubscriptionRequest(thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_get_shadow_accepted) + + get_rejected_subscribed_future, _ = shadow_client.subscribe_to_get_shadow_rejected( + request=iotshadow.GetShadowSubscriptionRequest(thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=on_get_shadow_rejected) + + # Wait for subscriptions to succeed + get_accepted_subscribed_future.result() + get_rejected_subscribed_future.result() + + # Issue request for shadow's current state. + # The response will be received by the on_get_accepted() callback + print("Requesting current shadow state...") + + with locked_data.lock: + # use a unique token so we can correlate this "request" message to + # any "response" messages received on the /accepted and /rejected topics + token = str(uuid4()) + + publish_get_future = shadow_client.publish_get_shadow( + request=iotshadow.GetShadowRequest(thing_name=shadow_thing_name, client_token=token), + qos=mqtt_qos) + + locked_data.request_tokens.add(token) + + # Ensure that publish succeeds + publish_get_future.result() + + subscribe_future, _ = shadow_client.subscribe_to_shadow_updated_events( + request=iotshadow.ShadowUpdatedSubscriptionRequest(thing_name=shadow_thing_name), + qos=mqtt_qos, + callback=update_event_received) + subscribe_future.result() + + state=iotshadow.ShadowState( + reported={shadow_property: cmdData.input_shadow_value}, + desired={shadow_property: cmdData.input_shadow_value}, + token=token) + + update_thing_update_future = shadow_client.publish_update_shadow(request = iotshadow.UpdateShadowRequest + (thing_name = shadow_thing_name, state=state), qos=mqtt_qos) + + change_shadow_value(cmdData.input_shadow_value) + update_thing_update_future.result() + + except Exception as e: + exit(e) + + +if __name__ == '__main__': + # Create the proxy options if the data is present in cmdData + proxy_options = None + if cmdData.input_proxy_host is not None and cmdData.input_proxy_port != 0: + proxy_options = http.HttpProxyOptions( + host_name=cmdData.input_proxy_host, + port=cmdData.input_proxy_port) + + if cmdData.input_mqtt_version == 5: + mqtt_qos = mqtt5.QoS.AT_LEAST_ONCE + # Create a mqtt5 connection from the command line data + mqtt5_client = mqtt5_client_builder.mtls_from_path( + endpoint=cmdData.input_endpoint, + port=cmdData.input_port, + cert_filepath=cmdData.input_cert, + pri_key_filepath=cmdData.input_key, + ca_filepath=cmdData.input_ca, + client_id=cmdData.input_clientId, + clean_session=False, + keep_alive_secs=30, + http_proxy_options=proxy_options, + on_lifecycle_connection_success=on_lifecycle_connection_success, + on_lifecycle_stopped=on_lifecycle_stopped) + print(f"Connecting to {cmdData.input_endpoint} with client ID '{cmdData.input_clientId}' with MQTT5...") + + mqtt5_client.start() + + shadow_client = iotshadow.IotShadowClient(mqtt5_client) + future_connection_success.result() + + # Wait for connection to be fully established. + # Note that it's not necessary to wait, commands issued to the + # mqtt5_client before its fully connected will simply be queued. + # But this sample waits here so it's obvious when a connection + # fails or succeeds. + elif cmdData.input_mqtt_version == 3: + mqtt_qos = mqtt.QoS.AT_LEAST_ONCE + # Create a MQTT connection from the command line data + mqtt_connection = mqtt_connection_builder.mtls_from_path( + endpoint=cmdData.input_endpoint, + port=cmdData.input_port, + cert_filepath=cmdData.input_cert, + pri_key_filepath=cmdData.input_key, + ca_filepath=cmdData.input_ca, + client_id=cmdData.input_clientId, + clean_session=False, + keep_alive_secs=30, + http_proxy_options=proxy_options) + + connected_future = mqtt_connection.connect() + + shadow_client = iotshadow.IotShadowClient(mqtt_connection) + + # Wait for connection to be fully established. + # Note that it's not necessary to wait, commands issued to the + # mqtt_connection before its fully connected will simply be queued. + # But this sample waits here so it's obvious when a connection + # fails or succeeds. + connected_future.result() + else: + print("Unsopported MQTT version number\n") + sys.exit(-1) + + print("Connected!") + + if not cmdData.input_shadow_name: + update_shadow() + else: + update_named_shadow() + + print("waitin on update event\n") + update_received.result(); + exit(0) + # Wait for the sample to finish + is_sample_done.wait() + + +